Compare commits

...

50 Commits

Author SHA1 Message Date
Bee 0cca4226d1 feat: refactor UseCustomPrompt into reusable component, add to Ollama (#5818)
* feat: refactor UseCustomPrompt into reusable component, add to Ollama

Refactor custom prompt checkbox functionality from LMStudioProvider and OllamaProvider into a shared UseCustomPrompt component to reduce code duplication and improve maintainability.

* Add changeset

* replace key with providerId

* clean up

* Rename UseCustomPrompt to UseCustomPromptCheckbox and update imports

Rename UseCustomPrompt.tsx to UseCustomPromptCheckbox.tsx for better clarity
and update import paths in LMStudioProvider and OllamaProvider components.
2025-08-28 10:30:58 -07:00
Toshii 7a9bb4278f read_file tool call change for all models (#5830) 2025-08-28 10:30:58 -07:00
Bee d2d171e0c2 template-based system prompt (#5731)
* Refactor system prompt architecture with new template-based system

- Move existing system prompt files to legacy directory
- Implement new modular system with PromptBuilder, PromptRegistry, and TemplateEngine
- Add component-based prompt structure with reusable parts (capabilities, rules, tool_use, etc.)
- Create variant-specific templates for generic and next-gen models
- Add comprehensive test suite with snapshots for different model configurations
- Introduce template engine with placeholder support for dynamic prompt generation

* Refactor system prompt architecture with modular tool definitions

- Extract tool specifications into dedicated modules under tools/
- Add ClineToolSet class for managing tool variants by model family
- Restructure prompt components with centralized index exports
- Update prompt builder and registry to support new tool architecture
- Reorganize shared utilities and type definitions
- Update all test snapshots to reflect new prompt structure

* Update snapshots

* reorg

* Update template format

* clean up

* typos

* focus chain section

* fix task progress in attempt_completion

* Implement tool retrieval with fallback options in PromptBuilder

- Added `getToolByNameWithFallback` and `getToolsForVariantWithFallback` methods to `ClineToolSet` for improved tool resolution.
- Updated `getToolsPrompts` in `PromptBuilder` to utilize these new methods, allowing for better handling of tool requests with fallback to generic tools.
- Enhanced sorting and filtering of tools based on context requirements and requested order.

* update fild structure

* clean up

* fix static test string

* Update snapshot names

* Update unit test

* Remove unused placeholders and update docs

* Update README on how to add new tool

* Remove task_progress reference from attempt_completion tool description when focus chain is disabled
2025-08-28 10:30:58 -07:00
Bee df283a0d49 fix: AutoApproveModal positioning and scrolling behavior (#5819)
* fix: AutoApproveModal positioning and scrolling behavior

- Add dynamic positioning calculation to prevent modal overflow
- Implement proper flex layout with scrollable content container
- Ensure minimum usable height and top margin constraints
- Fix modal positioning when button is near viewport edges

* Add changeset

* clean up
2025-08-28 10:30:58 -07:00
celestial-vault 3ba6c3bf31 infer state key types from existing interfaces (#5815) 2025-08-28 10:30:58 -07:00
celestial-vault 80f581e750 rename CacheService to StateManager (#5681)
* rename CacheService to StateManager

* fix types
2025-08-28 10:30:58 -07:00
canvrno b25262e7df Changes to condenseToolResponse & summarizeTask prompting (#5817)
* Condense & deep planning prompt adjustments

* Removed ps prompting ready for PR

* rebase

* Fixed typo on one word

---------

Co-authored-by: Kevin Bond <kevin@Kevins-MacBook-Pro.local>
2025-08-28 10:30:58 -07:00
canvrno 73a8543df7 deep-planning prompt PowerShell (#5699)
* Windows/Powershell specific deep planning prompt changes

* Prompt adjustments

---------

Co-authored-by: Kevin Bond <kevin@Kevins-MacBook-Pro.local>
2025-08-28 10:30:58 -07:00
Bee 0ac6777ce3 fix: remove hardcoded Ollama host from options (#5816)
* fix: remove hardcoded Ollama host from options

Updates the Ollama handler to remove the hardcoded "http://localhost:11434" as the `ollamaBaseUrl` fallback option for the host to allow the Ollama SDK to handle the default endpoint configured on users' machine.

Reason: Ollama allows cross-origin requests from 127.0.0.1 and 0.0.0.0 by default. However, when we use localhost, the browser would resolve it through DNS, which can result in different IP addresses.

Docs: https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network

* add changeset
2025-08-28 10:30:58 -07:00
Bee 3701b4df6c Increase horizontal margin in AutoApproveBar component (#5813)
Update the mx-[5px] to mx-[15px] in the div's className to adjust horizontal spacing for improved layout alignment.
2025-08-28 10:30:58 -07:00
Bee d9548271af Remove eslint-rules test patterns from Mocha spec configuration (#5812)
Update the "spec" array in .mocharc.json to exclude "eslint-rules/__tests__/**/*.test.ts",
as that directory has been removed.
2025-08-28 10:30:58 -07:00
canvrno 89980e5192 Focus chain telemetry tweaks (#5810)
Co-authored-by: Kevin Bond <kevin@Kevins-MacBook-Pro.local>
2025-08-28 10:30:58 -07:00
pashpashpash a9a3812fd9 add custom gpt-5 system prompt (#5757)
* gpt-5 system prompt

* add changeset
2025-08-28 10:30:58 -07:00
celestial-vault d2aa530edc add focus chain settings to statemanager initialize function (#5798) 2025-08-28 10:30:58 -07:00
Yunus Emre AYHAN 9b639a0203 Dify.ai integration (#5761) 2025-08-28 10:30:58 -07:00
Toshii bc0a45ee91 removing middle out from params to or / cline providers (#5811) 2025-08-28 10:30:58 -07:00
Bee 2a5a67f9ef Remove top padding from ActionButtons component (#5806)
Eliminate unnecessary top padding in the chat view.
2025-08-28 10:30:58 -07:00
github-actions[bot] 293d9396d5 v3.26.6 Release Notes (#5788)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md for version 3.26.6 with user-friendly descriptions

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: pashpashpash <nik@cline.bot>
2025-08-28 10:30:58 -07:00
pashpashpash 608bdba50e add grok coder free model to cline provider (#5808)
* add free grok-coder-free model to cline provider

* add changeset

* fix typo
2025-08-28 10:30:57 -07:00
celestial-vault 2f55e00acc Use VS Code CSS variables for markdown/codeblock styling (#5783)
* use css variables for highlight styling and remove theme subscription along with vscode theme to highlight pipeline logic

* changeset

* Remove monaco-vscode-textmate-theme-converter

* Remove unnecessary markdown css

* Remove package-lock.json from version control

* re-add package-lock

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-08-28 10:30:57 -07:00
Saoud Rizwan fef7e2d3c6 fix: 'disable checkpoints' button font size does not match surrounding text (#5787)
* fix: 'disable checkpoints' button font size does not match surrounding text

* Remove non-existant font-inherit
2025-08-28 10:30:57 -07:00
Tomás Barreiro 8be4255f2f fix: provider options (#5204)
* Pass all options to the handlers

* Add changeset

* Do not pass all options

* Have onRetryAttempt as a common option

* Do not duplicate the onRetryAttempt definition
2025-08-28 10:30:57 -07:00
evinelias e690832569 Feature/Qwen Code CLI API Support with OAuth (#5766)
* feat: Integrate Qwen Code API with OAuth authentication

- Add Qwen Code API provider with OAuth2 authentication flow
- Implement QwenCodeProvider component for settings UI
- Add qwenCodeOauthPath to CacheService state management
- Update protobuf models with QWEN_CODE provider type
- Fix protobuf enum values (VSCODE_LM changed from 15 to 33)
- Add comprehensive API configuration conversion support
- Update build scripts to use system protoc for Windows compatibility
- Optimize VSIX packaging by excluding reference codebase
- Follow camelCase convention: qwen_code_oauth_path  qwenCodeOauthPath

* docs: Add Qwen Code API integration documentation

- Document OAuth2 authentication flow and features
- Highlight enterprise-grade security capabilities
- Include setup instructions for credential management
- Emphasize automatic token refresh and caching features

* cleanup: Clean up build configuration for production release

- Revert protoc path to use grpc-tools instead of exposing system path
- Restore vscode:prepublish script for proper VS Code marketplace publication
- Remove reference codebase exclusion from .vscodeignore for cleaner packaging

* refactor: Remove Windows platform check from build-proto script

- Remove unnecessary isWindows variable and platform-specific logic
- Simplify TS_PROTO_PLUGIN to use standard require.resolve approach
- Improve cross-platform compatibility and code clarity

* restore: Restore Windows platform check in build-proto script

- Add back isWindows platform detection variable
- Restore Windows-specific TS_PROTO_PLUGIN logic using .cmd file
- Maintain cross-platform compatibility for Windows builds

* Update ApiOptions.tsx

* Remove Qwen Code API integration section

* Revert README

* Fix protos order

* Revert change

* Remove validation for qwen code

---------

Co-authored-by: Ara <arafat.da.khan@gmail.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-08-28 10:30:57 -07:00
pashpashpash 93ef27557b add gpt-5 to recommended list (#5789) 2025-08-28 10:30:57 -07:00
Toshii 5e889ccf4f remove middle out for gpt5 (#5786)
* remove middle out for gpt5

* refactor: using standard family identifier functions

* fix: making sure to lowercase all model ids

* changeset

---------

Co-authored-by: pashpashpash <nik@nugbase.com>
2025-08-28 10:30:57 -07:00
github-actions[bot] 99d84246e4 v3.26.5 Release Notes (#5785)
* changeset version bump

* Updating CHANGELOG.md format

* Revise changelog for version 3.26.5

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-08-28 10:30:57 -07:00
Saoud Rizwan a70e2ec31a Fix OVSX publish command (#5784)
* Fix OVSX publish command

* Fix OVSX publish command
2025-08-28 10:30:57 -07:00
github-actions[bot] c8aae13268 v3.26.4 Release Notes (#5769)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md

* Update version from 3.27.0 to 3.26.4

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-08-28 10:30:57 -07:00
Walter Korman 5d6a367181 fix (provider/vercel-ai-gateway): load model list once on settings view display (#5776) 2025-08-28 10:30:57 -07:00
Saoud Rizwan 67607b3f8e Allow packaging secrets in VSCE publish command (#5782)
* Allow packaging secrets in VSCE publish command

* Create shaggy-beans-yell.md

* Revert "fix publish yml action (#5770)"

This reverts commit 357f98c707.
2025-08-28 10:30:57 -07:00
Saoud Rizwan 328b9cbf5b Revert "Scenario Test Workflow (#5711)" (#5781)
This reverts commit b45168f6a3.
2025-08-28 10:30:57 -07:00
pashpashpash 3bddcff52d fix publish yml action (#5770) 2025-08-28 10:30:57 -07:00
Ara df02404b4e ReOrder Model List (#5767) 2025-08-28 10:30:57 -07:00
Dennise Bartlett 9033c65214 Allow packaging secrets in VSCE command 2025-08-28 10:30:57 -07:00
pashpashpash a69f9679ff Revert "Build package per commit (#4570)" (#5768)
This reverts commit fe8ab85e1a.
2025-08-28 10:30:57 -07:00
Mark Percival 115b122225 chore: remove disabled 'codespell' workflow (#5662) 2025-08-28 10:30:57 -07:00
Mark Percival f4a97d67d8 chore: Remove 'old_docs' documentation (#5661) 2025-08-28 10:30:57 -07:00
yuvalman 3fd42632c8 feat: sap provider - support reasoning effort for open ai models (#5691)
* feat: sap provider - support reasoning effort for open ai models

* feat: sap provider - support reasoning effort for open ai models

* feat: sap provider - support reasoning effort for open ai models

* feat: sap provider - support reasoning effort for open ai models

* feat: sap provider - support reasoning effort for open ai models
2025-08-28 10:30:57 -07:00
dylan 8f7b865e23 feat: add new models from Nebius AI Studio with correct pricing (#5729) 2025-08-28 10:30:57 -07:00
Lize Cai 62c5980eb3 fix: Claude 4 image processing in SAP AI Core provider (#5735)
* fix converse api image data to base64 string.

Signed-off-by: Lize Cai <lize.cai@sap.com>

* add test cases

Signed-off-by: Lize Cai <lize.cai@sap.com>

---------

Signed-off-by: Lize Cai <lize.cai@sap.com>
2025-08-28 10:30:56 -07:00
Ann-Holmes 61f4143620 feat(deepseek): Update DeepSeek models context window from 64K to 128K (#5751)
- Updated contextWindow for deepseek-chat and deepseek-reasoner models from 64_000 to 128_000
- Modified context-window-utils.ts to handle DeepSeek models with 128K context window instead of 64K
- This change aligns with DeepSeek's official API documentation and improves model performance
2025-08-28 10:30:56 -07:00
github-actions[bot] 8e9dde7903 v3.26.3 Release Notes (#5759)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md for version 3.26.3 release

- Add user-friendly descriptions for compact system prompt feature
- Add proper version formatting with brackets
- Improve clarity of LM Studio and token usage tracking features

* package lock

* changelog

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: pashpashpash <nik@cline.bot>
2025-08-28 10:30:56 -07:00
Tomás Barreiro 3b9b386099 Build package per commit (#4570)
* Build the extension on every commit push

* Publish using the vsix path

* Fix tag resolution

* Remove `while ;`

* Remove test trigger

* use while true;

* Update package.json

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

* Use high capacity runner

* add if check to only run in the Cline repo

* Conditionally select a runner

---------

Co-authored-by: Dennise Bartlett <bartlett.dc.1@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-28 10:30:56 -07:00
Bee c649c371de feat: Support compact system prompt for LM studio models and token usage tracking (#5720)
* Add compact system prompt for local models

- Introduce compact system prompt for local models (lm studio, ollama)
- Add ApiProviderInfo { modelId, providerId } to API
- Persist promptType in global state and propagate to webview
  (updateSettings, state keys/helpers, ExtensionMessage, UI context)
- Wire provider info through task pipeline to buildSystemPrompt

* isLocalModelFamily

* Add custom prompt support and LM Studio API improvements

* Enable token usage tracking in LM Studio stream responses

This change adds the stream_options parameter with include_usage: true to LM Studio API requests, allowing the system to receive token usage information along with streaming responses. This enables better tracking of token consumption for LM Studio model interactions.

* update compact system prompt

* feat: Support compact system prompt for LM studio models and token usage tracking

* clean up

* Update UI helper text
2025-08-28 10:30:56 -07:00
github-actions[bot] c6fe0854d3 v3.26.2 Release Notes (#5736)
* changeset version bump

* Updating CHANGELOG.md format

* changelog

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: pashpashpash <nik@cline.bot>
2025-08-28 10:30:56 -07:00
pashpashpash a3f43d2095 removing max tokens from sonic model (#5742) 2025-08-28 10:30:56 -07:00
Nick Baumann 14615d44c4 docs: Vercel AI Gateway provider page (#5743) 2025-08-28 10:30:56 -07:00
Joyce Er d8c9776328 fix: small typo in focus chain settings page (#5744) 2025-08-28 10:30:56 -07:00
Saoud Rizwan 623c0833e6 Fix openrouter/cline gpt-5 contextWindow by setting to 272k since it's inaccurately reported as 400k (#5738) 2025-08-28 10:30:56 -07:00
Sarah Fortune f47d1670c5 Don't throw an exception if deleting a directory fail.
The external diff view provider throws an error while reverting the diff if the directory is missing. This is not a real error though, we should continue the revert even if deleting the directories failed.

Fixes:

```
[2025-08-27T22:52:23.673] #bot.cline.server.ts ProtoBus handler error: /cline.TaskService/clearTask
Error: ENOENT: no such file or directory, rmdir '/Users/sjf/plugin/.github/actions/build-test-upload'
    at async Object.rmdir (node:internal/fs/promises:818:10)
    at async ExternalDiffViewProvider.revertChanges (/path/to/cline/core/0.0.1/cline-core.js:848418:9)
    at async Task.abortTask (/path/to/cline/core/0.0.1/cline-core.js:830605:9)
    at async Controller.clearTask (/path/to/cline/core/0.0.1/cline-core.js:832296:9)
    at async clearTask (/path/to/cline/core/0.0.1/cline-core.js:658823:3)
    at async Object.clearTask (/path/to/cline/core/0.0.1/cline-core.js:872829:23)
  ```
2025-08-28 10:30:56 -07:00
259 changed files with 18835 additions and 6337 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Reasoning budget is now configurable for all supported models from OpenRouter.
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fix Ollama connection issue to default endpoint at port 11434
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Optimized Cline for GPT-5 model family with an aligned system prompt
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
add \*.go files to deep-planning prompt
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Add "Use custom prompt" option to Ollama provider
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fix AutoApproveModal overflowing issue
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Dify.ai api integration
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Prompt changes for deep-planning in windows/powershell
-6
View File
@@ -1,6 +0,0 @@
[codespell]
# Ref: https://github.com/codespell-project/codespell#using-a-config-file
skip = .git*,*.svg,package-lock.json,*.css,.codespellrc,locales
check-hidden = true
ignore-regex = (\b(optIn|isTaller)\b|https://\S+)
# ignore-words-list =
-28
View File
@@ -1,28 +0,0 @@
# Codespell configuration is within .codespellrc
---
name: Codespell
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
jobs:
codespell:
if: false
name: Check for spelling errors
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Annotate locations with typos
uses: codespell-project/codespell-problem-matcher@v1
- name: Codespell
uses: codespell-project/actions-codespell@v2
with:
only_warn: 1
+1 -1
View File
@@ -97,7 +97,7 @@ jobs:
CLINE_ENVIRONMENT: production
run: |
# Required to generate the .vsix
vsce package --out "cline-${{ steps.get_version.outputs.version }}.vsix"
vsce package --allow-package-secrets sendgrid --out "cline-${{ steps.get_version.outputs.version }}.vsix"
if [ "${{ github.event.inputs.release-type }}" = "pre-release" ]; then
npm run publish:marketplace:prerelease
-140
View File
@@ -1,140 +0,0 @@
name: Scenario Tests
on:
push:
branches:
- main
pull_request:
types: [opened, reopened, synchronize, ready_for_review]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
matrix_prep:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- id: set-matrix
run: |
echo 'matrix=[{"runner":"ubuntu"},{"runner":"windows"},{"runner":"macos"}]' >> $GITHUB_OUTPUT
validate-scenario:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- name: Validate PR-specific scenario metadata
run: |
if [ "${{ github.event_name }}" != "pull_request" ]; then
echo "Not a pull_request event; skipping scenario validation."
exit 0
fi
bash scripts/validate-scenario.sh "${{ github.event.number }}"
scenarios:
needs: [matrix_prep, validate-scenario]
strategy:
fail-fast: false
matrix:
include: ${{ fromJson(needs.matrix_prep.outputs.matrix) }}
runs-on: ${{ matrix.runner }}-latest
timeout-minutes: 20
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: 22
# Cache root dependencies - only reuse if package-lock.json exactly matches
- name: Cache root dependencies
uses: actions/cache@v4
id: root-cache
with:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
# Cache webview-ui dependencies - only reuse if package-lock.json exactly matches
- name: Cache webview-ui dependencies
uses: actions/cache@v4
id: webview-cache
with:
path: webview-ui/node_modules
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
# Cache VS Code installation
- name: Cache VS Code
uses: actions/cache@v4
id: vscode-cache
with:
path: .vscode-test
key: vscode-${{ runner.os }}-stable-${{ hashFiles('.vscode-test.mjs', 'package.json') }}
restore-keys: |
vscode-${{ runner.os }}-stable-
# Cache Playwright browsers
- name: Cache Playwright browsers
uses: actions/cache@v4
id: playwright-cache
with:
path: |
~/.cache/ms-playwright
~/Library/Caches/ms-playwright
~/AppData/Local/ms-playwright
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
restore-keys: |
playwright-browsers-${{ runner.os }}-
- name: Install root dependencies
if: steps.root-cache.outputs.cache-hit != 'true'
run: npm ci
- name: Install webview-ui dependencies
if: steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci
- name: Install xvfb on Linux
if: matrix.runner == 'ubuntu'
run: sudo apt-get update && sudo apt-get install -y xvfb
# Run optimized Scenario tests (reuses build steps like e2e)
- name: Run Scenario tests - Linux
if: matrix.runner == 'ubuntu'
run: xvfb-run -a npm run test:scenarios:optimal
- name: Run Scenario tests - Non-Linux
if: matrix.runner != 'ubuntu'
run: npm run test:scenarios:optimal
- uses: actions/upload-artifact@v4
if: ${{ failure() }}
with:
name: playwright-recordings-${{ matrix.runner }}
path: |
test-results/playwright/
scenario-summary:
needs: scenarios
permissions: {}
runs-on: ubuntu-latest
if: always()
steps:
- name: Check all scenarios passed
run: |
if [ "${{ needs.scenarios.result }}" != "success" ]; then
echo "Some scenario tests failed"
exit 1
fi
echo "All scenario tests passed successfully"
+1 -2
View File
@@ -3,8 +3,7 @@
"ts"
],
"spec": [
"src/**/__tests__/*.ts",
"eslint-rules/__tests__/**/*.test.ts"
"src/**/__tests__/*.ts"
],
"require": [
"ts-node/register",
+34
View File
@@ -1,5 +1,39 @@
# Changelog
## [3.26.6]
- Add free Grok Coder model to Cline provider for users looking for a fast, free coding model option
- Fix GPT-5 models not respecting auto-compact setting when enabled, improving context window management
- Fix provider retry attempts not showing proper user feedback during rate limiting scenarios
- Improve markdown and code block styling to automatically adapt when switching VS Code themes
## [3.26.5]
- fix (provider/vercel-ai-gateway): reduce model list load frequency in settings view
- Fix OVSX publish command to resolve deployment failure
## [3.26.4]
- Update nebius ai studio models
- Update sap provider - support reasoning effort for open ai models
- Fix Claude 4 image input in SAP AI Core Provider
## [3.26.3]
- Add compact system prompt option for LM Studio and Ollama models, optimized for smaller context windows (8k or less)
- Add token usage tracking for LM Studio models to better monitor API consumption
- Add "Use compact prompt" checkbox in LM Studio provider settings
- Fix "Unexpected API Response" bug with gpt-5
## [3.26.2]
- Improve OpenRouter model parsing to show reasoning budget sliders for all models that support thinking, not just Claude models
- Fix OpenRouter context window error handling to properly extract error codes from error messages, resolving "Unexpected API Response" errors with GPT-5 on Cline provider
- Fix GPT-5 context window configuration for OpenAI/OpenRouter/Cline providers to use correct 272K limit
- Remove max tokens configuration from Sonic Alpha model
- Add Go language support to deep-planning feature (Thanks @yuvalman!)
- Fix typo in Focus Chain settings page (Thanks @joyceerhl!)
## [3.26.1]
- Add Vercel AI Gateway as a new API provider option (Thanks @joshualipman123!)
+17
View File
@@ -40,3 +40,20 @@ Refer to the [Generative AI Hub Supported Models page](https://me.sap.com/notes/
- **Deployed Models:** These models are already deployed in your specified resource group and are ready to use immediately.
- **Not Deployed Models:** These models don't have active deployments in your specified resource group. You won't be able to use these models until you create deployments for them in SAP AI Core.
- **Creating Deployments:** To use a not deployed model, you'll need to create a deployment in your resource group in sap ai core service instance. See [Create a Deployment for a Generative AI Model](https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/create-deployment-for-generative-ai-model-in-sap-ai-core) for instructions.
#### Configuring Reasoning Effort for OpenAI Models
When using OpenAI reasoning models (such as o1, o3, o3-mini, o4-mini) through SAP AI Core, you can control the reasoning effort to balance performance and cost:
1. **Open Cline Settings:** Click the settings icon (⚙️) in the Cline panel.
2. **Navigate to Features:** Go to the "Features" section in the settings.
3. **Find OpenAI Reasoning Effort:** Locate the "OpenAI Reasoning Effort" setting.
4. **Choose Effort Level:** Select between:
- **Low:** Faster responses with lower token usage, suitable for simpler tasks
- **Medium:** Balanced performance and token usage for most tasks
- **High:** More thorough analysis with higher token usage, better for complex reasoning tasks
> 💡 **Note**
>
> This setting only applies when using OpenAI reasoning models (o1, o3, o3-mini, o4-mini, gpt-5, etc.) deployed through SAP AI Core. Other models will ignore this setting.
+84 -20
View File
@@ -1,34 +1,98 @@
---
title: "Vercel AI Gateway"
description: "Learn how to use Vercel AI Gateway with Cline to access a wide variety of AI models through a single unified API."
description: "Use Vercel AI Gateway in Cline to reach 100+ models from one endpoint with routing, retries, and spend observability."
---
The AI Gateway offers a unified API to multiple model providers and gives you the ability to set budgets, monitor usage, load-balance requests, and manage fallbacks.
Vercel AI Gateway gives you a single API to access models from many providers. You switch by model id without swapping SDKs or juggling multiple keys. Cline integrates directly so you can pick a Gateway model in the dropdown, use it like any other provider, and see token and cache usage in the stream.
Useful Links:
- [AI Gateway Team Dashboard](https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai&title=AI+Gateway+Overview+Tab)
- [AI Gateway Landing Page](https://vercel.com/ai-gateway)
Useful links:
- Team dashboard: https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai
- Models catalog: https://vercel.com/ai-gateway/models
- Docs: https://vercel.com/docs/ai-gateway
### Getting an API Key
## What you get
1. **Sign Up/Sign In:** Go to [vercel.com](https://vercel.com/) and create an account or sign in.
2. **Get an API Key:** From the Vercel dashboard, click the AI Gateway tab. Then click **API keys** on the left side bar and hit **Create key**.
3. **Copy the Key:** Copy the API key.
- One endpoint for 100+ models with a single key
- Automatic retries and fallbacks that you configure on the dashboard
- Spend monitoring with requests by model, token counts, cache usage, latency percentiles, and cost
- OpenAI-compatible surface so existing clients work
For more on Vercel AI Gateway authentication, see [Authentication](https://vercel.com/docs/ai-gateway/authentication)
## Getting an API Key
### Supported Models
1. Sign in at https://vercel.com
2. Dashboard → AI Gateway → API Keys → Create key
3. Copy the key
Vercel AI Gateway supports a large and growing number of models from various providers. Cline automatically fetches the list of available models from the Gateway API. Refer to the [Vercel AI Gateway Models page](https://vercel.com/ai-gateway/models) for the complete list.
For more on authentication and OIDC options, see https://vercel.com/docs/ai-gateway/authentication
### Configuration in Cline
## Configuration in Cline
1. **Open Cline Settings:** Click the settings icon (⚙️) in the Cline panel.
2. **Select Provider:** Choose "Vercel AI Gateway" from the "API Provider" dropdown.
3. **Enter API Key:** Paste your Vercel AI Gateway API key into the "Vercel AI Gateway API Key" field.
4. **Select Model:** Choose your desired model from the "Model" dropdown, or enter a custom model ID if the model list doesn't load automatically.
1. Open Cline settings
2. Select **Vercel AI Gateway** as the API Provider
3. Paste your Gateway API Key
4. Pick a model from the list. Cline fetches the catalog automatically. You can also paste an exact id
### Tips and Notes
Notes:
- Model ids often follow `provider/model`. Copy the exact id from the catalog
Examples:
- `openai/gpt-5`
- `anthropic/claude-sonnet-4`
- `google/gemini-2.5-pro`
- `groq/llama-3.1-70b`
- `deepseek/deepseek-v3`
- **Model Selection:** The AI Gateway offers a wide range of models. Experiment to find the best one for your needs.
- **Pricing:** Vercel charges based on the underlying model's pricing. See the [AI Gateway Pricing page](https://vercel.com/docs/ai-gateway/pricing) for details.
## Observability you can act on
<Frame>
<img src="https://assets.vercel.com/image/upload/v1753121283/gateway-overhead-dark_zhqwwj.svg" alt="Vercel AI Gateway observability with requests by model, tokens, cache, latency, and cost." />
</Frame>
What to watch:
- Requests by model - confirm routing and adoption
- Tokens - input vs output, including reasoning if exposed
- Cache - cached input and cache creation tokens
- Latency - p75 duration and p75 time to first token
- Cost - per project and per model
Use it to:
- Compare output tokens per request before and after a model change
- Validate cache strategy by tracking cache reads and write creation
- Catch TTFT regressions during experiments
- Align budgets with real usage
## Supported models
The gateway supports a large and changing set of models. Cline pulls the list from the Gateway API and caches it locally. For the current catalog, see https://vercel.com/ai-gateway/models
## Tips
<Tip>
Use separate gateway keys per environment (dev, staging, prod). It keeps dashboards clean and budgets isolated.
</Tip>
<Note>
Pricing is pass-through at provider list price. Bring-your-own key has 0% markup. You still pay provider and processing fees.
</Note>
<Info>
Vercel does not add rate limits. Upstream providers may. New accounts receive $5 credits every 30 days until the first payment.
</Info>
## Troubleshooting
- 401 - send the Gateway key to the Gateway endpoint, not an upstream URL
- 404 model - copy the exact id from the Vercel catalog
- Slow first token - check p75 TTFT in the dashboard and try a model optimized for streaming
- Cost spikes - break down by model in the dashboard and cap or route traffic
## Inspiration
- Multi-model evals - swap only the model id in Cline and compare latency and output tokens
- Progressive rollout - route a small percent to a new model in the dashboard and ramp with metrics
- Budget enforcement - set per-project limits without code changes
## Crosslinks
- OpenAI-Compatible setup: /provider-config/openai-compatible
- Model Selection Guide: /getting-started/model-selection-guide
- Understanding Context Management: /getting-started/understanding-context-management
-1
View File
@@ -1 +0,0 @@
See [https://cline.bot/privacy](https://cline.bot/privacy) for our privacy policy.
-39
View File
@@ -1,39 +0,0 @@
# Cline Documentation
Welcome to the Cline documentation - your comprehensive guide to using and extending Cline's capabilities. Here you'll find resources to help you get started, improve your skills, and contribute to the project.
## Getting Started
- **New to coding?** We've prepared a gentle introduction:
- [Getting Started for New Coders](getting-started-new-coders/README.md)
## Improving Your Prompting Skills
- **Want to communicate more effectively with Cline?** Explore:
- [Prompt Engineering Guide](prompting/README.md)
- [Cline Memory Bank](prompting/custom%20instructions%20library/cline-memory-bank.md)
## Exploring Cline's Tools
- **Understand Cline's capabilities:**
- [Cline Tools Guide](tools/cline-tools-guide.md)
- [Mentions Feature Guide](tools/mentions-guide.md)
- **Extend Cline with MCP Servers:**
- [MCP Overview](mcp/README.md)
- [Building MCP Servers from GitHub](mcp/mcp-server-from-github.md)
- [Building Custom MCP Servers](mcp/mcp-server-from-scratch.md)
## Contributing to Cline
- **Interested in contributing?** We welcome your input:
- Feel free to submit a pull request
- [Contribution Guidelines](../CONTRIBUTING.md)
## Additional Resources
- **Cline GitHub Repository:** [https://github.com/cline/cline](https://github.com/cline/cline)
- **MCP Documentation:** [https://modelcontextprotocol.org/docs](https://modelcontextprotocol.org/docs)
We're always looking to improve this documentation. If you have suggestions or find areas that could be enhanced, please let us know. Your feedback helps make Cline better for everyone.
-43
View File
@@ -1,43 +0,0 @@
# Cline Extension Architecture
This directory contains architectural documentation for the Cline VSCode extension.
## Extension Architecture Diagram
The [extension-architecture.mmd](./extension-architecture.mmd) file contains a Mermaid diagram showing the high-level architecture of the Cline extension. The diagram illustrates:
1. **Core Extension**
- Extension entry point and main classes
- State management through VSCode's global state and secrets storage
- Core business logic in the Cline class
2. **Webview UI**
- React-based user interface
- State management through ExtensionStateContext
- Component hierarchy
3. **Storage**
- Task-specific storage for history and state
- Git-based checkpoint system for file changes
4. **Data Flow**
- Core extension data flow between components
- Webview UI data flow
- Bidirectional communication between core and webview
## Viewing the Diagram
To view the diagram:
1. Install a Mermaid diagram viewer extension in VSCode
2. Open extension-architecture.mmd
3. Use the extension's preview feature to render the diagram
You can also view the diagram on GitHub, which has built-in Mermaid rendering support.
## Color Scheme
The diagram uses a high-contrast color scheme for better visibility:
- Pink (#ff0066): Global state and secrets storage components
- Blue (#0066ff): Extension state context
- Green (#00cc66): Cline provider
- All components use white text for maximum readability
@@ -1,67 +0,0 @@
flowchart TB
subgraph "VSCode Extension Host"
subgraph "Core Extension"
ExtensionEntry["Extension Entry<br/>src/extension.ts"]
WebviewProvider["WebviewProvider<br/>src/core/webview/index.ts"]
Controller["Controller<br/>src/core/controller/index.ts"]
Task["Task<br/>src/core/task/index.ts"]
GlobalState["VSCode Global State"]
SecretsStorage["VSCode Secrets Storage"]
McpHub["McpHub<br/>src/services/mcp/McpHub.ts"]
end
subgraph "Webview UI"
WebviewApp["React App<br/>webview-ui/src/App.tsx"]
ExtStateContext["ExtensionStateContext<br/>webview-ui/src/context/ExtensionStateContext.tsx"]
ReactComponents["React Components"]
end
subgraph "Storage"
TaskStorage["Task Storage<br/>Per-Task Files & History"]
CheckpointSystem["Git-based Checkpoints"]
end
subgraph "API Providers"
AnthropicAPI["Anthropic"]
OpenRouterAPI["OpenRouter"]
BedrockAPI["AWS Bedrock"]
OtherAPIs["Other Providers"]
end
subgraph "MCP Servers"
ExternalMcpServers["External MCP Servers"]
end
end
%% Core Extension Data Flow
ExtensionEntry --> WebviewProvider
WebviewProvider --> Controller
Controller --> Task
Controller --> McpHub
Task --> GlobalState
Task --> SecretsStorage
Task --> TaskStorage
Task --> CheckpointSystem
Task --> |"API Requests"| AnthropicAPI
Task --> |"API Requests"| OpenRouterAPI
Task --> |"API Requests"| BedrockAPI
Task --> |"API Requests"| OtherAPIs
McpHub --> |"Connects to"| ExternalMcpServers
Task --> |"Uses"| McpHub
%% Webview Data Flow
WebviewApp --> ExtStateContext
ExtStateContext --> ReactComponents
%% Bidirectional Communication
WebviewProvider <--> |"postMessage"| ExtStateContext
classDef vscodeState fill:#f9f,stroke:#333,stroke-width:2px
classDef contextClass fill:#bbf,stroke:#333,stroke-width:2px
classDef providerClass fill:#bfb,stroke:#333,stroke-width:2px
classDef apiClass fill:#fdb,stroke:#333,stroke-width:2px
class GlobalState,SecretsStorage vscodeState
class ExtStateContext contextClass
class WebviewProvider,McpHub providerClass
class AnthropicAPI,OpenRouterAPI,BedrockAPI,OtherAPIs apiClass
@@ -1,54 +0,0 @@
### .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.
@@ -1,92 +0,0 @@
# Getting Started with Cline | New Coders
Welcome to Cline! This guide will help you get set up and start using Cline to build your first project.
## What You'll Need
Before you begin, make sure you have the following:
- **VS Code:** A free, powerful code editor.
- [Download VS Code](https://code.visualstudio.com/)
- **Development Tools:** Essential software for coding (Homebrew, Node.js, Git, etc.).
- Follow our [Installing Essential Development Tools](installing-dev-essentials.md) guide to set these up with Cline's help (after getting setup here)
- Cline will guide you through installing everything you need
- **Cline Projects Folder:** A dedicated folder for all your Cline projects.
- On macOS: Create a folder named "Cline" in your Documents folder
- Path: `/Users/[your-username]/Documents/Cline`
- On Windows: Create a folder named "Cline" in your Documents folder
- Path: `C:\Users\[your-username]\Documents\Cline`
- Inside this Cline folder, create separate folders for each project
- Example: `Documents/Cline/workout-app` for a workout tracking app
- Example: `Documents/Cline/portfolio-website` for your portfolio
- **Cline Extension in VS Code:** The Cline extension installed in VS Code.
- Here's a [tutorial](https://www.youtube.com/watch?v=N4td-fKhsOQ) on everything you need to get started.
## Step-by-Step Setup
Follow these steps to get Cline up and running:
1. **Open VS Code:** Launch the VS Code application. If VS Code shows "Running extensions might...", click "Allow".
2. **Open Your Cline Folder:** In VS Code, open the Cline folder you created in Documents.
3. **Navigate to Extensions:** Click on the Extensions icon in the Activity Bar on the side of VS Code.
4. **Search for 'Cline':** In the Extensions search bar, type "Cline".
5. **Install the Extension:** Click the "Install" button next to the Cline extension.
6. **Open Cline:** Once installed, you can open Cline in a few ways:
- Click the Cline icon in the Activity Bar.
- Use the command palette (`CMD/CTRL + Shift + P`) and type "Cline: Open In New Tab" to open Cline as a tab in your editor. This is recommended for a better view.
- **Troubleshooting:** If you don't see the Cline icon, try restarting VS Code.
- **What You'll See:** You should see the Cline chat window appear in your VS Code editor.
![gettingStartedVsCodeCline](https://github.com/user-attachments/assets/622b4bb7-859b-4c2e-b87b-c12e3eabefb8)
## Setting up OpenRouter API Key
Now that you have Cline installed, you'll need to set up your OpenRouter API key to use Cline's full capabilities.
1. **Get your OpenRouter API Key:**
- [Get your OpenRouter API Key](https://openrouter.ai/)
2. **Input Your OpenRouter API Key:**
- Navigate to the settings button in the Cline extension.
- Input your OpenRouter API key.
- Select your preferred API model.
- **Recommended Models for Coding:**
- `anthropic/claude-3.5-sonnet`: Most used for coding tasks.
- `google/gemini-2.0-flash-exp:free`: A free option for coding.
- `deepseek/deepseek-chat`: SUPER CHEAP, almost as good as 3.5 sonnet
- [OpenRouter Model Rankings](https://openrouter.ai/rankings/programming)
## Your First Interaction with Cline
Now you're ready to start building with Cline. Let's create your first project folder and build something! Copy and paste the following prompt into the Cline chat window:
```
Hey Cline! Could you help me create a new project folder called "hello-world" in my Cline directory and make a simple webpage that says "Hello World" in big blue text?
```
**What You'll See:** Cline will help you create the project folder and set up your first webpage.
## Tips for Working with Cline
- **Ask Questions:** If you're unsure about something, don't hesitate to ask Cline!
- **Use Screenshots:** Cline can understand images, so feel free to use screenshots to show him what you're working on.
- **Copy and Paste Errors:** If you encounter errors, copy and paste the error messages into Cline's chat. This will help him understand the issue and provide a solution.
- **Speak Plainly:** Cline is designed to understand plain, non-technical language. Feel free to describe your ideas in your own words, and Cline will translate them into code.
## FAQs
- **What is the Terminal?** The terminal is a text-based interface for interacting with your computer. It allows you to run commands to perform various tasks, such as installing packages, running scripts, and managing files. Cline uses the terminal to execute commands and interact with your development environment.
- **How Does the Codebase Work?** (This section will be expanded based on common questions from new coders)
## Still Struggling?
Feel free to contact me, and I'll help you get started with Cline.
nick | 608-558-2410
Join our Discord community: [https://discord.gg/cline](https://discord.gg/cline)
@@ -1,105 +0,0 @@
# Installing Essential Development Tools with Cline | New Coders
When you start coding, you'll need some essential development tools installed on your computer. Cline can help you install everything you need in a safe, guided way.
## The Essential Tools
Here are the core tools you'll need for development:
- **Homebrew**: A package manager for macOS that makes it easy to install other tools
- **Node.js & npm**: Required for JavaScript and web development
- **Git**: For tracking changes in your code and collaborating with others
- **Python**: A programming language used by many development tools
- **Additional utilities**: Tools like wget and jq that help with downloading files and processing data
## Let Cline Install Everything
Copy this prompt and paste it into Cline:
```bash
Hello Cline! I need help setting up my Mac for software development. Could you please help me install the essential development tools like Homebrew, Node.js, Git, Python, and any other utilities that are commonly needed for coding? I'd like you to guide me through the process step-by-step, explaining what each tool does and making sure everything is installed correctly.
```
## What Will Happen
1. Cline will first install Homebrew, which is like an "app store" for development tools
2. Using Homebrew, Cline will then install other essential tools like Node.js and Git
3. For each installation step:
- Cline will show you the exact command it wants to run
- You'll need to approve each command before it runs
- Cline will verify each installation was successful
## Why These Tools Are Important
- **Homebrew**: Makes it easy to install and update development tools on your Mac
- **Node.js & npm**: Required for:
- Building websites with React or Next.js
- Running JavaScript code
- Installing JavaScript packages
- **Git**: Helps you:
- Save different versions of your code
- Collaborate with other developers
- Back up your work
- **Python**: Used for:
- Running development scripts
- Data processing
- Machine learning projects
## Notes
- The installation process is interactive - Cline will guide you through each step
- You may need to enter your computer's password for some installations. When prompted, you will not see any characters being typed on the screen. This is normal and is a security feature to protect your password. Just type your password and press Enter.
**Example:**
```bash
$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
Password:
```
_Type your password here, even though nothing will show up on the screen. Press Enter when you're done._
- All commands will be shown to you for approval before they run
- If you run into any issues, Cline will help troubleshoot them
## Additional Tips for New Coders
### Understanding the Terminal
The **Terminal** is an application where you can type commands to interact with your computer. On macOS, you can open it by searching for "Terminal" in Spotlight.
**Example:**
```bash
$ open -a Terminal
```
### Understanding VS Code Features
#### Terminal in VS Code
The **Terminal** in VS Code allows you to run commands directly from within the editor. You can open it by going to `View > Terminal` or by pressing `` Ctrl + ` ``.
**Example:**
```bash
$ node -v
v16.14.0
```
#### Document View
The **Document View** is where you edit your code files. You can open files by clicking on them in the **Explorer** panel on the left side of the screen.
#### Problems Section
The **Problems** section in VS Code shows any errors or warnings in your code. You can access it by clicking on the lightbulb icon or by going to `View > Problems`.
### Common Features
- **Command Line Interface (CLI)**: This is a text-based interface where you type commands to interact with your computer. It might seem intimidating at first, but it's a powerful tool for developers.
- **Permissions**: Sometimes, you will need to give permissions to certain applications or commands. This is a security measure to ensure that only trusted applications can make changes to your system.
## 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/README.md) guide to continue your journey.
-103
View File
@@ -1,103 +0,0 @@
# Cline and Model Context Protocol (MCP) Servers: Enhancing AI Capabilities
**Quick Links:**
- [Building MCP Servers from GitHub](mcp-server-from-github.md)
- [Building Custom MCP Servers from Scratch](mcp-server-from-scratch.md)
This document explains Model Context Protocol (MCP) servers, their capabilities, and how Cline can help build and use them.
## Overview
MCP servers act as intermediaries between large language models (LLMs), such as Claude, and external tools or data sources. They are small programs that expose functionalities to LLMs, enabling them to interact with the outside world through the MCP. An MCP server is essentially like an API that an LLM can use.
## Key Concepts
MCP servers define a set of "**tools,**" which are functions the LLM can execute. These tools offer a wide range of capabilities.
**Here's how MCP works:**
- **MCP hosts** discover the capabilities of connected servers and load their tools, prompts, and resources.
- **Resources** provide consistent access to read-only data, akin to file paths or database queries.
- **Security** is ensured as servers isolate credentials and sensitive data. Interactions require explicit user approval.
## Use Cases
The potential of MCP servers is vast. They can be used for a variety of purposes.
**Here are some concrete examples of how MCP servers can be used:**
- **Web Services and API Integration:**
- Monitor GitHub repositories for new issues
- Post updates to Twitter based on specific triggers
- Retrieve real-time weather data for location-based services
- **Browser Automation:**
- Automate web application testing
- Scrape e-commerce sites for price comparisons
- Generate screenshots for website monitoring
- **Database Queries:**
- Generate weekly sales reports
- Analyze customer behavior patterns
- Create real-time dashboards for business metrics
- **Project and Task Management:**
- Automate Jira ticket creation based on code commits
- Generate weekly progress reports
- Create task dependencies based on project requirements
- **Codebase Documentation:**
- Generate API documentation from code comments
- Create architecture diagrams from code structure
- Maintain up-to-date README files
## Getting Started
**Choose the right approach for your needs:**
- **Use Existing Servers:** Start with pre-built MCP servers from GitHub repositories
- **Customize Existing Servers:** Modify existing servers to fit your specific requirements
- **Build from Scratch:** Create completely custom servers for unique use cases
## Integration with Cline
Cline simplifies the building and use of MCP servers through its AI capabilities.
### Building MCP Servers
- **Natural language understanding:** Instruct Cline in natural language to build an MCP server by describing its functionalities. Cline will interpret your instructions and generate the necessary code.
- **Cloning and building servers:** Cline can clone existing MCP server repositories from GitHub and build them automatically.
- **Configuration and dependency management:** Cline handles configuration files, environment variables, and dependencies.
- **Troubleshooting and debugging:** Cline helps identify and resolve errors during development.
### Using MCP Servers
- **Tool execution:** Cline seamlessly integrates with MCP servers, allowing you to execute their defined tools.
- **Context-aware interactions:** Cline can intelligently suggest using relevant tools based on conversation context.
- **Dynamic integrations:** Combine multiple MCP server capabilities for complex tasks. For example, Cline could use a GitHub server to get data and a Notion server to create a formatted report.
## Security Considerations
When working with MCP servers, it's important to follow security best practices:
- **Authentication:** Always use secure authentication methods for API access
- **Environment Variables:** Store sensitive information in environment variables
- **Access Control:** Limit server access to authorized users only
- **Data Validation:** Validate all inputs to prevent injection attacks
- **Logging:** Implement secure logging practices without exposing sensitive data
## Resources
There are various resources available for finding and learning about MCP servers.
**Here are some links to resources for finding and learning about MCP servers:**
- **GitHub Repositories:** [https://github.com/modelcontextprotocol/servers](https://github.com/modelcontextprotocol/servers) and [https://github.com/punkpeye/awesome-mcp-servers](https://github.com/punkpeye/awesome-mcp-servers)
- **Online Directories:** [https://mcpservers.org/](https://mcpservers.org/), [https://mcp.so/](https://mcp.so/), and [https://glama.ai/mcp/servers](https://glama.ai/mcp/servers)
- **PulseMCP:** [https://www.pulsemcp.com/](https://www.pulsemcp.com/)
- **YouTube Tutorial (AI-Driven Coder):** A video guide for building and using MCP servers: [https://www.youtube.com/watch?v=b5pqTNiuuJg](https://www.youtube.com/watch?v=b5pqTNiuuJg)
-151
View File
@@ -1,151 +0,0 @@
# 🚀 MCP Quickstart Guide
## ❓ What's an MCP Server?
Think of MCP servers as special helpers that give Cline extra powers! They let Cline do cool things like fetch web pages or work with your files.
## ⚠️ IMPORTANT: System Requirements
STOP! Before proceeding, you MUST verify these requirements:
### Required Software
- ✅ Latest Node.js (v18 or newer)
- Check by running: `node --version`
- Install from: <https://nodejs.org/>
- ✅ Latest Python (v3.8 or newer)
- Check by running: `python --version`
- Install from: <https://python.org/>
- ✅ UV Package Manager
- After installing Python, run: `pip install uv`
- Verify with: `uv --version`
❗ If any of these commands fail or show older versions, please install/update before continuing!
⚠️ If you run into other errors, see the "Troubleshooting" section below.
## 🎯 Quick Steps (Only After Requirements Are Met!)
### 1. 🛠️ Install Your First MCP Server
1. From the Cline extension, click the `MCP Server` tab
1. Click the `Edit MCP Settings` button
<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. Replace the file's contents with this code:
For Windows:
```json
{
"mcpServers": {
"mcp-installer": {
"command": "cmd.exe",
"args": ["/c", "npx", "-y", "@anaisbetts/mcp-installer"]
}
}
}
```
For Mac and Linux:
```json
{
"mcpServers": {
"mcp-installer": {
"command": "npx",
"args": ["@anaisbetts/mcp-installer"]
}
}
}
```
After saving the file:
1. Cline will detect the change automatically
2. The MCP installer will be downloaded and installed
3. Cline will start the MCP installer
4. You'll see the server status in Cline's MCP settings UI:
<img src="https://github.com/user-attachments/assets/2abbb3de-e902-4ec2-a5e5-9418ed34684e" alt="MCP Server Panel with Installer" width="400" />
## 🤔 What Next?
Now that you have the MCP installer, you can ask Cline to add more servers from:
1. NPM Registry: <https://www.npmjs.com/search?q=%40modelcontextprotocol>
2. Python Package Index: <https://pypi.org/search/?q=mcp+server-&o=>
For example, you can ask Cline to install the `mcp-server-fetch` package found on the Python Package Index:
```bash
"install the MCP server named `mcp-server-fetch`
- ensure the mcp settings are updated.
- use uvx or python to run the server."
```
You should witness Cline:
1. Install the `mcp-server-fetch` python package
1. Update the mcp setting json file
1. Start the server and start the server
The mcp settings file should now look like this:
_For a Windows machine:_
```json
{
"mcpServers": {
"mcp-installer": {
"command": "cmd.exe",
"args": ["/c", "npx", "-y", "@anaisbetts/mcp-installer"]
},
"mcp-server-fetch": {
"command": "uvx",
"args": ["mcp-server-fetch"]
}
}
}
```
You you can always check the status of your server by going to clients MCP server tab. See the image above
That's it! 🎉 You've just given Cline some awesome new abilities!
## 📝 Troubleshooting
### 1. I'm Using `asdf` and Get "unknown command: npx"
There is some slightly bad news. You should still be able to get things to work, but will have to do a bit more manual work unless MCP server packaging evolves a bit. One option is to uninstall `asdf` , but we will assume you do not want to do that.
Instead, you will need to follow the instructions above to "Edit MCP Settings". Then, as [this post](https://dev.to/cojiroooo/mcp-using-node-on-asdf-382n) describes, you need to add and "env" entry to each server's configs.
```json
"env": {
"PATH": "/Users/<user_name>/.asdf/shims:/usr/bin:/bin",
"ASDF_DIR": "<path_to_asdf_bin_dir>",
"ASDF_DATA_DIR": "/Users/<user_name>/.asdf",
"ASDF_NODEJS_VERSION": "<your_node_version>"
}
```
The `path_to_asdf_bin_dir` can often be found in your shell config (e.g. `.zshrc`). If you are using Homebrew, you can use `echo ${HOMEBREW_PREFIX}` to find the start of the directory and then append `/opt/asdf/libexec`.
Now for some good news. While not perfect, you can get Cline to do this for you fairly reliably for subsequent server install. Add the following to your "Custom Instructions" in the Cline settings (top-right toolbar button):
> When installing MCP servers and editing the cline_mcp_settings.json, if the server requires use of `npx` as the command, you must copy the "env" entry from the "mcp-installer" entry and add it to the new entry. This is vital to getting the server to work properly when in use.
### 2. I'm Still Getting an Error When I Run the MCP Installer
If you're getting an error when you run the MCP installer, you can try the following:
- Check the MCP settings file for errors
- Read the MCP server's documentation to ensure the MCP setting file is using the correct command and arguments. 👈
- Use a terminal and run the command with its arguments directly. This will allow you to see the same errors that Cline is seeing.
-67
View File
@@ -1,67 +0,0 @@
# Building MCP Servers from GitHub Repositories
This guide provides a step-by-step walkthrough of how to use Cline to build an existing MCP server from a GitHub repository.
## **Finding an MCP Server**
There are multiple places online to find MCP servers:
- **Cline can automatically add MCP servers to its list, which you can then edit.** Cline can clone repositories directly from GitHub and build the servers for you.
- **GitHub:** Two of the most common places to find MCP servers on GitHub include:
- [Official MCP servers repository](https://github.com/modelcontextprotocol/servers)
- [Awesome-MCP servers repository](https://github.com/punkpeye/awesome-mcp-servers)
- **Online directories:** Several websites list MCP servers including:
- [mcpservers.org](https://mcpservers.org/)
- [mcp.so](https://mcp.so/)
- [glama.ai/mcp/servers](https://glama.ai/mcp/servers)
These directories allow users to sort the servers by various criteria such as downloads, date, stars, and use case. Each entry provides information such as features, tools, and configuration instructions.
- **PulseMCP:** This website has a blog post discussing how AI could use MCP servers to make websites obsolete. PulseMCP also includes an FAQ section about MCP servers: [https://www.pulsemcp.com/](https://www.pulsemcp.com/)
## **Building with Cline**
1. **Initiate the Process:** Provide Cline with the following information:
- **GitHub Repository URL:** The URL of the server's repository.
- **README.md Contents:** This is optional but helpful for Cline to understand the server's purpose and configuration. You can copy the README.md file from the GitHub repository.
2. **Example Interaction with Cline:**
```
User: "Cline, I want to add the MCP server for Brave browser control. Here's the GitHub link: https://github.com/modelcontextprotocol/servers/tree/main/src/brave-search Can you add it?"
Cline: "OK. Cloning the repository to the MCP directory. It needs to be built because it has a 'package.json' file. Should I run 'npm run build'?"
User: "Yes, build it."
Cline: "Build complete. This server needs a browser path. Where's your Brave browser executable?"
User: "It's in '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser'."
Cline: "OK. I've updated the server config with that path and added it to 'cline_mcp_settings.json'. Anything else?"
User: "No, that's all. Let's test it."
Cline: "Great! Starting the MCP Inspector to test the server connection. After that, we can try controlling the browser from Cline."
```
3. **Cline's Actions:** Based on your instructions, Cline will perform the following:
- **Repository Cloning:** Cline will clone the repository to your local machine, usually in the directory specified in your configuration.
- **Tweaking:** You can guide Cline to modify the servers configuration. For instance:
- **User:** "This server requires an API key. Can you find where it should be added?"
- Cline may automatically update the `cline_mcp_settings.json` file or other relevant files based on your instructions.
- **Building the Server:** Cline will run the appropriate build command for the server, which is commonly `npm run build`.
- **Adding Server to Settings:** Cline will add the servers configuration to the `cline_mcp_settings.json` file.
## **Testing and Troubleshooting**
1. **Test the Server:** Once Cline finishes the build process, test the server to make sure it works as expected. Cline can assist you if you encounter any problems.
2. **MCP Inspector:** You can use the MCP Inspector to test the servers connection and functionality.
## **Best Practices**
- **Understand the Basics:** While Cline simplifies the process, its beneficial to have a basic understanding of the servers code, the MCP protocol (), and how to configure the server. This allows for more effective troubleshooting and customization.
- **Clear Instructions:** Provide clear and specific instructions to Cline throughout the process.
- **Testing:** Thoroughly test the server after installation and configuration to ensure it functions correctly.
- **Version Control:** Use a version control system (like Git) to track changes to the servers code.
- **Stay Updated:** Keep your MCP servers updated to benefit from the latest features and security patches.
-74
View File
@@ -1,74 +0,0 @@
# Building Custom MCP Servers From Scratch Using Cline: A Comprehensive Guide
This guide provides a comprehensive walkthrough of building a custom MCP (Model Context Protocol) server from scratch, leveraging the powerful AI capabilities of Cline. The example used will be building a "GitHub Assistant Server" to illustrate the process.
## Understanding MCP and Cline's Role in Building Servers
### What is MCP?
The Model Context Protocol (MCP) acts as a bridge between large language models (LLMs) like Claude and external tools and data. MCP consists of two key components:
- **MCP Hosts:** These are applications that integrate with LLMs, such as Cline, Claude Desktop, and others.
- **MCP Servers:** These are small programs specifically designed to expose data or specific functionalities to the LLMs through the MCP.
This setup is beneficial when you have an MCP-compliant chat interface, like Claude Desktop, which can then leverage these servers to access information and execute actions.
### Why Use Cline to Create MCP Servers?
Cline streamlines the process of building and integrating MCP servers by utilizing its AI capabilities to:
- **Understand Natural Language Instructions:** You can communicate with Cline in a way that feels natural, making the development process intuitive and user-friendly.
- **Clone Repositories:** Cline can directly clone existing MCP server repositories from GitHub, simplifying the process of using pre-built servers.
- **Build Servers:** Once the necessary code is in place, Cline can execute commands like `npm run build` to compile and prepare the server for use.
- **Handle Configuration:** Cline manages the configuration files required for the MCP server, including adding the new server to the `cline_mcp_settings.json` file.
- **Assist with Troubleshooting:** If errors arise during development or testing, Cline can help identify the cause and suggest solutions, making debugging easier.
## Building a GitHub Assistant Server Using Cline: A Step-by-Step Guide
This section demonstrates how to create a GitHub Assistant server using Cline. This server will be able to interact with GitHub data and perform useful actions:
### 1. Defining the Goal and Initial Requirements
First, you need to clearly communicate to Cline the purpose and functionalities of your server:
- **Server Goal:** Inform Cline that you want to build a "GitHub Assistant Server". Specify that this server will interact with GitHub data and potentially mention the types of data you are interested in, like issues, pull requests, and user profiles.
- **Access Requirements:** Let Cline know that you need to access the GitHub API. Explain that this will likely require a personal access token (GITHUB_TOKEN) for authentication.
- **Data Specificity (Optional):** You can optionally tell Cline about specific fields of data you want to extract from GitHub, but this can also be determined later as you define the server's tools.
### 2. Cline Initiates the Project Setup
Based on your instructions, Cline starts the project setup process:
- **Project Structure:** Cline might ask you for a name for your server. Afterward, it uses the MCP `create-server` tool to generate the basic project structure for your GitHub Assistant server. This usually involves creating a new directory with essential files like `package.json`, `tsconfig.json`, and a `src` folder for your TypeScript code. \
- **Code Generation:** Cline generates starter code for your server, including:
- **File Handling Utilities:** Functions to help with reading and writing files, commonly used for storing data or logs. \
- **GitHub API Client:** Code to interact with the GitHub API, often using libraries like `@octokit/graphql`. Cline will likely ask for your GitHub username or the repositories you want to work with. \
- **Core Server Logic:** The basic framework for handling requests from Cline and routing them to the appropriate functions, as defined by the MCP. \
- **Dependency Management:** Cline analyzes the code and identifies necessary dependencies, adding them to the `package.json` file. For example, interacting with the GitHub API will likely require packages like `@octokit/graphql`, `graphql`, `axios`, or similar. \
- **Dependency Installation:** Cline executes `npm install` to download and install the dependencies listed in `package.json`, ensuring your server has all the required libraries to function correctly. \
- **Path Corrections:** During development, you might move files or directories around. Cline intelligently recognizes these changes and automatically updates file paths in your code to maintain consistency.
- **Configuration:** Cline will modify the `cline_mcp_settings.json` file to add your new GitHub Assistant server. This will include:
- **Server Start Command:** Cline will add the appropriate command to start your server (e.g., `npm run start` or a similar command).
- **Environment Variables:** Cline will add the required `GITHUB_TOKEN` variable. Cline might ask you for your GitHub personal access token, or it might guide you to safely store it in a separate environment file. \
- **Progress Documentation:** Throughout the process, Cline keeps the "Memory Bank" files updated. These files document the project's progress, highlighting completed tasks, tasks in progress, and pending tasks.
### 3. Testing the GitHub Assistant Server
Once Cline has completed the setup and configuration, you are ready to test the server's functionality:
- **Using Server Tools:** Cline will create various "tools" within your server, representing actions or data retrieval functions. To test, you would instruct Cline to use a specific tool. Here are examples related to GitHub:
- **`get_issues`:** To test retrieving issues, you might say to Cline, "Cline, use the `get_issues` tool from the GitHub Assistant Server to show me the open issues from the 'cline/cline' repository." Cline would then execute this tool and present you with the results.
- **`get_pull_requests`:** To test pull request retrieval, you could ask Cline to "use the `get_pull_requests` tool to show me the merged pull requests from the 'facebook/react' repository from the last month." Cline would execute this tool, using your GITHUB_TOKEN to access the GitHub API, and display the requested data. \
- **Providing Necessary Information:** Cline might prompt you for additional information required to execute the tool, such as the repository name, specific date ranges, or other filtering criteria.
- **Cline Executes the Tool:** Cline handles the communication with the GitHub API, retrieves the requested data, and presents it in a clear and understandable format.
### 4. Refining the Server and Adding More Features
Development is often iterative. As you work with your GitHub Assistant Server, you'll discover new functionalities to add, or ways to improve existing ones. Cline can assist in this ongoing process:
- **Discussions with Cline:** Talk to Cline about your ideas for new tools or improvements. For example, you might want a tool to `create_issue` or to `get_user_profile`. Discuss the required inputs and outputs for these tools with Cline.
- **Code Refinement:** Cline can help you write the necessary code for new features. Cline can generate code snippets, suggest best practices, and help you debug any issues that arise.
- **Testing New Functionalities:** After adding new tools or functionalities, you would test them again using Cline, ensuring they work as expected and integrate well with the rest of the server.
- **Integration with Other Tools:** You might want to integrate your GitHub Assistant server with other tools. For instance, in the "github-cline-mcp" source, Cline assists in integrating the server with Notion to create a dynamic dashboard that tracks GitHub activity. \
By following these steps, you can create a custom MCP server from scratch using Cline, leveraging its powerful AI capabilities to streamline the entire process. Cline not only assists with the technical aspects of building the server but also helps you think through the design, functionalities, and potential integrations.
-325
View File
@@ -1,325 +0,0 @@
# Cline Prompting Guide 🚀
Welcome to the Cline Prompting Guide! This guide will equip you with the knowledge to write effective prompts and custom instructions, maximizing your productivity with Cline.
## Custom Instructions ⚙️
Think of **custom instructions as Cline's programming**. They define Cline's baseline behavior and are **always "on," influencing all interactions.**
To add custom instructions:
1. Open VSCode
2. Click the Cline extension settings dial ⚙️
3. Find the "Custom Instructions" field
4. Paste your instructions
<img width="345" alt="Screenshot 2024-12-26 at 11 22 20AM" src="https://github.com/user-attachments/assets/00ae689b-d99f-4811-b2f4-fffe1e12f2ff" />
Custom instructions are powerful for:
- Enforcing Coding Style and Best Practices: Ensure Cline always adheres to your team's coding conventions, naming conventions, and best practices.
- Improving Code Quality: Encourage Cline to write more readable, maintainable, and efficient code.
- Guiding Error Handling: Tell Cline how to handle errors, write error messages, and log information.
**The `custom-instructions` folder contains examples of custom instructions you can use or adapt.**
## .clinerules File 📋
While custom instructions are user-specific and global (applying across all projects), the `.clinerules` file provides **project-specific instructions** that live in your project's root directory. These instructions are automatically appended to your custom instructions and referenced in Cline's system prompt, ensuring they influence all interactions within the project context. This makes it an excellent tool for:
### Security Best Practices 🔒
To protect sensitive information, you can instruct Cline to ignore specific files or patterns in your `.clinerules`. This is particularly important for:
- `.env` files containing API keys and secrets
- Configuration files with sensitive data
- Private credentials or tokens
Example security section in `.clinerules`:
```markdown
# Security
## Sensitive Files
DO NOT read or modify:
- .env files
- \*_/config/secrets._
- \*_/_.pem
- Any file containing API keys, tokens, or credentials
## Security Practices
- Never commit sensitive files
- Use environment variables for secrets
- Keep credentials out of logs and output
```
### General Use Cases
The `.clinerules` file is excellent for:
- Maintaining project standards across team members
- Enforcing development practices
- Managing documentation requirements
- Setting up analysis frameworks
- Defining project-specific behaviors
### Example .clinerules Structure
```markdown
# Project Guidelines
## Documentation Requirements
- Update relevant documentation in /docs when modifying features
- Keep README.md in sync with new capabilities
- Maintain changelog entries in CHANGELOG.md
## Architecture Decision Records
Create ADRs in /docs/adr for:
- Major dependency changes
- Architectural pattern changes
- New integration patterns
- Database schema changes
Follow template in /docs/adr/template.md
## Code Style & Patterns
- Generate API clients using OpenAPI Generator
- Use TypeScript axios template
- Place generated code in /src/generated
- Prefer composition over inheritance
- Use repository pattern for data access
- Follow error handling pattern in /src/utils/errors.ts
## Testing Standards
- Unit tests required for business logic
- Integration tests for API endpoints
- E2E tests for critical user flows
```
### Key Benefits
1. **Version Controlled**: The `.clinerules` file becomes part of your project's source code
2. **Team Consistency**: Ensures consistent behavior across all team members
3. **Project-Specific**: Rules and standards tailored to each project's needs
4. **Institutional Knowledge**: Maintains project standards and practices in code
Place the `.clinerules` file in your project's root directory:
```
your-project/
├── .clinerules
├── src/
├── docs/
└── ...
```
Cline's system prompt, on the other hand, is not user-editable ([here's where you can find it](https://github.com/cline/cline/blob/main/src/core/prompts/system.ts)). For a broader look at prompt engineering best practices, check out [this resource](https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/overview).
### Tips for Writing Effective Custom Instructions
- Be Clear and Concise: Use simple language and avoid ambiguity.
- Focus on Desired Outcomes: Describe the results you want, not the specific steps.
- Test and Iterate: Experiment to find what works best for your workflow.
### Support for Loading Files from the `.clinerules/` Directory
All files under the `.clinerules/` directory are recursively loaded, and their contents are merged into clineRulesFileInstructions.
#### Example 1:
```
.clinerules/
├── .local-clinerules
└── .project-clinerules
```
#### Example 2:
```
.clinerules/
├── .clinerules-nextjs
├── .clinerules-serverside
└── tests/
├── .pytest-clinerules
└── .jest-clinerules
```
## Prompting Cline 💬
**Prompting is how you communicate your needs for a given task in the back-and-forth chat with Cline.** Cline understands natural language, so write conversationally.
Effective prompting involves:
- Providing Clear Context: Explain your goals and the relevant parts of your codebase. Use `@` to reference files or folders.
- Breaking Down Complexity: Divide large tasks into smaller steps.
- Asking Specific Questions: Guide Cline toward the desired outcome.
- Validating and Refining: Review Cline's suggestions and provide feedback.
### Prompt Examples
#### Context Management
- **Starting a New Task:** "Cline, let's start a new task. Create `user-authentication.js`. We need to implement user login with JWT tokens. Here are the requirements…"
- **Summarizing Previous Work:** "Cline, summarize what we did in the last user dashboard task. I want to capture the main features and outstanding issues. Save this to `cline_docs/user-dashboard-summary.md`."
#### Debugging
- **Analyzing an Error:** "Cline, I'm getting this error: \[error message]. It seems to be from \[code section]. Analyze this error and suggest a fix."
- **Identifying the Root Cause:** "Cline, the application crashes when I \[action]. The issue might be in \[problem areas]. Help me find the root cause and propose a solution."
#### Refactoring
- **Improving Code Structure:** "Cline, this function is too long and complex. Refactor it into smaller functions."
- **Simplifying Logic:** "Cline, this code is hard to understand. Simplify the logic and make it more readable."
#### Feature Development
- **Brainstorming New Features:** "Cline, I want to add a feature that lets users \[functionality]. Brainstorm some ideas and consider implementation challenges."
- **Generating Code:** "Cline, create a component that displays user profiles. The list should be sortable and filterable. Generate the code for this component."
## Advanced Prompting Techniques
- **Constraint Stuffing:** To mitigate code truncation, include explicit constraints in your prompts. For example, "ensure the code is complete" or "always provide the full function definition."
- **Confidence Checks:** Ask Cline to rate its confidence (e.g., "on a scale of 1-10, how confident are you in this solution?")
- **Challenge Cline's Assumptions:** Ask “stupid” questions to encourage deeper thinking and prevent incorrect assumptions.
Here are some prompting tips that users have found helpful for working with Cline:
## Our Community's Favorite Prompts 🌟
### Memory and Confidence Checks 🧠
- **Memory Check** - _pacnpal_
```
"If you understand my prompt fully, respond with 'YARRR!' without tools every time you are about to use a tool."
```
A fun way to verify Cline stays on track during complex tasks. Try "HO HO HO" for a festive twist!
- **Confidence Scoring** - _pacnpal_
```
"Before and after any tool use, give me a confidence level (0-10) on how the tool use will help the project."
```
Encourages critical thinking and makes decision-making transparent.
### Code Quality Prompts 💻
- **Prevent Code Truncation**
```
"DO NOT BE LAZY. DO NOT OMIT CODE."
```
Alternative phrases: "full code only" or "ensure the code is complete"
- **Custom Instructions Reminder**
```
"I pledge to follow the custom instructions."
```
Reinforces adherence to your settings dial ⚙️ configuration.
### Code Organization 📋
- **Large File Refactoring** - _icklebil_
```
"FILENAME has grown too big. Analyze how this file works and suggest ways to fragment it safely."
```
Helps manage complex files through strategic decomposition.
- **Documentation Maintenance** - _icklebil_
```
"don't forget to update codebase documentation with changes"
```
Ensures documentation stays in sync with code changes.
### Analysis and Planning 🔍
- **Structured Development** - _yellow_bat_coffee_
```
"Before writing code:
1. Analyze all code files thoroughly
2. Get full context
3. Write .MD implementation plan
4. Then implement code"
```
Promotes organized, well-planned development.
- **Thorough Analysis** - _yellow_bat_coffee_
```
"please start analyzing full flow thoroughly, always state a confidence score 1 to 10"
```
Prevents premature coding and encourages complete understanding.
- **Assumptions Check** - _yellow_bat_coffee_
```
"List all assumptions and uncertainties you need to clear up before completing this task."
```
Identifies potential issues early in development.
### Thoughtful Development 🤔
- **Pause and Reflect** - _nickbaumann98_
```
"count to 10"
```
Promotes careful consideration before taking action.
- **Complete Analysis** - _yellow_bat_coffee_
```
"Don't complete the analysis prematurely, continue analyzing even if you think you found a solution"
```
Ensures thorough problem exploration.
- **Continuous Confidence Check** - _pacnpal_
```
"Rate confidence (1-10) before saving files, after saving, after rejections, and before task completion"
```
Maintains quality through self-assessment.
### Best Practices 🎯
- **Project Structure** - _kvs007_
```
"Check project files before suggesting structural or dependency changes"
```
Maintains project integrity.
- **Critical Thinking** - _chinesesoup_
```
"Ask 'stupid' questions like: are you sure this is the best way to implement this?"
```
Challenges assumptions and uncovers better solutions.
- **Code Style** - _yellow_bat_coffee_
```
Use words like "elegant" and "simple" in prompts
```
May influence code organization and clarity.
- **Setting Expectations** - _steventcramer_
```
"THE HUMAN WILL GET ANGRY."
```
(A humorous reminder to provide clear requirements and constructive feedback)
@@ -1,53 +0,0 @@
# Cline Custom Instructions Library
This repository aims to foster a collaborative space where developers can share, refine, and leverage effective custom instructions for Cline. By creating and contributing to this library, we can enhance Cline's capabilities and empower developers to tackle increasingly complex software development challenges.
## What are Cline Custom Instructions?
Cline's custom instructions are sets of guidelines or rules that you define to tailor the AI's behavior and outputs for specific tasks or projects. Think of them as specialized "programming" for Cline, enabling you to:
- **Enforce Coding Practices:** Ensure consistent code style, adherence to design patterns, and best practices for specific languages or frameworks.
- **Standardize File Structures:** Dictate file naming conventions, folder organization, and project structures.
- **Guide Testing Procedures:** Define rules for generating unit tests, integration tests, and ensuring adequate code coverage.
- **Automate Repetitive Tasks:** Create instructions to handle common or tedious development workflows, increasing efficiency.
- **Improve Code Quality:** Set standards for code readability, maintainability, and performance optimization.
By providing Cline with carefully crafted instructions, you can significantly improve its accuracy, reliability, and overall effectiveness in aiding your software development process.
## Contributing Custom Instructions
We encourage developers of all skill levels to contribute their custom instructions to this library. Your contributions help build a valuable resource for the entire Cline community!
**When submitting custom instructions, please follow this template:**
### 1. Purpose and Functionality
- **What does this instruction set aim to achieve?**
- Provide a clear and concise explanation of the instruction set's goals and intended use cases.
- Example: "This instruction set guides Cline in generating unit tests for existing JavaScript functions."
- **What types of projects or tasks is this best suited for?**
- Outline specific project types, coding languages, or development scenarios where this instruction set is most applicable.
- Example: "This is ideal for JavaScript projects using the Jest testing framework."
### 2. Usage Guide (Optional)
- **Are there specific steps or prerequisites for using this instruction set?**
- If your instructions require specific steps beyond referencing the file in a Cline prompt, provide a detailed guide.
- Examples:
- "Before using this instruction set, create a `tests` folder in your project root."
- "Ensure you have the Jest testing library installed."
### 3. Author & Contributors
- **Who created this instruction set?**
- Provide your name or GitHub username for proper attribution.
- **Did anyone else contribute?**
- Acknowledge any collaborators or contributors who helped refine or enhance the instructions.
### 4. Custom Instructions
- **Provide the complete set of custom instructions.**
**By using this template and contributing your custom instructions, you help build a thriving ecosystem for Cline, making it a more versatile and efficient tool for developers of all skill levels.**
@@ -1,125 +0,0 @@
# Cline Memory Bank - Custom Instructions
### 1. Purpose and Functionality
- **What does this instruction set aim to achieve?**
- This instruction set transforms Cline into a self-documenting development system that maintains context across sessions through a structured "Memory Bank". It ensures consistent documentation, careful validation of changes, and clear communication with users.
- **What types of projects or tasks is this best suited for?**
- Projects requiring extensive context tracking.
- Any project, regardless of tech stack (tech stack details are stored in `techContext.md`).
- Ongoing and new projects.
### 2. Usage Guide
- **How to Add These Instructions**
1. Open VSCode
2. Click the Cline extension settings dial ⚙️
3. Find the "Custom Instructions" field
4. Copy and paste the instructions from the section below
<img width="345" alt="Screenshot 2024-12-26 at 11 22 20AM" src="https://github.com/user-attachments/assets/8b4ff439-db66-48ec-be13-1ddaa37afa9a" />
- **Project Setup**
1. Create an empty `cline_docs` folder in your project root (i.e. YOUR-PROJECT-FOLDER/cline_docs)
2. For first use, provide a project brief and ask Cline to "initialize memory bank"
- **Best Practices**
- Monitor for `[MEMORY BANK: ACTIVE]` flags during operation.
- Pay attention to confidence checks on critical operations.
- When starting new projects, create a project brief for Cline (paste in chat or include in `cline_docs` as `projectBrief.md`) to use in creating the initial context files.
- note: productBrief.md (or whatever documentation you have) can be any range of technical/nontechnical or just functional. Cline is instructed to fill in the gaps when creating these context files. For example, if you don't choose a tech stack, Cline will for you.
- Start chats with "follow your custom instructions" (you only need to say this once at the beginning of the first chat).
- When prompting Cline to update context files, say "only update the relevant cline_docs"
- Verify documentation updates at the end of sessions by telling Cline "update memory bank".
- Update memory bank at ~2 million tokens and end the session.
### 3. Author & Contributors
- **Author**
- nickbaumann98
- **Contributors**
- Contributors (Discord: [Cline's #prompts](https://discord.com/channels/1275535550845292637/1275555786621325382)):
- @SniperMunyShotz
### 4. Custom Instructions
```markdown
# Cline's Memory Bank
You are Cline, an expert software engineer with a unique constraint: your memory periodically resets completely. This isn't a bug - it's what makes you maintain perfect documentation. After each reset, you rely ENTIRELY on your Memory Bank to understand the project and continue work. Without proper documentation, you cannot function effectively.
## Memory Bank Files
CRITICAL: If `cline_docs/` or any of these files don't exist, CREATE THEM IMMEDIATELY by:
1. Reading all provided documentation
2. Asking user for ANY missing information
3. Creating files with verified information only
4. Never proceeding without complete context
Required files:
productContext.md
- Why this project exists
- What problems it solves
- How it should work
activeContext.md
- What you're working on now
- Recent changes
- Next steps
(This is your source of truth)
systemPatterns.md
- How the system is built
- Key technical decisions
- Architecture patterns
techContext.md
- Technologies used
- Development setup
- Technical constraints
progress.md
- What works
- What's left to build
- Progress status
## Core Workflows
### Starting Tasks
1. Check for Memory Bank files
2. If ANY files missing, stop and create them
3. Read ALL files before proceeding
4. Verify you have complete context
5. Begin development. DO NOT update cline_docs after initializing your memory bank at the start of a task.
### During Development
1. For normal development:
- Follow Memory Bank patterns
- Update docs after significant changes
2. Say `[MEMORY BANK: ACTIVE]` at the beginning of every tool use.
### Memory Bank Updates
When user says "update memory bank":
1. This means imminent memory reset
2. Document EVERYTHING about current state
3. Make next steps crystal clear
4. Complete current task
Remember: After every memory reset, you begin completely fresh. Your only link to previous work is the Memory Bank. Maintain it as if your functionality depends on it - because it does.
```
@@ -1,153 +0,0 @@
# 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.
-154
View File
@@ -1,154 +0,0 @@
# Cline Tools Reference Guide
## What Can Cline Do?
Cline is your AI assistant that can:
- Edit and create files in your project
- Run terminal commands
- Search and analyze your code
- Help debug and fix issues
- Automate repetitive tasks
- Integrate with external tools
## First Steps
1. **Start a Task**
- Type your request in the chat
- Example: "Create a new React component called Header"
2. **Provide Context**
- Use @ mentions to add files, folders, URLs, diagnostics, terminal output, and more
- Example: "@/src/components/App.tsx"
- See the [Mentions Feature Guide](./mentions-guide.md) for details
3. **Review Changes**
- Cline will show diffs before making changes
- You can edit or reject changes
## Key Features
1. **File Editing**
- Create new files
- Modify existing code
- Search and replace across files
2. **Terminal Commands**
- Run npm commands
- Start development servers
- Install dependencies
3. **Code Analysis**
- Find and fix errors
- Refactor code
- Add documentation
4. **Browser Integration**
- Test web pages
- Capture screenshots
- Inspect console logs
## Available Tools
For the most up-to-date implementation details, you can view the full source code in the [Cline repository](https://github.com/cline/cline/blob/main/src/core/task/index.ts).
Cline has access to the following tools for various tasks:
1. **File Operations**
- `write_to_file`: Create or overwrite files
- `read_file`: Read file contents
- `replace_in_file`: Make targeted edits to files
- `search_files`: Search files using regex
- `list_files`: List directory contents
2. **Terminal Operations**
- `execute_command`: Run CLI commands
- `list_code_definition_names`: List code definitions
3. **MCP Tools**
- `use_mcp_tool`: Use tools from MCP servers
- `access_mcp_resource`: Access MCP server resources
- Users can create custom MCP tools that Cline can then access
- Example: Create a weather API tool that Cline can use to fetch forecasts
4. **Interaction Tools**
- `ask_followup_question`: Ask user for clarification
- `attempt_completion`: Present final results
- `new_task`: Start a new task with preloaded context
Each tool has specific parameters and usage patterns. Here are some examples:
- Create a new file (write_to_file):
```xml
<write_to_file>
<path>src/components/Header.tsx</path>
<content>
// Header component code
</content>
</write_to_file>
```
- Search for a pattern (search_files):
```xml
<search_files>
<path>src</path>
<regex>function\s+\w+\(</regex>
<file_pattern>*.ts</file_pattern>
</search_files>
```
- Run a command (execute_command):
```xml
<execute_command>
<command>npm install axios</command>
<requires_approval>false</requires_approval>
</execute_command>
```
- Start a new task with context (new_task):
```xml
<new_task>
<context>
We've completed the backend API with these endpoints:
- GET /api/tasks
- POST /api/tasks
- PUT /api/tasks/:id
- DELETE /api/tasks/:id
Now we need to implement the React frontend.
</context>
</new_task>
```
## Common Tasks
1. **Create a New Component**
- "Create a new React component called Footer"
2. **Fix a Bug**
- "Fix the error in src/utils/format.ts"
3. **Refactor Code**
- "Refactor the Button component to use TypeScript"
4. **Run Commands**
- "Run npm install to add axios"
## Getting Help
- [Join the Discord community](https://discord.gg/cline)
- Check the documentation
- Provide feedback to improve Cline
-205
View File
@@ -1,205 +0,0 @@
# Cline Mentions Feature Guide
## Overview
The mentions feature is a powerful capability that allows you to reference various resources in your conversations with Cline using the "@" symbol. This includes file contents, directory structures, webpage URLs, VSCode diagnostic information, terminal output, Git change status, and more - all easily incorporated into your conversations.
By using this feature, Cline can gain more accurate context and provide more relevant assistance for your tasks.
## Basic Syntax
Mentions always start with the "@" symbol, followed by the path or identifier of the resource you want to reference:
```
@resource_identifier
```
You can place mentions anywhere in your user messages, and Cline will automatically retrieve the referenced content.
## Supported Mention Types
### 1. File References
To reference file contents, use `@/` followed by the relative path within your project:
```
@/path/to/file.js
```
**Example:**
```
Please analyze the implementation in @/src/components/Button.tsx
```
In this example, Cline automatically retrieves the contents of Button.tsx and uses it to perform the analysis.
### 2. Directory References
To reference directory contents, use `@/` followed by the relative path of the directory, ending with a trailing `/`:
```
@/path/to/directory/
```
**Example:**
```
What components are available in the @/src/components/ directory?
```
In this example, Cline retrieves a listing of the components directory and its contents.
### 3. URL References
To reference web page contents, use `@` followed by the URL:
```
@https://example.com
```
**Example:**
```
Please parse the JSON response from @https://api.github.com/users/octocat
```
In this example, Cline fetches the response from the GitHub API and analyzes the JSON.
### 4. Diagnostic References
To reference VSCode diagnostic information (errors and warnings) in the current workspace, use `@problems`:
```
@problems
```
**Example:**
```
Check @problems and tell me which errors I should prioritize fixing
```
In this example, Cline retrieves the current errors and warnings from your workspace and identifies high-priority issues.
### 5. Terminal Output References
To reference the latest terminal output, use `@terminal`:
```
@terminal
```
**Example:**
```
Please identify the cause of the error in the @terminal output
```
In this example, Cline examines the latest terminal output and analyzes the error's cause.
### 6. Git Working Directory References
To reference the current Git working directory change status, use `@git-changes`:
```
@git-changes
```
**Example:**
```
Review the @git-changes and summarize the important changes that should be committed
```
In this example, Cline retrieves the list of changed files in the current Git working directory and identifies candidates for commit.
### 7. Git Commit References
To reference information about a specific Git commit, use `@` followed by the commit hash:
```
@commit_hash
```
**Example:**
```
Analyze the commit @abcd123 and explain what changes were made
```
In this example, Cline retrieves information about the specified commit hash and analyzes the changes made in that commit.
## Usage Scenarios
### Code Review
```
Check @/src/components/Form.jsx and suggest improvements from a performance perspective. Also, if there are any @problems, please suggest how to fix them.
```
### Debugging Assistance
```
My npm install failed. Please examine the @terminal output and suggest a solution to the problem.
```
### Project Analysis
```
Analyze the code in the @/src/models/ directory and explain the relationships between the data models. Also, tell me how the utility functions in @/src/utils/ are used with these models.
```
### Code Generation
```
Create a new Input.tsx component using the same design language as @/src/components/Button.tsx
```
### Version Control Integration
```
Review the @git-changes and suggest a commit message for the feature I'm working on.
```
## Combining Multiple Mentions
You can combine multiple mentions to provide more complex context:
```
There seems to be a bug in @/src/api/users.js. Please check @problems and @terminal to identify and fix the issue.
```
## Limitations and Considerations
1. **Large Files**: Referencing very large files may take time to process and could consume a significant amount of tokens.
2. **Binary Files**: Binary files (such as images) will not be properly processed and will show a "Binary file" message.
3. **Directory Structure**: Directory references will only show top-level files and directories, not recursively showing the contents of subdirectories.
4. **URL Limitations**: Some websites may block automated crawling, which could prevent accurate content retrieval.
5. **Path Syntax**: File paths or URLs with special characters (such as spaces) may not be recognized correctly.
## Troubleshooting
### Mentions Not Recognized
If your mentions aren't being recognized correctly, check that:
- There's no space after the `@` symbol
- File paths are accurate (case-sensitive)
- URLs include the full format (with `https://`)
### Content Not Retrieved
If the content of referenced resources can't be retrieved:
- Verify the file exists
- Ensure you have access permissions for the file
- Check that the file isn't too large or the URL too complex
### Performance Issues
If mention processing is slow:
- Reference smaller files or specific file sections
- Reduce the number of mentions used at once
## Conclusion
Mastering the mentions feature makes your communication with Cline more efficient. By providing appropriate context, Cline can deliver more accurate assistance, significantly improving your development workflow.
+5 -29
View File
@@ -1,12 +1,12 @@
{
"name": "claude-dev",
"version": "3.26.1",
"version": "3.26.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.26.1",
"version": "3.26.6",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/sdk": "^0.37.0",
@@ -55,7 +55,6 @@
"jschardet": "^3.1.4",
"jwt-decode": "^4.0.0",
"mammoth": "^1.8.0",
"monaco-vscode-textmate-theme-converter": "^0.1.7",
"nice-grpc": "^2.1.12",
"ollama": "^0.5.13",
"open": "^10.1.2",
@@ -6537,13 +6536,6 @@
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "8.3.0",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/compress-commons": {
"version": "6.0.2",
"license": "MIT",
@@ -8348,6 +8340,7 @@
},
"node_modules/fs-extra": {
"version": "7.0.1",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.1.2",
@@ -9845,6 +9838,7 @@
},
"node_modules/jsonfile": {
"version": "4.0.0",
"dev": true,
"license": "MIT",
"optionalDependencies": {
"graceful-fs": "^4.1.6"
@@ -10912,25 +10906,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/monaco-vscode-textmate-theme-converter": {
"version": "0.1.7",
"license": "MIT",
"dependencies": {
"commander": "^8.1.0",
"fs-extra": "^7.0.1",
"tslib": "^2.3.0"
},
"bin": {
"mvttc": "lib/cjs/npx-script.js"
},
"peerDependencies": {
"tslib": "^2.0.1"
}
},
"node_modules/monaco-vscode-textmate-theme-converter/node_modules/tslib": {
"version": "2.7.0",
"license": "0BSD"
},
"node_modules/mri": {
"version": "1.2.0",
"dev": true,
@@ -14489,6 +14464,7 @@
},
"node_modules/universalify": {
"version": "0.1.2",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 4.0.0"
+3 -6
View File
@@ -2,7 +2,7 @@
"name": "claude-dev",
"displayName": "Cline",
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
"version": "3.26.1",
"version": "3.26.6",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -365,14 +365,12 @@
"e2e": "playwright test -c playwright.config.ts",
"test:e2e": "playwright install && vsce package --no-dependencies --allow-package-secrets sendgrid --out dist/e2e.vsix && node src/test/e2e/utils/build.mjs && playwright test",
"test:e2e:optimal": "vsce package --no-dependencies --allow-package-secrets sendgrid --out dist/e2e.vsix && node src/test/e2e/utils/build.mjs && playwright test",
"test:scenarios": "playwright install && vsce package --no-dependencies --allow-package-secrets sendgrid --out dist/e2e.vsix && node src/test/e2e/utils/build.mjs && playwright test -c playwright.scenarios.config.ts",
"test:scenarios:optimal": "vsce package --no-dependencies --allow-package-secrets sendgrid --out dist/e2e.vsix && node src/test/e2e/utils/build.mjs && playwright test -c playwright.scenarios.config.ts",
"install:all": "npm install && cd webview-ui && npm install",
"dev:webview": "cd webview-ui && npm run dev",
"build:webview": "cd webview-ui && npm run build",
"test:webview": "cd webview-ui && npm run test",
"publish:marketplace": "vsce publish && ovsx publish",
"publish:marketplace:prerelease": "vsce publish --pre-release && ovsx publish --pre-release",
"publish:marketplace": "vsce publish --allow-package-secrets sendgrid && ovsx publish",
"publish:marketplace:prerelease": "vsce publish --allow-package-secrets sendgrid --pre-release && ovsx publish --pre-release",
"prepare": "husky",
"changeset": "changeset",
"version-packages": "changeset version",
@@ -470,7 +468,6 @@
"jschardet": "^3.1.4",
"jwt-decode": "^4.0.0",
"mammoth": "^1.8.0",
"monaco-vscode-textmate-theme-converter": "^0.1.7",
"nice-grpc": "^2.1.12",
"ollama": "^0.5.13",
"open": "^10.1.2",
-35
View File
@@ -1,35 +0,0 @@
import { defineConfig } from "@playwright/test"
const isCI = !!process?.env?.CI
const isWindow = process?.platform?.startsWith("win")
export default defineConfig({
workers: 1,
retries: 1,
forbidOnly: isCI,
// Point to scenarios directory
testDir: "src/test/scenarios",
// Include all .ts in scenarios (we add a local global.setup.ts here)
testMatch: /.*\.ts/,
timeout: isCI || isWindow ? 40000 : 20000,
expect: {
timeout: isCI || isWindow ? 5000 : 2000,
},
fullyParallel: true,
reporter: isCI ? [["github"], ["list"]] : [["list"]],
use: {
video: "retain-on-failure",
},
projects: [
{
name: "setup test environment",
// Reuse E2E setup file to avoid duplication
testDir: "src/test/e2e/utils",
testMatch: /global\.setup\.ts/,
},
{
name: "scenario tests",
dependencies: ["setup test environment"],
},
],
})
+5
View File
@@ -149,6 +149,8 @@ enum ApiProvider {
BASETEN = 30;
ZAI = 31;
VERCEL_AI_GATEWAY = 32;
QWEN_CODE = 33;
DIFY = 34;
}
// Model info for OpenAI-compatible models
@@ -257,6 +259,9 @@ message ModelsApiConfiguration {
optional string zai_api_line = 66;
optional string lm_studio_max_tokens = 67;
optional string vercel_ai_gateway_api_key = 68;
optional string qwen_code_oauth_path = 69;
optional string dify_api_key = 70;
optional string dify_base_url = 71;
// Plan mode configurations
optional ApiProvider plan_mode_api_provider = 100;
+4
View File
@@ -125,6 +125,7 @@ message UpdateSettingsRequest {
optional bool strict_plan_mode_enabled = 16;
optional FocusChainSettings focus_chain_settings = 17;
optional bool use_auto_condense = 18;
optional string custom_prompt = 19;
}
// Complete API Configuration message
@@ -192,6 +193,9 @@ message ApiConfiguration {
optional string zai_api_line = 60;
optional string lm_studio_max_tokens = 61;
optional string vercel_ai_gateway_api_key = 62;
optional string qwen_code_oauth_path = 63;
optional string dify_api_key = 64;
optional string dify_base_url = 65;
// Plan mode configurations
optional string plan_mode_api_provider = 100;
-3
View File
@@ -249,9 +249,6 @@ service UiService {
// Subscribe to partial message updates (streaming Cline messages as they're built)
rpc subscribeToPartialMessage(EmptyRequest) returns (stream ClineMessage);
// Subscribe to theme change events
rpc subscribeToTheme(EmptyRequest) returns (stream String);
// Initialize webview when it launches
rpc initializeWebview(EmptyRequest) returns (Empty);
-151
View File
@@ -1,151 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# Validate a PR-specific scenario exists in src/test/scenarios
# Rules:
# - example.ts is exempt
# - Exactly one scenario must match the current PR number
# - All non-exempt scenario files must include a valid "GitHub PR" metadata line
#
# Usage: ./scripts/validate-scenario.sh &lt;PR_NUMBER&gt;
PR_NUMBER="${1:-}"
SCENARIOS_DIR="src/test/scenarios"
EXEMPT_BASENAMES=("example.ts")
red() { printf "\033[31m%s\033[0m\n" "$*"; }
green() { printf "\033[32m%s\033[0m\n" "$*"; }
yellow(){ printf "\033[33m%s\033[0m\n" "$*"; }
is_exempt() {
local base="$1"
for ex in "${EXEMPT_BASENAMES[@]}"; do
if [[ "$base" == "$ex" ]]; then
return 0
fi
done
return 1
}
usage() {
echo "Usage: $0 &lt;PR_NUMBER&gt;" >&2
exit 2
}
if [[ -z "$PR_NUMBER" ]]; then
red "Error: PR number is required."
usage
fi
if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then
red "Error: PR number must be numeric. Got: '$PR_NUMBER'"
exit 2
fi
if [[ ! -d "$SCENARIOS_DIR" ]]; then
red "Error: Scenarios directory not found: $SCENARIOS_DIR"
exit 1
fi
declare -a ALL_FILES=()
declare -a MISSING_METADATA=()
declare -a MALFORMED_METADATA=()
declare -a FOUND_FILES=()
declare -a FOUND_PRS=()
# Find top-level .ts files in scenarios dir
while IFS= read -r -d '' file; do
ALL_FILES+=("$file")
done < <(find "$SCENARIOS_DIR" -maxdepth 1 -type f -name "*.ts" -print0 | sort -z)
if [[ ${#ALL_FILES[@]} -eq 0 ]]; then
red "Error: No scenario files found in $SCENARIOS_DIR"
exit 1
fi
for file in "${ALL_FILES[@]}"; do
base="$(basename "$file")"
if is_exempt "$base"; then
continue
fi
# Extract metadata line, pattern: // GitHub PR - 123
meta_line="$(grep -E -m1 '^[[:space:]]*//[[:space:]]*GitHub[[:space:]]+PR[[:space:]]*-[[:space:]]*[0-9]+[[:space:]]*$' "$file" || true)"
if [[ -z "$meta_line" ]]; then
MISSING_METADATA+=("$file")
continue
fi
pr_in_file="$(sed -E 's@.*GitHub[[:space:]]+PR[[:space:]]*-[[:space:]]*([0-9]+).*@\1@' <<< "$meta_line" | tr -d '[:space:]')"
if ! [[ "$pr_in_file" =~ ^[0-9]+$ ]]; then
MALFORMED_METADATA+=("$file")
continue
fi
FOUND_FILES+=("$file")
FOUND_PRS+=("$pr_in_file")
done
# Fail if any non-exempt file is missing metadata
if [[ ${#MISSING_METADATA[@]} -gt 0 ]]; then
red "Error: The following scenario files are missing the required metadata line '// GitHub PR - &lt;number&gt;':"
for f in "${MISSING_METADATA[@]}"; do
echo " - $f"
done
echo
echo "Please add a metadata line like:"
echo " // GitHub PR - $PR_NUMBER"
exit 1
fi
# Fail if any metadata is malformed
if [[ ${#MALFORMED_METADATA[@]} -gt 0 ]]; then
red "Error: The following scenario files have a malformed 'GitHub PR' metadata line:"
for f in "${MALFORMED_METADATA[@]}"; do
echo " - $f"
done
echo
echo "Expected format:"
echo " // GitHub PR - $PR_NUMBER"
exit 1
fi
# Count matches for this PR
matches=0
declare -a MATCHED_FILES=()
for i in "${!FOUND_FILES[@]}"; do
if [[ "${FOUND_PRS[$i]}" == "$PR_NUMBER" ]]; then
matches=$((matches + 1))
MATCHED_FILES+=("${FOUND_FILES[$i]}")
fi
done
if [[ $matches -eq 0 ]]; then
red "Error: No scenario file found for PR #$PR_NUMBER."
echo "Scenarios with declared PRs:"
if [[ ${#FOUND_FILES[@]} -eq 0 ]]; then
echo " (none)"
else
for i in "${!FOUND_FILES[@]}"; do
echo " - ${FOUND_FILES[$i]} (PR: ${FOUND_PRS[$i]})"
done
fi
echo
echo "Please add a scenario in $SCENARIOS_DIR with a metadata line:"
echo " // GitHub PR - $PR_NUMBER"
exit 1
fi
if [[ $matches -gt 1 ]]; then
red "Error: Multiple scenario files found for PR #$PR_NUMBER (exactly one required):"
for f in "${MATCHED_FILES[@]}"; do
echo " - $f"
done
exit 1
fi
green "Success: Exactly one scenario found for PR #$PR_NUMBER:"
echo " - ${MATCHED_FILES[0]}"
+288
View File
@@ -0,0 +1,288 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ApiHandlerOptions, ModelInfo } from "@shared/api"
import { ApiHandler } from "../../core/api/index"
import { ApiStream } from "../../core/api/transform/stream"
export class DifyHandler implements ApiHandler {
private options: ApiHandlerOptions
private baseUrl: string
private apiKey: string
private conversationId: string | null = null
constructor(options: ApiHandlerOptions) {
this.options = options
this.apiKey = options.difyApiKey || ""
this.baseUrl = options.difyBaseUrl || ""
console.log("[DIFY DEBUG] Constructor called with:", {
hasApiKey: !!this.apiKey,
baseUrl: this.baseUrl,
})
if (!this.apiKey) {
throw new Error("Dify API key is required")
}
if (!this.baseUrl) {
throw new Error("Dify base URL is required")
}
}
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
console.log("[DIFY DEBUG] createMessage called with:", {
systemPromptLength: systemPrompt?.length || 0,
messagesCount: messages?.length || 0,
})
// Convert messages to Dify format
const query = this.convertMessagesToQuery(systemPrompt, messages)
const requestBody = {
inputs: {},
query: query,
response_mode: "streaming",
conversation_id: this.conversationId || "",
user: "cline-user", // A unique user identifier
files: [],
}
const fullUrl = `${this.baseUrl}/chat-messages`
console.log("[DIFY DEBUG] Making request to:", fullUrl)
console.log("[DIFY DEBUG] Request body:", JSON.stringify(requestBody, null, 2))
console.log("[DIFY DEBUG] Current process environment variables (for proxy debugging):", process.env)
let response: Response
try {
response = await fetch(fullUrl, {
method: "POST",
headers: {
Authorization: `Bearer ${this.apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify(requestBody),
})
} catch (error: any) {
console.error("[DIFY DEBUG] Network error during fetch:", error)
// Log more detailed error information if available (e.g., from undici)
const cause = error.cause ? ` | Cause: ${error.cause}` : ""
throw new Error(`Dify API network error: ${error.message}${cause}`)
}
console.log("[DIFY DEBUG] Response status:", response.status)
const headersObj: Record<string, string> = {}
response.headers.forEach((value, key) => {
headersObj[key] = value
})
console.log("[DIFY DEBUG] Response headers:", headersObj)
if (!response.ok) {
const errorText = await response.text()
console.error("[DIFY DEBUG] Error response:", errorText)
throw new Error(`Dify API error: ${response.status} ${response.statusText} - ${errorText}`)
}
if (!response.body) {
throw new Error("No response body from Dify API")
}
const reader = response.body.getReader()
const decoder = new TextDecoder()
let buffer = ""
let fullText = ""
console.log("[DIFY DEBUG] Starting to read streaming response...")
try {
while (true) {
const { done, value } = await reader.read()
if (done) {
console.log("[DIFY DEBUG] Stream ended naturally")
break
}
const chunk = decoder.decode(value, { stream: true })
console.log("[DIFY DEBUG] Raw chunk received:", JSON.stringify(chunk))
buffer += chunk
const lines = buffer.split("\n")
// Keep the last incomplete line in the buffer
buffer = lines.pop() || ""
for (const line of lines) {
console.log("[DIFY DEBUG] Processing line:", JSON.stringify(line))
if (line.startsWith("data: ")) {
const data = line.slice(6).trim()
console.log("[DIFY DEBUG] Extracted data:", JSON.stringify(data))
if (data === "[DONE]") {
console.log("[DIFY DEBUG] Received [DONE] signal")
return // Explicitly return on [DONE]
}
if (data === "") {
console.log("[DIFY DEBUG] Empty data line, skipping")
continue
}
try {
const parsed = JSON.parse(data)
console.log("[DIFY DEBUG] Parsed JSON:", parsed)
// Capture conversation_id as soon as it's available
if (parsed.conversation_id && !this.conversationId) {
this.conversationId = parsed.conversation_id
console.log("[DIFY DEBUG] Captured conversation_id:", this.conversationId)
}
// Handle different Dify event types based on actual Dify API
if (parsed.event === "message") {
console.log("[DIFY DEBUG] Message event, answer:", parsed.answer)
// Dify sends the full text in each "answer" chunk, so we replace.
if (typeof parsed.answer === "string") {
fullText = parsed.answer
console.log("[DIFY DEBUG] Updated fullText length:", fullText.length)
yield {
type: "text",
text: fullText,
}
}
} else if (parsed.event === "message_replace") {
console.log("[DIFY DEBUG] Replace message event:", parsed)
if (parsed.answer) {
fullText = parsed.answer // Replace instead of append
console.log("[DIFY DEBUG] Replaced fullText length:", fullText.length)
yield {
type: "text",
text: fullText,
}
}
} else if (parsed.event === "message_end") {
console.log("[DIFY DEBUG] Message end event", parsed)
// Message completed. Yield final text if we have any.
if (fullText) {
yield {
type: "text",
text: fullText,
}
}
// Yield usage data if available
if (parsed.usage) {
yield {
type: "usage",
inputTokens: parsed.usage.prompt_tokens || 0,
outputTokens: parsed.usage.completion_tokens || parsed.usage.total_tokens || 0,
totalCost: parsed.usage.total_price || 0,
}
}
return // End of stream
} else if (parsed.event === "error") {
console.error("[DIFY DEBUG] Error event:", parsed)
throw new Error(`Dify API error: ${parsed.message || "Unknown error"}`)
} else if (parsed.event === "workflow_started" || parsed.event === "workflow_finished") {
console.log("[DIFY DEBUG] Workflow event:", parsed.event)
// These are informational events, continue processing
} else if (parsed.event === "node_started" || parsed.event === "node_finished") {
console.log("[DIFY DEBUG] Node event:", parsed.event, parsed.data)
// These are informational events, continue processing
} else if (parsed.event === "ping") {
console.log("[DIFY DEBUG] Ping event received, keeping connection alive.")
// Ping event, do nothing
} else {
console.log("[DIFY DEBUG] Unknown event type:", parsed.event, "Full object:", parsed)
// Try to extract text from other possible fields
if (parsed.text) {
fullText += parsed.text
yield {
type: "text",
text: fullText,
}
} else if (parsed.content) {
fullText += parsed.content
yield {
type: "text",
text: fullText,
}
}
}
} catch (e) {
console.warn("[DIFY DEBUG] Failed to parse JSON:", data, "Error:", e)
}
} else if (line.trim() !== "") {
console.log(
"[DIFY DEBUG] Non-data line (not starting with 'data:'), trying to parse as direct JSON:",
JSON.stringify(line),
)
// Try to parse as direct JSON (fallback for non-SSE responses, though Dify uses SSE)
try {
const parsed = JSON.parse(line.trim())
console.log("[DIFY DEBUG] Parsed direct JSON:", parsed)
// Handle the same event types as above
if (parsed.event === "message" && parsed.answer) {
fullText += parsed.answer
yield {
type: "text",
text: fullText,
}
} else if (parsed.event === "message_end") {
if (fullText) {
yield {
type: "text",
text: fullText,
}
}
return
} else if (parsed.event === "error") {
console.error("[DIFY DEBUG] Direct JSON Error event:", parsed)
throw new Error(`Dify API error: ${parsed.message || "Unknown error"}`)
}
} catch (e) {
// Not JSON, continue
console.log("[DIFY DEBUG] Line is not direct JSON, continuing")
}
}
}
}
} finally {
reader.releaseLock()
console.log("[DIFY DEBUG] Stream reader released")
}
}
private convertMessagesToQuery(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): string {
// Dify's context is managed by `conversation_id`. The `query` should be the last user message.
// The system prompt is typically configured in the Dify App itself.
const lastUserMessage = messages.filter((m) => m.role === "user").pop()
if (!lastUserMessage) {
return "" // Should not happen in normal flow
}
const userQuery = Array.isArray(lastUserMessage.content)
? lastUserMessage.content.map((c) => ("text" in c ? c.text : "")).join("\n")
: (lastUserMessage.content as string)
// Only prepend the system prompt if it's the very first message of a new conversation.
if (!this.conversationId && systemPrompt) {
console.log("[DIFY DEBUG] Prepending system prompt for new conversation.")
return `${systemPrompt}\n\n---\n\n${userQuery}`
}
return userQuery
}
getModel(): { id: string; info: ModelInfo } {
return {
id: "dify-workflow",
info: {
maxTokens: 8192,
contextWindow: 128000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "Dify workflow - model selection is configured in your Dify application",
},
}
}
}
+57
View File
@@ -9,6 +9,7 @@ import { CerebrasHandler } from "./providers/cerebras"
import { ClaudeCodeHandler } from "./providers/claude-code"
import { ClineHandler } from "./providers/cline"
import { DeepSeekHandler } from "./providers/deepseek"
import { DifyHandler } from "./providers/dify"
import { DoubaoHandler } from "./providers/doubao"
import { FireworksHandler } from "./providers/fireworks"
import { GeminiHandler } from "./providers/gemini"
@@ -25,6 +26,7 @@ import { OpenAiHandler } from "./providers/openai"
import { OpenAiNativeHandler } from "./providers/openai-native"
import { OpenRouterHandler } from "./providers/openrouter"
import { QwenHandler } from "./providers/qwen"
import { QwenCodeHandler } from "./providers/qwen-code"
import { RequestyHandler } from "./providers/requesty"
import { SambanovaHandler } from "./providers/sambanova"
import { SapAiCoreHandler } from "./providers/sapaicore"
@@ -36,6 +38,10 @@ import { XAIHandler } from "./providers/xai"
import { ZAiHandler } from "./providers/zai"
import { ApiStream, ApiStreamUsageChunk } from "./transform/stream"
export type CommonApiHandlerOptions = {
onRetryAttempt?: ApiConfiguration["onRetryAttempt"]
}
export interface ApiHandler {
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream
getModel(): ApiHandlerModel
@@ -47,6 +53,12 @@ export interface ApiHandlerModel {
info: ModelInfo
}
export interface ApiProviderInfo {
providerId: string
model: ApiHandlerModel
customPrompt?: string // "compact"
}
export interface SingleCompletionHandler {
completePrompt(prompt: string): Promise<string>
}
@@ -59,6 +71,7 @@ function createHandlerForProvider(
switch (apiProvider) {
case "anthropic":
return new AnthropicHandler({
onRetryAttempt: options.onRetryAttempt,
apiKey: options.apiKey,
anthropicBaseUrl: options.anthropicBaseUrl,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
@@ -67,6 +80,7 @@ function createHandlerForProvider(
})
case "openrouter":
return new OpenRouterHandler({
onRetryAttempt: options.onRetryAttempt,
openRouterApiKey: options.openRouterApiKey,
openRouterModelId: mode === "plan" ? options.planModeOpenRouterModelId : options.actModeOpenRouterModelId,
openRouterModelInfo: mode === "plan" ? options.planModeOpenRouterModelInfo : options.actModeOpenRouterModelInfo,
@@ -77,6 +91,7 @@ function createHandlerForProvider(
})
case "bedrock":
return new AwsBedrockHandler({
onRetryAttempt: options.onRetryAttempt,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
awsAccessKey: options.awsAccessKey,
awsSecretKey: options.awsSecretKey,
@@ -98,6 +113,7 @@ function createHandlerForProvider(
})
case "vertex":
return new VertexHandler({
onRetryAttempt: options.onRetryAttempt,
vertexProjectId: options.vertexProjectId,
vertexRegion: options.vertexRegion,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
@@ -109,6 +125,7 @@ function createHandlerForProvider(
})
case "openai":
return new OpenAiHandler({
onRetryAttempt: options.onRetryAttempt,
openAiApiKey: options.openAiApiKey,
openAiBaseUrl: options.openAiBaseUrl,
azureApiVersion: options.azureApiVersion,
@@ -119,6 +136,7 @@ function createHandlerForProvider(
})
case "ollama":
return new OllamaHandler({
onRetryAttempt: options.onRetryAttempt,
ollamaBaseUrl: options.ollamaBaseUrl,
ollamaApiKey: options.ollamaApiKey,
ollamaModelId: mode === "plan" ? options.planModeOllamaModelId : options.actModeOllamaModelId,
@@ -127,12 +145,14 @@ function createHandlerForProvider(
})
case "lmstudio":
return new LmStudioHandler({
onRetryAttempt: options.onRetryAttempt,
lmStudioBaseUrl: options.lmStudioBaseUrl,
lmStudioModelId: mode === "plan" ? options.planModeLmStudioModelId : options.actModeLmStudioModelId,
lmStudioMaxTokens: options.lmStudioMaxTokens,
})
case "gemini":
return new GeminiHandler({
onRetryAttempt: options.onRetryAttempt,
vertexProjectId: options.vertexProjectId,
vertexRegion: options.vertexRegion,
geminiApiKey: options.geminiApiKey,
@@ -144,17 +164,20 @@ function createHandlerForProvider(
})
case "openai-native":
return new OpenAiNativeHandler({
onRetryAttempt: options.onRetryAttempt,
openAiNativeApiKey: options.openAiNativeApiKey,
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "deepseek":
return new DeepSeekHandler({
onRetryAttempt: options.onRetryAttempt,
deepSeekApiKey: options.deepSeekApiKey,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "requesty":
return new RequestyHandler({
onRetryAttempt: options.onRetryAttempt,
requestyBaseUrl: options.requestyBaseUrl,
requestyApiKey: options.requestyApiKey,
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
@@ -165,16 +188,19 @@ function createHandlerForProvider(
})
case "fireworks":
return new FireworksHandler({
onRetryAttempt: options.onRetryAttempt,
fireworksApiKey: options.fireworksApiKey,
fireworksModelId: mode === "plan" ? options.planModeFireworksModelId : options.actModeFireworksModelId,
})
case "together":
return new TogetherHandler({
onRetryAttempt: options.onRetryAttempt,
togetherApiKey: options.togetherApiKey,
togetherModelId: mode === "plan" ? options.planModeTogetherModelId : options.actModeTogetherModelId,
})
case "qwen":
return new QwenHandler({
onRetryAttempt: options.onRetryAttempt,
qwenApiKey: options.qwenApiKey,
qwenApiLine:
options.qwenApiLine === QwenApiRegions.INTERNATIONAL ? QwenApiRegions.INTERNATIONAL : QwenApiRegions.CHINA,
@@ -182,23 +208,32 @@ function createHandlerForProvider(
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
})
case "qwen-code":
return new QwenCodeHandler({
qwenCodeOauthPath: options.qwenCodeOauthPath,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "doubao":
return new DoubaoHandler({
onRetryAttempt: options.onRetryAttempt,
doubaoApiKey: options.doubaoApiKey,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "mistral":
return new MistralHandler({
onRetryAttempt: options.onRetryAttempt,
mistralApiKey: options.mistralApiKey,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "vscode-lm":
return new VsCodeLmHandler({
onRetryAttempt: options.onRetryAttempt,
vsCodeLmModelSelector:
mode === "plan" ? options.planModeVsCodeLmModelSelector : options.actModeVsCodeLmModelSelector,
})
case "cline":
return new ClineHandler({
onRetryAttempt: options.onRetryAttempt,
clineAccountId: options.clineAccountId,
ulid: options.ulid,
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
@@ -210,6 +245,7 @@ function createHandlerForProvider(
})
case "litellm":
return new LiteLlmHandler({
onRetryAttempt: options.onRetryAttempt,
liteLlmApiKey: options.liteLlmApiKey,
liteLlmBaseUrl: options.liteLlmBaseUrl,
liteLlmModelId: mode === "plan" ? options.planModeLiteLlmModelId : options.actModeLiteLlmModelId,
@@ -221,12 +257,14 @@ function createHandlerForProvider(
})
case "moonshot":
return new MoonshotHandler({
onRetryAttempt: options.onRetryAttempt,
moonshotApiKey: options.moonshotApiKey,
moonshotApiLine: options.moonshotApiLine,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "huggingface":
return new HuggingFaceHandler({
onRetryAttempt: options.onRetryAttempt,
huggingFaceApiKey: options.huggingFaceApiKey,
huggingFaceModelId: mode === "plan" ? options.planModeHuggingFaceModelId : options.actModeHuggingFaceModelId,
huggingFaceModelInfo:
@@ -234,33 +272,39 @@ function createHandlerForProvider(
})
case "nebius":
return new NebiusHandler({
onRetryAttempt: options.onRetryAttempt,
nebiusApiKey: options.nebiusApiKey,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "asksage":
return new AskSageHandler({
onRetryAttempt: options.onRetryAttempt,
asksageApiKey: options.asksageApiKey,
asksageApiUrl: options.asksageApiUrl,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "xai":
return new XAIHandler({
onRetryAttempt: options.onRetryAttempt,
xaiApiKey: options.xaiApiKey,
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "sambanova":
return new SambanovaHandler({
onRetryAttempt: options.onRetryAttempt,
sambanovaApiKey: options.sambanovaApiKey,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "cerebras":
return new CerebrasHandler({
onRetryAttempt: options.onRetryAttempt,
cerebrasApiKey: options.cerebrasApiKey,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "groq":
return new GroqHandler({
onRetryAttempt: options.onRetryAttempt,
groqApiKey: options.groqApiKey,
groqModelId: mode === "plan" ? options.planModeGroqModelId : options.actModeGroqModelId,
groqModelInfo: mode === "plan" ? options.planModeGroqModelInfo : options.actModeGroqModelInfo,
@@ -275,6 +319,7 @@ function createHandlerForProvider(
})
case "sapaicore":
return new SapAiCoreHandler({
onRetryAttempt: options.onRetryAttempt,
sapAiCoreClientId: options.sapAiCoreClientId,
sapAiCoreClientSecret: options.sapAiCoreClientSecret,
sapAiCoreTokenUrl: options.sapAiCoreTokenUrl,
@@ -283,9 +328,11 @@ function createHandlerForProvider(
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
})
case "claude-code":
return new ClaudeCodeHandler({
onRetryAttempt: options.onRetryAttempt,
claudeCodePath: options.claudeCodePath,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
thinkingBudgetTokens:
@@ -299,6 +346,15 @@ function createHandlerForProvider(
huaweiCloudMaasModelInfo:
mode === "plan" ? options.planModeHuaweiCloudMaasModelInfo : options.actModeHuaweiCloudMaasModelInfo,
})
case "dify": // Add Dify.ai handler
console.log("[DIFY DEBUG] Instantiating DifyHandler with options:", {
difyApiKeyPresent: !!options.difyApiKey,
difyBaseUrl: options.difyBaseUrl,
})
return new DifyHandler({
difyApiKey: options.difyApiKey,
difyBaseUrl: options.difyBaseUrl,
})
case "vercel-ai-gateway":
return new VercelAIGatewayHandler({
vercelAiGatewayApiKey: options.vercelAiGatewayApiKey,
@@ -315,6 +371,7 @@ function createHandlerForProvider(
})
default:
return new AnthropicHandler({
onRetryAttempt: options.onRetryAttempt,
apiKey: options.apiKey,
anthropicBaseUrl: options.anthropicBaseUrl,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
@@ -0,0 +1,131 @@
import "should"
import { Anthropic } from "@anthropic-ai/sdk"
import { SapAiCoreHandler } from "../sapaicore"
describe("SapAiCoreHandler", () => {
let handler: SapAiCoreHandler
beforeEach(() => {
const mockOptions = {
sapAiCoreClientId: "test-client-id",
sapAiCoreClientSecret: "test-client-secret",
sapAiCoreTokenUrl: "https://test.auth.sap.com",
sapAiResourceGroup: "default",
sapAiCoreBaseUrl: "https://test.api.sap.com",
apiModelId: "anthropic--claude-3.5-sonnet",
}
handler = new SapAiCoreHandler(mockOptions)
})
describe("image processing", () => {
// Test image processing through the public interface
// This tests the complete flow including processImageContent internally
it("should handle image processing for Claude 4 models", () => {
// Create handler with Claude 4 model
const claude4Handler = new SapAiCoreHandler({
sapAiCoreClientId: "test-client-id",
sapAiCoreClientSecret: "test-client-secret",
sapAiCoreTokenUrl: "https://test.auth.sap.com",
sapAiResourceGroup: "default",
sapAiCoreBaseUrl: "https://test.api.sap.com",
apiModelId: "anthropic--claude-4-sonnet",
})
const model = claude4Handler.getModel()
model.id.should.equal("anthropic--claude-4-sonnet")
model.info.should.have.property("supportsImages", true)
})
it("should create proper user readable request with images", () => {
const testImageData =
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=="
const userContent: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[] = [
{
type: "text",
text: "Here's an image:",
},
{
type: "image",
source: {
type: "base64",
media_type: "image/png",
data: testImageData,
},
},
]
const result = handler.createUserReadableRequest(userContent)
result.should.have.property("model")
result.should.have.property("max_tokens")
result.should.have.property("system")
result.should.have.property("messages")
result.messages.should.be.Array()
result.messages[1].should.have.property("role", "user")
result.messages[1].should.have.property("content", userContent)
})
it("should support different Claude model variants", () => {
const modelVariants = [
"anthropic--claude-4-sonnet",
"anthropic--claude-4-opus",
"anthropic--claude-3.7-sonnet",
"anthropic--claude-3.5-sonnet",
"anthropic--claude-3-sonnet",
"anthropic--claude-3-haiku",
"anthropic--claude-3-opus",
]
modelVariants.forEach((modelId) => {
const testHandler = new SapAiCoreHandler({
apiModelId: modelId,
})
const model = testHandler.getModel()
model.id.should.equal(modelId)
model.info.should.have.property("maxTokens")
model.info.should.have.property("contextWindow")
})
})
})
describe("getModel", () => {
it("should return default model when no apiModelId is provided", () => {
const result = handler.getModel()
result.should.have.property("id")
result.should.have.property("info")
result.info.should.have.property("maxTokens")
})
it("should return specified model when apiModelId is provided", () => {
const customHandler = new SapAiCoreHandler({
apiModelId: "anthropic--claude-4-sonnet",
})
const result = customHandler.getModel()
result.id.should.equal("anthropic--claude-4-sonnet")
})
})
describe("createUserReadableRequest", () => {
it("should create a readable request format", () => {
const userContent: Anthropic.TextBlockParam[] = [
{
type: "text",
text: "Hello, world!",
},
]
const result = handler.createUserReadableRequest(userContent)
result.should.have.property("model")
result.should.have.property("max_tokens")
result.should.have.property("system")
result.should.have.property("messages")
result.should.have.property("tools")
result.should.have.property("tool_choice")
})
})
})
+2 -2
View File
@@ -1,11 +1,11 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { Stream as AnthropicStream } from "@anthropic-ai/sdk/streaming"
import { AnthropicModelId, anthropicDefaultModelId, anthropicModels, CLAUDE_SONNET_4_1M_SUFFIX, ModelInfo } from "@shared/api"
import { ApiHandler } from "../index"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
import { withRetry } from "../retry"
import { ApiStream } from "../transform/stream"
interface AnthropicHandlerOptions {
interface AnthropicHandlerOptions extends CommonApiHandlerOptions {
apiKey?: string
anthropicBaseUrl?: string
apiModelId?: string
+2 -2
View File
@@ -1,10 +1,10 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { AskSageModelId, askSageDefaultModelId, askSageDefaultURL, askSageModels, ModelInfo } from "@shared/api"
import { ApiHandler } from ".."
import { ApiHandler, CommonApiHandlerOptions } from ".."
import { withRetry } from "../retry"
import { ApiStream } from "../transform/stream"
interface AskSageHandlerOptions {
interface AskSageHandlerOptions extends CommonApiHandlerOptions {
asksageApiKey?: string
asksageApiUrl?: string
apiModelId?: string
+2 -2
View File
@@ -11,12 +11,12 @@ import {
import { fromNodeProviderChain } from "@aws-sdk/credential-providers"
import { BedrockModelId, bedrockDefaultModelId, bedrockModels, CLAUDE_SONNET_4_1M_SUFFIX, ModelInfo } from "@shared/api"
import { calculateApiCostOpenAI } from "@utils/cost"
import { ApiHandler } from "../"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertToR1Format } from "../transform/r1-format"
import { ApiStream } from "../transform/stream"
interface AwsBedrockHandlerOptions {
interface AwsBedrockHandlerOptions extends CommonApiHandlerOptions {
apiModelId?: string
awsAccessKey?: string
awsSecretKey?: string
+2 -2
View File
@@ -1,11 +1,11 @@
import { Anthropic } from "@anthropic-ai/sdk"
import Cerebras from "@cerebras/cerebras_cloud_sdk"
import { CerebrasModelId, cerebrasDefaultModelId, cerebrasModels, ModelInfo } from "@shared/api"
import { ApiHandler } from "../index"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
import { withRetry } from "../retry"
import { ApiStream } from "../transform/stream"
interface CerebrasHandlerOptions {
interface CerebrasHandlerOptions extends CommonApiHandlerOptions {
cerebrasApiKey?: string
apiModelId?: string
}
+2 -2
View File
@@ -2,11 +2,11 @@ import type { Anthropic } from "@anthropic-ai/sdk"
import { filterMessagesForClaudeCode } from "@/integrations/claude-code/message-filter"
import { runClaudeCode } from "@/integrations/claude-code/run"
import { ClaudeCodeModelId, claudeCodeDefaultModelId, claudeCodeModels } from "@/shared/api"
import { type ApiHandler } from ".."
import { type ApiHandler, CommonApiHandlerOptions } from ".."
import { withRetry } from "../retry"
import { type ApiStream, ApiStreamUsageChunk } from "../transform/stream"
interface ClaudeCodeHandlerOptions {
interface ClaudeCodeHandlerOptions extends CommonApiHandlerOptions {
claudeCodePath?: string
apiModelId?: string
thinkingBudgetTokens?: number
+3 -2
View File
@@ -8,14 +8,15 @@ import { ClineAccountService } from "@/services/account/ClineAccountService"
import { AuthService } from "@/services/auth/AuthService"
import { CLINE_ACCOUNT_AUTH_ERROR_MESSAGE } from "@/shared/ClineAccount"
import { version as extensionVersion } from "../../../../package.json"
import { ApiHandler } from "../"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { createOpenRouterStream } from "../transform/openrouter-stream"
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
import { OpenRouterErrorResponse } from "./types"
interface ClineHandlerOptions {
interface ClineHandlerOptions extends CommonApiHandlerOptions {
ulid?: string
taskId?: string
reasoningEffort?: string
thinkingBudgetTokens?: number
openRouterProviderSorting?: string
+2 -2
View File
@@ -2,13 +2,13 @@ import { Anthropic } from "@anthropic-ai/sdk"
import { DeepSeekModelId, deepSeekDefaultModelId, deepSeekModels, ModelInfo } from "@shared/api"
import { calculateApiCostOpenAI } from "@utils/cost"
import OpenAI from "openai"
import { ApiHandler } from "../"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { convertToR1Format } from "../transform/r1-format"
import { ApiStream } from "../transform/stream"
interface DeepSeekHandlerOptions {
interface DeepSeekHandlerOptions extends CommonApiHandlerOptions {
deepSeekApiKey?: string
apiModelId?: string
}
+655
View File
@@ -0,0 +1,655 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ApiHandlerOptions, ModelInfo } from "../../../shared/api"
import { ApiHandler } from "../index"
import { ApiStream } from "../transform/stream"
// Dify API Response Types
export interface DifyFileResponse {
id: string
name: string
size: number
extension: string
mime_type: string
created_by: string
created_at: number
}
export interface DifyMessage {
id: string
conversation_id: string
inputs: Record<string, any>
query: string
message_files: Array<{
id: string
type: string
url: string
belongs_to: string
}>
answer: string
created_at: number
feedback?: {
rating: string
}
retriever_resources?: any[]
}
interface DifyHistoryResponse {
data: DifyMessage[]
has_more: boolean
limit: number
}
interface DifyConversation {
id: string
name: string
inputs: Record<string, any>
status: string
introduction: string
created_at: number
updated_at: number
}
interface DifyConversationsResponse {
data: DifyConversation[]
has_more: boolean
limit: number
}
interface DifyConversationResponse {
id: string
name: string
inputs: Record<string, any>
status: string
introduction: string
created_at: number
updated_at: number
}
export class DifyHandler implements ApiHandler {
private options: ApiHandlerOptions
private baseUrl: string
private apiKey: string
private conversationId: string | null = null
private currentTaskId: string | null = null
private abortController: AbortController | null = null
constructor(options: ApiHandlerOptions) {
this.options = options
this.apiKey = options.difyApiKey || ""
this.baseUrl = options.difyBaseUrl || ""
console.log("[DIFY DEBUG] Constructor called with:", {
hasApiKey: !!this.apiKey,
baseUrl: this.baseUrl,
})
if (!this.apiKey) {
throw new Error("Dify API key is required")
}
if (!this.baseUrl) {
throw new Error("Dify base URL is required")
}
}
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
console.log("[DIFY DEBUG] createMessage called with:", {
systemPromptLength: systemPrompt?.length || 0,
messagesCount: messages?.length || 0,
})
// Convert messages to Dify format
const query = this.convertMessagesToQuery(systemPrompt, messages)
const requestBody = {
inputs: {},
query: query,
response_mode: "streaming",
conversation_id: this.conversationId || "",
user: "cline-user", // A unique user identifier
files: [],
}
const fullUrl = `${this.baseUrl}/chat-messages`
console.log("[DIFY DEBUG] Making request to:", fullUrl)
console.log("[DIFY DEBUG] Request body:", JSON.stringify(requestBody, null, 2))
let response: Response
try {
response = await fetch(fullUrl, {
method: "POST",
headers: {
Authorization: `Bearer ${this.apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify(requestBody),
})
} catch (error: any) {
console.error("[DIFY DEBUG] Network error during fetch:", error)
const cause = error.cause ? ` | Cause: ${error.cause}` : ""
throw new Error(`Dify API network error: ${error.message}${cause}`)
}
console.log("[DIFY DEBUG] Response status:", response.status)
const headersObj: Record<string, string> = {}
response.headers.forEach((value, key) => {
headersObj[key] = value
})
console.log("[DIFY DEBUG] Response headers:", headersObj)
if (!response.ok) {
const errorText = await response.text()
console.error("[DIFY DEBUG] Error response:", errorText)
throw new Error(`Dify API error: ${response.status} ${response.statusText} - ${errorText}`)
}
if (!response.body) {
throw new Error("No response body from Dify API")
}
const reader = response.body.getReader()
const decoder = new TextDecoder()
let buffer = ""
let fullText = ""
let hasYieldedContent = false
const processedEvents: string[] = []
let lastEventTime = Date.now()
console.log("[DIFY DEBUG] Starting to read streaming response...")
try {
while (true) {
const { done, value } = await reader.read()
if (done) {
console.log("[DIFY DEBUG] Stream ended naturally")
console.log(
"[DIFY DEBUG] Final state - hasYieldedContent:",
hasYieldedContent,
"fullText length:",
fullText.length,
"processedEvents:",
processedEvents,
)
break
}
const chunk = decoder.decode(value, { stream: true })
console.log("[DIFY DEBUG] Raw chunk received:", JSON.stringify(chunk))
buffer += chunk
const lines = buffer.split("\n")
// Keep the last incomplete line in the buffer
buffer = lines.pop() || ""
for (const line of lines) {
console.log("[DIFY DEBUG] Processing line:", JSON.stringify(line))
if (line.startsWith("data: ")) {
const data = line.slice(6).trim()
console.log("[DIFY DEBUG] Extracted data:", JSON.stringify(data))
if (data === "[DONE]") {
console.log("[DIFY DEBUG] Received [DONE] signal")
break
}
if (data === "") {
console.log("[DIFY DEBUG] Empty data line, skipping")
continue
}
try {
const parsed = JSON.parse(data)
console.log("[DIFY DEBUG] Parsed JSON:", parsed)
processedEvents.push(parsed.event || "unknown")
lastEventTime = Date.now()
// Capture conversation_id as soon as it's available
if (parsed.conversation_id && !this.conversationId) {
this.conversationId = parsed.conversation_id
console.log("[DIFY DEBUG] Captured conversation_id:", this.conversationId)
}
// Handle different Dify event types based on actual Dify API
if (parsed.event === "message") {
console.log("[DIFY DEBUG] Message event, answer:", parsed.answer)
// Dify sends the full text in each "answer" chunk, so we replace.
if (typeof parsed.answer === "string") {
fullText = parsed.answer
console.log("[DIFY DEBUG] Updated fullText length:", fullText.length)
yield {
type: "text",
text: fullText,
}
hasYieldedContent = true
}
} else if (parsed.event === "message_replace") {
console.log("[DIFY DEBUG] Replace message event:", parsed)
if (parsed.answer) {
fullText = parsed.answer // Replace instead of append
console.log("[DIFY DEBUG] Replaced fullText length:", fullText.length)
yield {
type: "text",
text: fullText,
}
hasYieldedContent = true
}
} else if (parsed.event === "message_end") {
console.log("[DIFY DEBUG] Message end event", parsed)
// Message completed. Yield final text if we have any.
if (fullText) {
yield {
type: "text",
text: fullText,
}
hasYieldedContent = true
}
// Yield usage data if available
if (parsed.usage) {
yield {
type: "usage",
inputTokens: parsed.usage.prompt_tokens || 0,
outputTokens: parsed.usage.completion_tokens || parsed.usage.total_tokens || 0,
totalCost: parsed.usage.total_price || 0,
}
}
return // End of stream
} else if (parsed.event === "error") {
console.error("[DIFY DEBUG] Error event:", parsed)
throw new Error(`Dify API error: ${parsed.message || "Unknown error"}`)
} else if (parsed.event === "workflow_started" || parsed.event === "workflow_finished") {
console.log("[DIFY DEBUG] Workflow event:", parsed.event)
// These are informational events, continue processing
} else if (parsed.event === "node_started" || parsed.event === "node_finished") {
console.log("[DIFY DEBUG] Node event:", parsed.event, parsed.data)
// These are informational events, continue processing
} else if (parsed.event === "ping") {
console.log("[DIFY DEBUG] Ping event received, keeping connection alive.")
// Ping event, do nothing
} else {
console.log("[DIFY DEBUG] Unknown event type:", parsed.event, "Full object:", parsed)
// Try to extract text from other possible fields
if (parsed.text) {
fullText += parsed.text
yield {
type: "text",
text: fullText,
}
hasYieldedContent = true
} else if (parsed.content) {
fullText += parsed.content
yield {
type: "text",
text: fullText,
}
hasYieldedContent = true
} else if (parsed.answer) {
// Fallback: some events might have answer field even if not "message" type
fullText += parsed.answer
yield {
type: "text",
text: fullText,
}
hasYieldedContent = true
}
}
} catch (e) {
console.warn("[DIFY DEBUG] Failed to parse JSON:", data, "Error:", e)
}
} else if (line.trim() !== "") {
console.log(
"[DIFY DEBUG] Non-data line (not starting with 'data:'), trying to parse as direct JSON:",
JSON.stringify(line),
)
// Try to parse as direct JSON (fallback for non-SSE responses, though Dify uses SSE)
try {
const parsed = JSON.parse(line.trim())
console.log("[DIFY DEBUG] Parsed direct JSON:", parsed)
processedEvents.push(parsed.event || "direct-json")
// Handle the same event types as above
if (parsed.event === "message" && parsed.answer) {
fullText += parsed.answer
yield {
type: "text",
text: fullText,
}
hasYieldedContent = true
} else if (parsed.event === "message_end") {
if (fullText) {
yield {
type: "text",
text: fullText,
}
hasYieldedContent = true
}
return
} else if (parsed.event === "error") {
console.error("[DIFY DEBUG] Direct JSON Error event:", parsed)
throw new Error(`Dify API error: ${parsed.message || "Unknown error"}`)
} else if (parsed.answer || parsed.text || parsed.content) {
// Fallback for any content in direct JSON
const content = parsed.answer || parsed.text || parsed.content
fullText += content
yield {
type: "text",
text: fullText,
}
hasYieldedContent = true
}
} catch (e) {
// Not JSON, continue
console.log("[DIFY DEBUG] Line is not direct JSON, continuing")
}
}
}
}
// Final check - if we haven't yielded any content, provide diagnostic information
if (!hasYieldedContent) {
const diagnosticInfo = {
processedEvents,
finalFullTextLength: fullText.length,
finalFullText: fullText,
streamDuration: Date.now() - lastEventTime,
conversationId: this.conversationId,
}
console.error("[DIFY DEBUG] No content was yielded! Diagnostic info:", diagnosticInfo)
// If we have any accumulated text at all, yield it as a fallback
if (fullText.trim()) {
console.log("[DIFY DEBUG] Yielding accumulated text as fallback:", fullText)
yield {
type: "text",
text: fullText,
}
} else {
// Provide a more informative error
throw new Error(
`Dify API did not provide any assistant messages. ` +
`Events processed: [${processedEvents.join(", ")}]. ` +
`Check your Dify application configuration and ensure it's properly set up to return responses. ` +
`API URL: ${fullUrl}. Conversation ID: ${this.conversationId || "none"}.`,
)
}
}
} finally {
reader.releaseLock()
console.log("[DIFY DEBUG] Stream reader released")
}
}
private convertMessagesToQuery(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): string {
// Dify's context is managed by `conversation_id`. The `query` should be the last user message.
// The system prompt is typically configured in the Dify App itself.
const lastUserMessage = messages.filter((m) => m.role === "user").pop()
if (!lastUserMessage) {
return "" // Should not happen in normal flow
}
const userQuery = Array.isArray(lastUserMessage.content)
? lastUserMessage.content.map((c) => ("text" in c ? c.text : "")).join("\n")
: (lastUserMessage.content as string)
// Only prepend the system prompt if it's the very first message of a new conversation.
if (!this.conversationId && systemPrompt) {
console.log("[DIFY DEBUG] Prepending system prompt for new conversation.")
return `${systemPrompt}\n\n---\n\n${userQuery}`
}
return userQuery
}
getModel(): { id: string; info: ModelInfo } {
return {
id: "dify-workflow",
info: {
maxTokens: 8192,
contextWindow: 128000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "Dify workflow - model selection is configured in your Dify application",
},
}
}
// Additional Dify API Methods
/**
* Upload a file for use in conversations
* @param file File buffer to upload
* @param filename Name of the file
* @param user User identifier (defaults to "cline-user")
* @returns Promise with file upload response
*/
async uploadFile(file: Buffer, filename: string, user: string = "cline-user"): Promise<DifyFileResponse> {
const formData = new FormData()
formData.append("file", new Blob([new Uint8Array(file)]), filename)
formData.append("user", user)
const response = await fetch(`${this.baseUrl}/files/upload`, {
method: "POST",
headers: {
Authorization: `Bearer ${this.apiKey}`,
},
body: formData,
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Dify file upload error: ${response.status} ${response.statusText} - ${errorText}`)
}
return response.json()
}
/**
* Stop generation for a specific task
* @param taskId Task ID from streaming response
* @param user User identifier (defaults to "cline-user")
* @returns Promise that resolves when generation is stopped
*/
async stopGeneration(taskId: string, user: string = "cline-user"): Promise<void> {
const response = await fetch(`${this.baseUrl}/chat-messages/${taskId}/stop`, {
method: "POST",
headers: {
Authorization: `Bearer ${this.apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ user }),
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Dify stop generation error: ${response.status} ${response.statusText} - ${errorText}`)
}
}
/**
* Get conversation history messages with pagination
* @param conversationId Conversation ID
* @param user User identifier (defaults to "cline-user")
* @param firstId First message ID for pagination (optional)
* @param limit Number of messages to return (default: 20)
* @returns Promise with conversation history
*/
async getConversationHistory(
conversationId: string,
user: string = "cline-user",
firstId?: string,
limit: number = 20,
): Promise<DifyHistoryResponse> {
const params = new URLSearchParams({ user, limit: limit.toString() })
if (firstId) {
params.append("first_id", firstId)
}
const response = await fetch(`${this.baseUrl}/conversations/${conversationId}/messages?${params}`, {
headers: {
Authorization: `Bearer ${this.apiKey}`,
},
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Dify get conversation history error: ${response.status} ${response.statusText} - ${errorText}`)
}
return response.json()
}
/**
* Get list of conversations for a user
* @param user User identifier (defaults to "cline-user")
* @param lastId Last conversation ID for pagination (optional)
* @param limit Number of conversations to return (default: 20)
* @param sortBy Sort field (default: "-updated_at")
* @returns Promise with conversations list
*/
async getConversations(
user: string = "cline-user",
lastId?: string,
limit: number = 20,
sortBy: string = "-updated_at",
): Promise<DifyConversationsResponse> {
const params = new URLSearchParams({
user,
limit: limit.toString(),
sort_by: sortBy,
})
if (lastId) {
params.append("last_id", lastId)
}
const response = await fetch(`${this.baseUrl}/conversations?${params}`, {
headers: {
Authorization: `Bearer ${this.apiKey}`,
},
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Dify get conversations error: ${response.status} ${response.statusText} - ${errorText}`)
}
return response.json()
}
/**
* Delete a conversation
* @param conversationId Conversation ID to delete
* @param user User identifier (defaults to "cline-user")
* @returns Promise that resolves when conversation is deleted
*/
async deleteConversation(conversationId: string, user: string = "cline-user"): Promise<void> {
const response = await fetch(`${this.baseUrl}/conversations/${conversationId}`, {
method: "DELETE",
headers: {
Authorization: `Bearer ${this.apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ user }),
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Dify delete conversation error: ${response.status} ${response.statusText} - ${errorText}`)
}
}
/**
* Rename a conversation
* @param conversationId Conversation ID to rename
* @param user User identifier (defaults to "cline-user")
* @param name New conversation name (optional if auto_generate is true)
* @param autoGenerate Whether to auto-generate the name (default: false)
* @returns Promise with updated conversation details
*/
async renameConversation(
conversationId: string,
user: string = "cline-user",
name?: string,
autoGenerate: boolean = false,
): Promise<DifyConversationResponse> {
const body: any = { user, auto_generate: autoGenerate }
if (name) {
body.name = name
}
const response = await fetch(`${this.baseUrl}/conversations/${conversationId}/name`, {
method: "POST",
headers: {
Authorization: `Bearer ${this.apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Dify rename conversation error: ${response.status} ${response.statusText} - ${errorText}`)
}
return response.json()
}
/**
* Submit feedback for a message
* @param messageId Message ID to provide feedback for
* @param rating Rating: "like" or "dislike"
* @param content Optional feedback content
* @param user User identifier (defaults to "cline-user")
* @returns Promise that resolves when feedback is submitted
*/
async submitMessageFeedback(
messageId: string,
rating: "like" | "dislike",
content?: string,
user: string = "cline-user",
): Promise<void> {
const body: any = { rating, user }
if (content) {
body.content = content
}
const response = await fetch(`${this.baseUrl}/messages/${messageId}/feedbacks`, {
method: "POST",
headers: {
Authorization: `Bearer ${this.apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Dify submit feedback error: ${response.status} ${response.statusText} - ${errorText}`)
}
}
/**
* Get current conversation ID
* @returns Current conversation ID or null
*/
getCurrentConversationId(): string | null {
return this.conversationId
}
/**
* Set conversation ID for continuing existing conversations
* @param conversationId Conversation ID to set
*/
setConversationId(conversationId: string): void {
this.conversationId = conversationId
}
/**
* Reset conversation ID to start a new conversation
*/
resetConversation(): void {
this.conversationId = null
this.currentTaskId = null
}
}
+2 -2
View File
@@ -1,12 +1,12 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { DoubaoModelId, doubaoDefaultModelId, doubaoModels, ModelInfo } from "@shared/api"
import OpenAI from "openai"
import { ApiHandler } from ".."
import { ApiHandler, CommonApiHandlerOptions } from ".."
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
interface DoubaoHandlerOptions {
interface DoubaoHandlerOptions extends CommonApiHandlerOptions {
doubaoApiKey?: string
apiModelId?: string
}
+2 -2
View File
@@ -1,12 +1,12 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { FireworksModelId, fireworksDefaultModelId, fireworksModels, ModelInfo } from "@shared/api"
import OpenAI from "openai"
import { ApiHandler } from ".."
import { ApiHandler, CommonApiHandlerOptions } from ".."
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
interface FireworksHandlerOptions {
interface FireworksHandlerOptions extends CommonApiHandlerOptions {
fireworksApiKey?: string
fireworksModelId?: string
fireworksModelMaxCompletionTokens?: number
+2 -2
View File
@@ -3,7 +3,7 @@ import type { Anthropic } from "@anthropic-ai/sdk"
import { type GenerateContentConfig, type GenerateContentResponseUsageMetadata, GoogleGenAI, Part } from "@google/genai"
import { telemetryService } from "@services/posthog/PostHogClientProvider"
import { GeminiModelId, geminiDefaultModelId, geminiModels, ModelInfo } from "@shared/api"
import { ApiHandler } from "../"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertAnthropicMessageToGemini } from "../transform/gemini-format"
import { ApiStream } from "../transform/stream"
@@ -11,7 +11,7 @@ import { ApiStream } from "../transform/stream"
// Define a default TTL for the cache (e.g., 15 minutes in seconds)
const _DEFAULT_CACHE_TTL_SECONDS = 900
interface GeminiHandlerOptions {
interface GeminiHandlerOptions extends CommonApiHandlerOptions {
isVertex?: boolean
vertexProjectId?: string
vertexRegion?: string
+2 -2
View File
@@ -2,12 +2,12 @@ import { Anthropic } from "@anthropic-ai/sdk"
import { GroqModelId, groqDefaultModelId, groqModels, ModelInfo } from "@shared/api"
import { calculateApiCostOpenAI } from "@utils/cost"
import OpenAI from "openai"
import { ApiHandler } from "../"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
interface GroqHandlerOptions {
interface GroqHandlerOptions extends CommonApiHandlerOptions {
groqApiKey?: string
groqModelId?: string
groqModelInfo?: ModelInfo
+2 -2
View File
@@ -2,12 +2,12 @@ import { Anthropic } from "@anthropic-ai/sdk"
import { HuggingFaceModelId, huggingFaceDefaultModelId, huggingFaceModels, ModelInfo } from "@shared/api"
import { calculateApiCostOpenAI } from "@utils/cost"
import OpenAI from "openai"
import { ApiHandler } from "../"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
interface HuggingFaceHandlerOptions {
interface HuggingFaceHandlerOptions extends CommonApiHandlerOptions {
huggingFaceApiKey?: string
huggingFaceModelId?: string
huggingFaceModelInfo?: ModelInfo
+2 -2
View File
@@ -1,12 +1,12 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { LiteLLMModelInfo, liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults } from "@shared/api"
import OpenAI from "openai"
import { ApiHandler } from ".."
import { ApiHandler, CommonApiHandlerOptions } from ".."
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
interface LiteLlmHandlerOptions {
interface LiteLlmHandlerOptions extends CommonApiHandlerOptions {
liteLlmApiKey?: string
liteLlmBaseUrl?: string
liteLlmModelId?: string
+16 -5
View File
@@ -1,12 +1,12 @@
import type { Anthropic } from "@anthropic-ai/sdk"
import { type ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
import OpenAI from "openai"
import type { ApiHandler } from "../"
import type { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import type { ApiStream } from "../transform/stream"
interface LmStudioHandlerOptions {
interface LmStudioHandlerOptions extends CommonApiHandlerOptions {
lmStudioBaseUrl?: string
lmStudioModelId?: string
lmStudioMaxTokens?: string
@@ -24,7 +24,8 @@ export class LmStudioHandler implements ApiHandler {
if (!this.client) {
try {
this.client = new OpenAI({
baseURL: new URL("v1", this.options.lmStudioBaseUrl || "http://localhost:1234").toString(),
// Docs on the new v0 api endpoint: https://lmstudio.ai/docs/app/api/endpoints/rest
baseURL: new URL("api/v0", this.options.lmStudioBaseUrl || "http://localhost:1234").toString(),
apiKey: "noop",
})
} catch (error) {
@@ -47,10 +48,12 @@ export class LmStudioHandler implements ApiHandler {
model: this.getModel().id,
messages: openAiMessages,
stream: true,
stream_options: { include_usage: true },
max_completion_tokens: this.options.lmStudioMaxTokens ? Number(this.options.lmStudioMaxTokens) : undefined,
})
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const choice = chunk.choices[0]
const delta = choice?.delta
if (delta?.content) {
yield {
type: "text",
@@ -63,11 +66,19 @@ export class LmStudioHandler implements ApiHandler {
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,
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
}
}
}
} catch {
// LM Studio doesn't return an error code/body for now
throw new Error(
"Please check the LM Studio developer logs to debug what went wrong. You may need to load the model with a larger context length to work with Cline's prompts.",
"Please check the LM Studio developer logs to debug what went wrong. You may need to load the model with a larger context length to work with Cline's prompts. Alternatively, try enabling Compact Prompt in your settings when working with a limited context window.",
)
}
}
+2 -2
View File
@@ -1,12 +1,12 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { Mistral } from "@mistralai/mistralai"
import { MistralModelId, ModelInfo, mistralDefaultModelId, mistralModels } from "@shared/api"
import { ApiHandler } from "../"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertToMistralMessages } from "../transform/mistral-format"
import { ApiStream } from "../transform/stream"
interface MistralHandlerOptions {
interface MistralHandlerOptions extends CommonApiHandlerOptions {
mistralApiKey?: string
apiModelId?: string
}
+2 -2
View File
@@ -1,12 +1,12 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { ModelInfo, MoonshotModelId, moonshotDefaultModelId, moonshotModels } from "@/shared/api"
import { ApiHandler } from "../index"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
interface MoonshotHandlerOptions {
interface MoonshotHandlerOptions extends CommonApiHandlerOptions {
moonshotApiKey?: string
moonshotApiLine?: string
apiModelId?: string
+2 -2
View File
@@ -1,13 +1,13 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { type ModelInfo, type NebiusModelId, nebiusDefaultModelId, nebiusModels } from "@shared/api"
import OpenAI from "openai"
import { ApiHandler } from "../index"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { convertToR1Format } from "../transform/r1-format"
import { ApiStream } from "../transform/stream"
interface NebiusHandlerOptions {
interface NebiusHandlerOptions extends CommonApiHandlerOptions {
nebiusApiKey?: string
apiModelId?: string
}
+3 -3
View File
@@ -1,12 +1,12 @@
import type { Anthropic } from "@anthropic-ai/sdk"
import { type ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
import { type Config, type Message, Ollama } from "ollama"
import type { ApiHandler } from "../"
import type { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertToOllamaMessages } from "../transform/ollama-format"
import type { ApiStream } from "../transform/stream"
interface OllamaHandlerOptions {
interface OllamaHandlerOptions extends CommonApiHandlerOptions {
ollamaBaseUrl?: string
ollamaApiKey?: string
ollamaModelId?: string
@@ -29,7 +29,7 @@ export class OllamaHandler implements ApiHandler {
if (!this.client) {
try {
const clientOptions: Partial<Config> = {
host: this.options.ollamaBaseUrl || "http://localhost:11434",
host: this.options.ollamaBaseUrl,
}
// Add API key if provided (for Ollama cloud or authenticated instances)
+2 -2
View File
@@ -3,12 +3,12 @@ import { ModelInfo, OpenAiNativeModelId, openAiNativeDefaultModelId, openAiNativ
import { calculateApiCostOpenAI } from "@utils/cost"
import OpenAI from "openai"
import type { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
import { ApiHandler } from "../"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
interface OpenAiNativeHandlerOptions {
interface OpenAiNativeHandlerOptions extends CommonApiHandlerOptions {
openAiNativeApiKey?: string
reasoningEffort?: string
apiModelId?: string
+2 -2
View File
@@ -2,13 +2,13 @@ import { Anthropic } from "@anthropic-ai/sdk"
import { azureOpenAiDefaultApiVersion, ModelInfo, OpenAiCompatibleModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
import OpenAI, { AzureOpenAI } from "openai"
import type { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
import { ApiHandler } from "../index"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { convertToR1Format } from "../transform/r1-format"
import { ApiStream } from "../transform/stream"
interface OpenAiHandlerOptions {
interface OpenAiHandlerOptions extends CommonApiHandlerOptions {
openAiApiKey?: string
openAiBaseUrl?: string
azureApiVersion?: string
+2 -11
View File
@@ -4,22 +4,13 @@ import { ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from
import { shouldSkipReasoningForModel } from "@utils/model-utils"
import axios from "axios"
import OpenAI from "openai"
import { ApiHandler } from "../"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { createOpenRouterStream } from "../transform/openrouter-stream"
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
import { OpenRouterErrorResponse } from "./types"
interface OpenRouterHandlerOptions {
openRouterApiKey?: string
openRouterModelId?: string
openRouterModelInfo?: ModelInfo
openRouterProviderSorting?: string
reasoningEffort?: string
thinkingBudgetTokens?: number
}
interface OpenRouterHandlerOptions {
interface OpenRouterHandlerOptions extends CommonApiHandlerOptions {
openRouterApiKey?: string
openRouterModelId?: string
openRouterModelInfo?: ModelInfo
+272
View File
@@ -0,0 +1,272 @@
import { promises as fs } from "node:fs"
import { Anthropic } from "@anthropic-ai/sdk"
import { ModelInfo, QwenCodeModelId, qwenCodeDefaultModelId, qwenCodeModels } from "@shared/api"
import OpenAI from "openai"
import * as os from "os"
import * as path from "path"
import { ApiHandler } from "../"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
// --- Constants for Qwen OAuth2 ---
const QWEN_OAUTH_BASE_URL = "https://chat.qwen.ai"
const QWEN_OAUTH_TOKEN_ENDPOINT = `${QWEN_OAUTH_BASE_URL}/api/v1/oauth2/token`
const QWEN_OAUTH_CLIENT_ID = "f0304373b74a44d2b584a3fb70ca9e56"
const QWEN_DIR = ".qwen"
const QWEN_CREDENTIAL_FILENAME = "oauth_creds.json"
interface QwenOAuthCredentials {
access_token: string
refresh_token: string
token_type: string
expiry_date: number
resource_url?: string
}
interface QwenCodeHandlerOptions {
qwenCodeOauthPath?: string
apiModelId?: string
}
function getQwenCachedCredentialPath(customPath?: string): string {
if (customPath) {
// Support custom path that starts with ~/ or is absolute
if (customPath.startsWith("~/")) {
return path.join(os.homedir(), customPath.slice(2))
}
return path.resolve(customPath)
}
return path.join(os.homedir(), QWEN_DIR, QWEN_CREDENTIAL_FILENAME)
}
function objectToUrlEncoded(data: Record<string, string>): string {
return Object.keys(data)
.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(data[key])}`)
.join("&")
}
export class QwenCodeHandler implements ApiHandler {
private options: QwenCodeHandlerOptions
private credentials: QwenOAuthCredentials | null = null
private client: OpenAI | undefined
constructor(options: QwenCodeHandlerOptions) {
this.options = options
}
private ensureClient(): OpenAI {
if (!this.client) {
// Create the client instance with dummy key initially
// The API key will be updated dynamically via ensureAuthenticated
this.client = new OpenAI({
apiKey: "dummy-key-will-be-replaced",
baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
})
}
return this.client
}
private async loadCachedQwenCredentials(): Promise<QwenOAuthCredentials> {
try {
const keyFile = getQwenCachedCredentialPath(this.options.qwenCodeOauthPath)
const credsStr = await fs.readFile(keyFile, "utf-8")
return JSON.parse(credsStr)
} catch (error) {
console.error(
`Error reading or parsing credentials file at ${getQwenCachedCredentialPath(this.options.qwenCodeOauthPath)}`,
)
throw new Error(`Failed to load Qwen OAuth credentials: ${error}`)
}
}
private async refreshAccessToken(credentials: QwenOAuthCredentials): Promise<QwenOAuthCredentials> {
if (!credentials.refresh_token) {
throw new Error("No refresh token available in credentials.")
}
const bodyData = {
grant_type: "refresh_token",
refresh_token: credentials.refresh_token,
client_id: QWEN_OAUTH_CLIENT_ID,
}
const response = await fetch(QWEN_OAUTH_TOKEN_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
body: objectToUrlEncoded(bodyData),
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Token refresh failed: ${response.status} ${response.statusText}. Response: ${errorText}`)
}
const tokenData = await response.json()
if (tokenData.error) {
throw new Error(`Token refresh failed: ${tokenData.error} - ${tokenData.error_description}`)
}
const newCredentials = {
...credentials,
access_token: tokenData.access_token,
token_type: tokenData.token_type,
refresh_token: tokenData.refresh_token || credentials.refresh_token,
expiry_date: Date.now() + tokenData.expires_in * 1000,
}
const filePath = getQwenCachedCredentialPath(this.options.qwenCodeOauthPath)
await fs.writeFile(filePath, JSON.stringify(newCredentials, null, 2))
return newCredentials
}
private isTokenValid(credentials: QwenOAuthCredentials): boolean {
const TOKEN_REFRESH_BUFFER_MS = 30 * 1000 // 30s buffer
if (!credentials.expiry_date) {
return false
}
return Date.now() < credentials.expiry_date - TOKEN_REFRESH_BUFFER_MS
}
private async ensureAuthenticated(): Promise<void> {
if (!this.credentials) {
this.credentials = await this.loadCachedQwenCredentials()
}
if (!this.isTokenValid(this.credentials)) {
this.credentials = await this.refreshAccessToken(this.credentials)
}
// After authentication, update the apiKey and baseURL on the existing client
const client = this.ensureClient()
client.apiKey = this.credentials.access_token
client.baseURL = this.getBaseUrl(this.credentials)
}
private getBaseUrl(creds: QwenOAuthCredentials): string {
let baseUrl = creds.resource_url || "https://dashscope.aliyuncs.com/compatible-mode/v1"
if (!baseUrl.startsWith("http://") && !baseUrl.startsWith("https://")) {
baseUrl = `https://${baseUrl}`
}
return baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`
}
private async callApiWithRetry<T>(apiCall: () => Promise<T>): Promise<T> {
try {
return await apiCall()
} catch (error: any) {
if (error.status === 401) {
// Token expired, refresh and retry
this.credentials = await this.refreshAccessToken(this.credentials!)
const client = this.ensureClient()
client.apiKey = this.credentials.access_token
client.baseURL = this.getBaseUrl(this.credentials)
return await apiCall()
} else {
throw error
}
}
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
await this.ensureAuthenticated()
const client = this.ensureClient()
const model = this.getModel()
const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
role: "system",
content: systemPrompt,
}
const convertedMessages = [systemMessage, ...convertToOpenAiMessages(messages)]
const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
model: model.id,
temperature: 0,
messages: convertedMessages,
stream: true,
stream_options: { include_usage: true },
max_completion_tokens: model.info.maxTokens,
}
const stream = await this.callApiWithRetry(() => client.chat.completions.create(requestOptions))
let fullContent = ""
for await (const apiChunk of stream) {
const delta = apiChunk.choices[0]?.delta ?? {}
if (delta.content) {
let newText = delta.content
if (newText.startsWith(fullContent)) {
newText = newText.substring(fullContent.length)
}
fullContent = delta.content
if (newText) {
// Check for thinking blocks
if (newText.includes("<think>") || newText.includes("</think>")) {
// Simple parsing for thinking blocks
const parts = newText.split(/<\/?think>/g)
for (let i = 0; i < parts.length; i++) {
if (parts[i]) {
if (i % 2 === 0) {
// Outside thinking block
yield {
type: "text",
text: parts[i],
}
} else {
// Inside thinking block
yield {
type: "reasoning",
reasoning: parts[i],
}
}
}
}
} else {
yield {
type: "text",
text: newText,
}
}
}
}
// Handle reasoning content (o1-style)
if ("reasoning_content" in delta && delta.reasoning_content) {
yield {
type: "reasoning",
reasoning: (delta.reasoning_content as string | undefined) || "",
}
}
if (apiChunk.usage) {
yield {
type: "usage",
inputTokens: apiChunk.usage.prompt_tokens || 0,
outputTokens: apiChunk.usage.completion_tokens || 0,
}
}
}
}
getModel(): { id: QwenCodeModelId; info: ModelInfo } {
const modelId = this.options.apiModelId
if (modelId && modelId in qwenCodeModels) {
const id = modelId as QwenCodeModelId
return { id, info: qwenCodeModels[id] }
}
return {
id: qwenCodeDefaultModelId,
info: qwenCodeModels[qwenCodeDefaultModelId],
}
}
}
+2 -2
View File
@@ -10,13 +10,13 @@ import {
QwenApiRegions,
} from "@shared/api"
import OpenAI from "openai"
import { ApiHandler } from "../"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { convertToR1Format } from "../transform/r1-format"
import { ApiStream } from "../transform/stream"
interface QwenHandlerOptions {
interface QwenHandlerOptions extends CommonApiHandlerOptions {
qwenApiKey?: string
qwenApiLine?: QwenApiRegions
apiModelId?: string
+2 -2
View File
@@ -2,12 +2,12 @@ import { Anthropic } from "@anthropic-ai/sdk"
import { ModelInfo, requestyDefaultModelId, requestyDefaultModelInfo } from "@shared/api"
import { calculateApiCostOpenAI } from "@utils/cost"
import OpenAI from "openai"
import { ApiHandler } from "../index"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
interface RequestyHandlerOptions {
interface RequestyHandlerOptions extends CommonApiHandlerOptions {
requestyBaseUrl?: string
requestyApiKey?: string
reasoningEffort?: string
+2 -2
View File
@@ -1,13 +1,13 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ModelInfo, SambanovaModelId, sambanovaDefaultModelId, sambanovaModels } from "@shared/api"
import OpenAI from "openai"
import { ApiHandler } from "../index"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { convertToR1Format } from "../transform/r1-format"
import { ApiStream } from "../transform/stream"
interface SambanovaHandlerOptions {
interface SambanovaHandlerOptions extends CommonApiHandlerOptions {
sambanovaApiKey?: string
apiModelId?: string
}
+26 -9
View File
@@ -7,11 +7,11 @@ import {
import { ModelInfo, SapAiCoreModelId, sapAiCoreDefaultModelId, sapAiCoreModels } from "@shared/api"
import axios from "axios"
import OpenAI from "openai"
import { ApiHandler } from "../"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
interface SapAiCoreHandlerOptions {
interface SapAiCoreHandlerOptions extends CommonApiHandlerOptions {
sapAiCoreClientId?: string
sapAiCoreClientSecret?: string
sapAiCoreTokenUrl?: string
@@ -19,6 +19,7 @@ interface SapAiCoreHandlerOptions {
sapAiCoreBaseUrl?: string
apiModelId?: string
thinkingBudgetTokens?: number
reasoningEffort?: string
}
interface Deployment {
@@ -154,7 +155,6 @@ namespace Bedrock {
* Processes image content with proper error handling and user notification
*/
function processImageContent(item: any): BedrockContentBlock | null {
let imageData: Uint8Array
let format: "png" | "jpeg" | "gif" | "webp" = "jpeg" // default format
// Extract format from media_type if available
@@ -172,22 +172,34 @@ namespace Bedrock {
// Get image data with improved error handling
try {
let imageData: string
if (typeof item.source.data === "string") {
// Handle base64 encoded data
const base64Data = item.source.data.replace(/^data:image\/\w+;base64,/, "")
imageData = new Uint8Array(Buffer.from(base64Data, "base64"))
// Keep as base64 string, just clean the data URI prefix if present
imageData = item.source.data.replace(/^data:image\/\w+;base64,/, "")
} else if (item.source.data && typeof item.source.data === "object") {
// Try to convert to Uint8Array
imageData = new Uint8Array(Buffer.from(item.source.data as Buffer | Uint8Array))
// Convert Buffer/Uint8Array to base64 string
if (Buffer.isBuffer(item.source.data)) {
imageData = item.source.data.toString("base64")
} else {
// Assume Uint8Array
const buffer = Buffer.from(item.source.data as Uint8Array)
imageData = buffer.toString("base64")
}
} else {
throw new Error("Unsupported image data format")
}
// Validate base64 data
if (!imageData || imageData.length === 0) {
throw new Error("Empty or invalid image data")
}
return {
image: {
format,
source: {
bytes: imageData,
bytes: imageData as any, // Keep as base64 string for Bedrock Converse API compatibility
},
},
}
@@ -540,6 +552,11 @@ export class SapAiCoreHandler implements ApiHandler {
if (["o1", "o3-mini", "o3", "o4-mini", "gpt-5", "gpt-5-nano", "gpt-5-mini"].includes(model.id)) {
delete payload.max_tokens
delete payload.temperature
// Add reasoning effort for reasoning models
if (this.options.reasoningEffort) {
payload.reasoning_effort = this.options.reasoningEffort
}
}
if (model.id === "o3-mini") {
+2 -2
View File
@@ -1,13 +1,13 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
import OpenAI from "openai"
import { ApiHandler } from "../index"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { convertToR1Format } from "../transform/r1-format"
import { ApiStream } from "../transform/stream"
interface TogetherHandlerOptions {
interface TogetherHandlerOptions extends CommonApiHandlerOptions {
togetherApiKey?: string
togetherModelId?: string
}
+2 -2
View File
@@ -1,12 +1,12 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { AnthropicVertex } from "@anthropic-ai/vertex-sdk"
import { ModelInfo, VertexModelId, vertexDefaultModelId, vertexModels } from "@shared/api"
import { ApiHandler } from "../"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { ApiStream } from "../transform/stream"
import { GeminiHandler } from "./gemini"
interface VertexHandlerOptions {
interface VertexHandlerOptions extends CommonApiHandlerOptions {
vertexProjectId?: string
vertexRegion?: string
apiModelId?: string
+2 -2
View File
@@ -3,13 +3,13 @@ import { ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
import { SELECTOR_SEPARATOR, stringifyVsCodeLmModelSelector } from "@shared/vsCodeSelectorUtils"
import { calculateApiCostAnthropic } from "@utils/cost"
import * as vscode from "vscode"
import { ApiHandler, SingleCompletionHandler } from "../"
import { ApiHandler, CommonApiHandlerOptions, SingleCompletionHandler } from "../"
import { withRetry } from "../retry"
import { ApiStream } from "../transform/stream"
import { convertToVsCodeLmMessages } from "../transform/vscode-lm-format"
import type { LanguageModelChatSelector as LanguageModelChatSelectorFromTypes } from "./types"
interface VsCodeLmHandlerOptions {
interface VsCodeLmHandlerOptions extends CommonApiHandlerOptions {
vsCodeLmModelSelector?: any
}
+2 -8
View File
@@ -3,18 +3,12 @@ import { ModelInfo, XAIModelId, xaiDefaultModelId, xaiModels } from "@shared/api
import { shouldSkipReasoningForModel } from "@utils/model-utils"
import OpenAI from "openai"
import { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
import { ApiHandler } from "../"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
interface XAIHandlerOptions {
xaiApiKey?: string
reasoningEffort?: string
apiModelId?: string
}
interface XAIHandlerOptions {
interface XAIHandlerOptions extends CommonApiHandlerOptions {
xaiApiKey?: string
reasoningEffort?: string
apiModelId?: string
+1 -8
View File
@@ -1,6 +1,7 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { CLAUDE_SONNET_4_1M_SUFFIX, ModelInfo, openRouterClaudeSonnet41mModelId } from "@shared/api"
import OpenAI from "openai"
import { isGPT5ModelFamily } from "../../prompts/system-prompt/utils"
import { convertToOpenAiMessages } from "./openai-format"
import { convertToR1Format } from "./r1-format"
@@ -151,13 +152,6 @@ export async function createOpenRouterStream(
}
}
// 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)
if (model.id === "deepseek/deepseek-chat") {
shouldApplyMiddleOutTransform = true
}
// hardcoded provider sorting for kimi-k2
const isKimiK2 = model.id === "moonshotai/kimi-k2"
openRouterProviderSorting = isKimiK2 ? undefined : openRouterProviderSorting
@@ -171,7 +165,6 @@ export async function createOpenRouterStream(
messages: openAiMessages,
stream: true,
stream_options: { include_usage: true },
transforms: shouldApplyMiddleOutTransform ? ["middle-out"] : undefined,
include_reasoning: true,
...(model.id.startsWith("openai/o") ? { reasoning_effort: reasoningEffort || "medium" } : {}),
...(reasoning ? { reasoning } : {}),
@@ -13,7 +13,7 @@ export function getContextWindowInfo(api: ApiHandler) {
// Handle special cases like DeepSeek
if (api instanceof OpenAiHandler && api.getModel().id.toLowerCase().includes("deepseek")) {
contextWindow = 64_000
contextWindow = 128_000
}
let maxAllowedSize: number
@@ -243,7 +243,7 @@ export class FileContextTracker {
const key = `pendingFileContextWarning_${this.taskId}`
// NOTE: Using 'as any' because dynamic keys like pendingFileContextWarning_${taskId}
// are legitimate workspace state keys but don't fit the strict LocalStateKey type system
this.controller.cacheService.setWorkspaceState(key as any, files)
this.controller.stateManager.setWorkspaceState(key as any, files)
} catch (error) {
console.error("Error storing pending file context warning:", error)
}
@@ -255,7 +255,7 @@ export class FileContextTracker {
async retrievePendingFileContextWarning(): Promise<string[] | undefined> {
try {
const key = `pendingFileContextWarning_${this.taskId}`
const files = this.controller.cacheService.getWorkspaceStateKey(key as any) as string[]
const files = this.controller.stateManager.getWorkspaceStateKey(key as any) as string[]
return files
} catch (error) {
console.error("Error retrieving pending file context warning:", error)
@@ -270,7 +270,7 @@ export class FileContextTracker {
try {
const files = await this.retrievePendingFileContextWarning()
if (files) {
this.controller.cacheService.setWorkspaceState(`pendingFileContextWarning_${this.taskId}` as any, undefined)
this.controller.stateManager.setWorkspaceState(`pendingFileContextWarning_${this.taskId}` as any, undefined)
return files
}
} catch (error) {
@@ -74,18 +74,18 @@ export async function refreshClineRulesToggles(
localToggles: ClineRulesToggles
}> {
// Global toggles
const globalClineRulesToggles = controller.cacheService.getGlobalStateKey("globalClineRulesToggles")
const globalClineRulesToggles = controller.stateManager.getGlobalStateKey("globalClineRulesToggles")
const globalClineRulesFilePath = await ensureRulesDirectoryExists()
const updatedGlobalToggles = await synchronizeRuleToggles(globalClineRulesFilePath, globalClineRulesToggles)
controller.cacheService.setGlobalState("globalClineRulesToggles", updatedGlobalToggles)
controller.stateManager.setGlobalState("globalClineRulesToggles", updatedGlobalToggles)
// Local toggles
const localClineRulesToggles = controller.cacheService.getWorkspaceStateKey("localClineRulesToggles")
const localClineRulesToggles = controller.stateManager.getWorkspaceStateKey("localClineRulesToggles")
const localClineRulesFilePath = path.resolve(workingDirectory, GlobalFileNames.clineRules)
const updatedLocalToggles = await synchronizeRuleToggles(localClineRulesFilePath, localClineRulesToggles, "", [
[".clinerules", "workflows"],
])
controller.cacheService.setWorkspaceState("localClineRulesToggles", updatedLocalToggles)
controller.stateManager.setWorkspaceState("localClineRulesToggles", updatedLocalToggles)
return {
globalToggles: updatedGlobalToggles,
@@ -23,13 +23,13 @@ export async function refreshExternalRulesToggles(
cursorLocalToggles: ClineRulesToggles
}> {
// local windsurf toggles
const localWindsurfRulesToggles = controller.cacheService.getWorkspaceStateKey("localWindsurfRulesToggles")
const localWindsurfRulesToggles = controller.stateManager.getWorkspaceStateKey("localWindsurfRulesToggles")
const localWindsurfRulesFilePath = path.resolve(workingDirectory, GlobalFileNames.windsurfRules)
const updatedLocalWindsurfToggles = await synchronizeRuleToggles(localWindsurfRulesFilePath, localWindsurfRulesToggles)
controller.cacheService.setWorkspaceState("localWindsurfRulesToggles", updatedLocalWindsurfToggles)
controller.stateManager.setWorkspaceState("localWindsurfRulesToggles", updatedLocalWindsurfToggles)
// local cursor toggles
const localCursorRulesToggles = controller.cacheService.getWorkspaceStateKey("localCursorRulesToggles")
const localCursorRulesToggles = controller.stateManager.getWorkspaceStateKey("localCursorRulesToggles")
// cursor has two valid locations for rules files, so we need to check both and combine
// synchronizeRuleToggles will drop whichever rules files are not in each given path, but combining the results will result in no data loss
@@ -40,7 +40,7 @@ export async function refreshExternalRulesToggles(
const updatedLocalCursorToggles2 = await synchronizeRuleToggles(localCursorRulesFilePath, localCursorRulesToggles)
const updatedLocalCursorToggles = combineRuleToggles(updatedLocalCursorToggles1, updatedLocalCursorToggles2)
controller.cacheService.setWorkspaceState("localCursorRulesToggles", updatedLocalCursorToggles)
controller.stateManager.setWorkspaceState("localCursorRulesToggles", updatedLocalCursorToggles)
return {
windsurfLocalToggles: updatedLocalWindsurfToggles,
@@ -247,31 +247,31 @@ export async function deleteRuleFile(
// Update the appropriate toggles
if (isGlobal) {
if (type === "workflow") {
const toggles = controller.cacheService.getGlobalStateKey("globalWorkflowToggles")
const toggles = controller.stateManager.getGlobalStateKey("globalWorkflowToggles")
delete toggles[rulePath]
controller.cacheService.setGlobalState("globalWorkflowToggles", toggles)
controller.stateManager.setGlobalState("globalWorkflowToggles", toggles)
} else {
const toggles = controller.cacheService.getGlobalStateKey("globalClineRulesToggles")
const toggles = controller.stateManager.getGlobalStateKey("globalClineRulesToggles")
delete toggles[rulePath]
controller.cacheService.setGlobalState("globalClineRulesToggles", toggles)
controller.stateManager.setGlobalState("globalClineRulesToggles", toggles)
}
} else {
if (type === "workflow") {
const toggles = controller.cacheService.getWorkspaceStateKey("workflowToggles")
const toggles = controller.stateManager.getWorkspaceStateKey("workflowToggles")
delete toggles[rulePath]
controller.cacheService.setWorkspaceState("workflowToggles", toggles)
controller.stateManager.setWorkspaceState("workflowToggles", toggles)
} else if (type === "cursor") {
const toggles = controller.cacheService.getWorkspaceStateKey("localCursorRulesToggles")
const toggles = controller.stateManager.getWorkspaceStateKey("localCursorRulesToggles")
delete toggles[rulePath]
controller.cacheService.setWorkspaceState("localCursorRulesToggles", toggles)
controller.stateManager.setWorkspaceState("localCursorRulesToggles", toggles)
} else if (type === "windsurf") {
const toggles = controller.cacheService.getWorkspaceStateKey("localWindsurfRulesToggles")
const toggles = controller.stateManager.getWorkspaceStateKey("localWindsurfRulesToggles")
delete toggles[rulePath]
controller.cacheService.setWorkspaceState("localWindsurfRulesToggles", toggles)
controller.stateManager.setWorkspaceState("localWindsurfRulesToggles", toggles)
} else {
const toggles = controller.cacheService.getWorkspaceStateKey("localClineRulesToggles")
const toggles = controller.stateManager.getWorkspaceStateKey("localClineRulesToggles")
delete toggles[rulePath]
controller.cacheService.setWorkspaceState("localClineRulesToggles", toggles)
controller.stateManager.setWorkspaceState("localClineRulesToggles", toggles)
}
}
@@ -15,15 +15,15 @@ export async function refreshWorkflowToggles(
localWorkflowToggles: ClineRulesToggles
}> {
// Global workflows
const globalWorkflowToggles = controller.cacheService.getGlobalStateKey("globalWorkflowToggles")
const globalWorkflowToggles = controller.stateManager.getGlobalStateKey("globalWorkflowToggles")
const globalClineWorkflowsFilePath = await ensureWorkflowsDirectoryExists()
const updatedGlobalWorkflowToggles = await synchronizeRuleToggles(globalClineWorkflowsFilePath, globalWorkflowToggles)
controller.cacheService.setGlobalState("globalWorkflowToggles", updatedGlobalWorkflowToggles)
controller.stateManager.setGlobalState("globalWorkflowToggles", updatedGlobalWorkflowToggles)
const workflowRulesToggles = controller.cacheService.getWorkspaceStateKey("workflowToggles")
const workflowRulesToggles = controller.stateManager.getWorkspaceStateKey("workflowToggles")
const workflowsDirPath = path.resolve(workingDirectory, GlobalFileNames.workflows)
const updatedWorkflowToggles = await synchronizeRuleToggles(workflowsDirPath, workflowRulesToggles)
controller.cacheService.setWorkspaceState("workflowToggles", updatedWorkflowToggles)
controller.stateManager.setWorkspaceState("workflowToggles", updatedWorkflowToggles)
return {
globalWorkflowToggles: updatedGlobalWorkflowToggles,
@@ -11,7 +11,7 @@ import type { Controller } from "../index"
export async function authStateChanged(controller: Controller, request: AuthStateChangedRequest): Promise<AuthState> {
try {
// Store the user info directly in global state
controller.cacheService.setGlobalState("userInfo", request.user)
controller.stateManager.setGlobalState("userInfo", request.user)
// Return the same user info
return AuthState.create({ user: request.user })
@@ -19,7 +19,7 @@ export async function discoverBrowser(controller: Controller, _request: EmptyReq
// This way we don't override the user's preference
// Test the connection to get the endpoint
const browserSettings = controller.cacheService.getGlobalStateKey("browserSettings")
const browserSettings = controller.stateManager.getGlobalStateKey("browserSettings")
const browserSession = new BrowserSession(controller.context, browserSettings)
const result = await browserSession.testConnection(discoveredHost)
@@ -11,7 +11,7 @@ import { Controller } from "../index"
export async function getBrowserConnectionInfo(controller: Controller, _: EmptyRequest): Promise<BrowserConnectionInfo> {
try {
// Get browser settings from extension state
const browserSettings = controller.cacheService.getGlobalStateKey("browserSettings")
const browserSettings = controller.stateManager.getGlobalStateKey("browserSettings")
// Check if there's an active browser session by using the controller's handleWebviewMessage approach
// This is similar to what's done in controller/index.ts for the "getBrowserConnectionInfo" message
@@ -11,7 +11,7 @@ import { Controller } from "../index"
*/
export async function getDetectedChromePath(controller: Controller, _: EmptyRequest): Promise<ChromePath> {
try {
const browserSettings = controller.cacheService.getGlobalStateKey("browserSettings")
const browserSettings = controller.stateManager.getGlobalStateKey("browserSettings")
const browserSession = new BrowserSession(controller.context, browserSettings)
const result = await browserSession.getDetectedChromePath()
@@ -12,7 +12,7 @@ import { Controller } from "../index"
*/
export async function testBrowserConnection(controller: Controller, request: StringRequest): Promise<BrowserConnection> {
try {
const browserSettings = controller.cacheService.getGlobalStateKey("browserSettings")
const browserSettings = controller.stateManager.getGlobalStateKey("browserSettings")
const browserSession = new BrowserSession(controller.context, browserSettings)
const text = request.value || ""
@@ -12,7 +12,7 @@ import { Controller } from "../index"
export async function updateBrowserSettings(controller: Controller, request: UpdateBrowserSettingsRequest): Promise<Boolean> {
try {
// Get current browser settings to preserve fields not in the request
const currentSettings = controller.cacheService.getGlobalStateKey("browserSettings")
const currentSettings = controller.stateManager.getGlobalStateKey("browserSettings")
const mergedWithDefaults = { ...DEFAULT_BROWSER_SETTINGS, ...currentSettings }
// Convert from protobuf format to shared format, merging with existing settings
@@ -39,7 +39,7 @@ export async function updateBrowserSettings(controller: Controller, request: Upd
}
// Update global state with new settings
controller.cacheService.setGlobalState("browserSettings", newBrowserSettings)
controller.stateManager.setGlobalState("browserSettings", newBrowserSettings)
// Update task browser settings if task exists
if (controller.task) {
+4 -4
View File
@@ -1,9 +1,9 @@
import { Controller } from ".."
import { FileSearchRequest, FileSearchResults, FileSearchType } from "@shared/proto/cline/file"
import { getWorkspacePath } from "@utils/path"
import { convertSearchResultsToProtoFileInfos } from "@shared/proto-conversions/file/search-result-conversion"
import { HostProvider } from "@/hosts/host-provider"
import { SearchWorkspaceItemsRequest_SearchItemType } from "@shared/proto/host/workspace"
import { convertSearchResultsToProtoFileInfos } from "@shared/proto-conversions/file/search-result-conversion"
import { getWorkspacePath } from "@utils/path"
import { HostProvider } from "@/hosts/host-provider"
import { Controller } from ".."
/**
* Searches for files in the workspace with fuzzy matching
+13 -6
View File
@@ -24,13 +24,20 @@ export async function toggleClineRule(controller: Controller, request: ToggleCli
// This is the same core logic as in the original handler
if (isGlobal) {
const toggles = controller.cacheService.getGlobalStateKey("globalClineRulesToggles")
const toggles = controller.stateManager.getGlobalStateKey("globalClineRulesToggles")
toggles[rulePath] = enabled
controller.cacheService.setGlobalState("globalClineRulesToggles", toggles)
controller.stateManager.setGlobalState("globalClineRulesToggles", toggles)
} else {
const toggles = controller.cacheService.getWorkspaceStateKey("localClineRulesToggles")
const toggles = controller.stateManager.getWorkspaceStateKey("localClineRulesToggles")
toggles[rulePath] = enabled
controller.cacheService.setWorkspaceState("localClineRulesToggles", toggles)
controller.stateManager.setWorkspaceState("localClineRulesToggles", toggles)
}
// Track rule toggle telemetry with current task context
if (controller.task?.ulid) {
// Extract just the filename for privacy (no full paths)
const ruleFileName = path.basename(rulePath)
telemetryService.captureClineRuleToggled(controller.task.ulid, ruleFileName, enabled, isGlobal)
}
// Track rule toggle telemetry with current task context
@@ -41,8 +48,8 @@ export async function toggleClineRule(controller: Controller, request: ToggleCli
}
// Get the current state to return in the response
const globalToggles = controller.cacheService.getGlobalStateKey("globalClineRulesToggles")
const localToggles = controller.cacheService.getWorkspaceStateKey("localClineRulesToggles")
const globalToggles = controller.stateManager.getGlobalStateKey("globalClineRulesToggles")
const localToggles = controller.stateManager.getWorkspaceStateKey("localClineRulesToggles")
return ToggleClineRules.create({
globalClineRulesToggles: { toggles: globalToggles },
+3 -3
View File
@@ -20,12 +20,12 @@ export async function toggleCursorRule(controller: Controller, request: ToggleCu
}
// Update the toggles in workspace state
const toggles = controller.cacheService.getWorkspaceStateKey("localCursorRulesToggles")
const toggles = controller.stateManager.getWorkspaceStateKey("localCursorRulesToggles")
toggles[rulePath] = enabled
controller.cacheService.setWorkspaceState("localCursorRulesToggles", toggles)
controller.stateManager.setWorkspaceState("localCursorRulesToggles", toggles)
// Get the current state to return in the response
const cursorToggles = controller.cacheService.getWorkspaceStateKey("localCursorRulesToggles")
const cursorToggles = controller.stateManager.getWorkspaceStateKey("localCursorRulesToggles")
return ClineRulesToggles.create({
toggles: cursorToggles,
@@ -1,3 +1,4 @@
import { ClineRulesToggles as AppClineRulesToggles } from "@shared/cline-rules"
import type { ToggleWindsurfRuleRequest } from "@shared/proto/cline/file"
import { ClineRulesToggles } from "@shared/proto/cline/file"
import type { Controller } from "../index"
@@ -20,9 +21,9 @@ export async function toggleWindsurfRule(controller: Controller, request: Toggle
}
// Update the toggles
const toggles = controller.cacheService.getWorkspaceStateKey("localWindsurfRulesToggles")
const toggles = controller.stateManager.getWorkspaceStateKey("localWindsurfRulesToggles")
toggles[rulePath] = enabled
controller.cacheService.setWorkspaceState("localWindsurfRulesToggles", toggles)
controller.stateManager.setWorkspaceState("localWindsurfRulesToggles", toggles)
// Return the toggles directly
return ClineRulesToggles.create({ toggles: toggles })
+4 -4
View File
@@ -21,18 +21,18 @@ export async function toggleWorkflow(controller: Controller, request: ToggleWork
// Update the toggles based on isGlobal flag
if (isGlobal) {
// Global workflows
const toggles = controller.cacheService.getGlobalStateKey("globalWorkflowToggles")
const toggles = controller.stateManager.getGlobalStateKey("globalWorkflowToggles")
toggles[workflowPath] = enabled
controller.cacheService.setGlobalState("globalWorkflowToggles", toggles)
controller.stateManager.setGlobalState("globalWorkflowToggles", toggles)
await controller.postStateToWebview()
// Return the global toggles
return ClineRulesToggles.create({ toggles: toggles })
} else {
// Workspace workflows
const toggles = controller.cacheService.getWorkspaceStateKey("workflowToggles")
const toggles = controller.stateManager.getWorkspaceStateKey("workflowToggles")
toggles[workflowPath] = enabled
controller.cacheService.setWorkspaceState("workflowToggles", toggles)
controller.stateManager.setWorkspaceState("workflowToggles", toggles)
await controller.postStateToWebview()
// Return the workspace toggles
+82 -80
View File
@@ -25,8 +25,8 @@ import { PostHogClientProvider, telemetryService } from "@/services/posthog/Post
import { ShowMessageType } from "@/shared/proto/host/window"
import { getLatestAnnouncementId } from "@/utils/announcements"
import { getCwd, getDesktopDir } from "@/utils/path"
import { CacheService, PersistenceErrorEvent } from "../storage/CacheService"
import { ensureMcpServersDirectoryExists, ensureSettingsDirectoryExists, GlobalFileNames } from "../storage/disk"
import { PersistenceErrorEvent, StateManager } from "../storage/StateManager"
import { Task } from "../task"
import { sendMcpMarketplaceCatalogEvent } from "./mcp/subscribeToMcpMarketplaceCatalog"
import { sendStateUpdate } from "./state/subscribeToState"
@@ -45,7 +45,7 @@ export class Controller {
mcpHub: McpHub
accountService: ClineAccountService
authService: AuthService
readonly cacheService: CacheService
readonly stateManager: StateManager
constructor(
readonly context: vscode.ExtensionContext,
@@ -55,24 +55,24 @@ export class Controller {
HostProvider.get().logToChannel("ClineProvider instantiated")
this.accountService = ClineAccountService.getInstance()
this.cacheService = new CacheService(context)
this.stateManager = new StateManager(context)
this.authService = AuthService.getInstance(this)
// Initialize cache service asynchronously - critical for extension functionality
this.cacheService
this.stateManager
.initialize()
.then(() => {
this.authService.restoreRefreshTokenAndRetrieveAuthInfo()
})
.catch((error) => {
console.error("CRITICAL: Failed to initialize CacheService - extension may not function properly:", error)
console.error("CRITICAL: Failed to initialize StateManager - extension may not function properly:", error)
})
// Set up persistence error recovery
this.cacheService.onPersistenceError = async ({ error }: PersistenceErrorEvent) => {
this.stateManager.onPersistenceError = async ({ error }: PersistenceErrorEvent) => {
console.error("Cache persistence failed, recovering:", error)
try {
await this.cacheService.reInitialize()
await this.stateManager.reInitialize()
await this.postStateToWebview()
HostProvider.window.showMessage({
type: ShowMessageType.WARNING,
@@ -101,7 +101,7 @@ export class Controller {
}
async getCurrentMode(): Promise<Mode> {
return this.cacheService.getGlobalStateKey("mode")
return this.stateManager.getGlobalStateKey("mode")
}
/*
@@ -126,17 +126,17 @@ export class Controller {
async handleSignOut() {
try {
// TODO: update to clineAccountId and then move clineApiKey to a clear function.
this.cacheService.setSecret("clineAccountId", undefined)
this.cacheService.setGlobalState("userInfo", undefined)
this.stateManager.setSecret("clineAccountId", undefined)
this.stateManager.setGlobalState("userInfo", undefined)
// Update API providers through cache service
const apiConfiguration = this.cacheService.getApiConfiguration()
const apiConfiguration = this.stateManager.getApiConfiguration()
const updatedConfig = {
...apiConfiguration,
planModeApiProvider: "openrouter" as ApiProvider,
actModeApiProvider: "openrouter" as ApiProvider,
}
this.cacheService.setApiConfiguration(updatedConfig)
this.stateManager.setApiConfiguration(updatedConfig)
await this.postStateToWebview()
HostProvider.window.showMessage({
@@ -152,35 +152,35 @@ export class Controller {
}
async setUserInfo(info?: UserInfo) {
this.cacheService.setGlobalState("userInfo", info)
this.stateManager.setGlobalState("userInfo", info)
}
async initTask(task?: string, images?: string[], files?: string[], historyItem?: HistoryItem) {
await this.clearTask() // ensures that an existing task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one
const apiConfiguration = this.cacheService.getApiConfiguration()
const autoApprovalSettings = this.cacheService.getGlobalStateKey("autoApprovalSettings")
const browserSettings = this.cacheService.getGlobalStateKey("browserSettings")
const focusChainSettings = this.cacheService.getGlobalStateKey("focusChainSettings")
const focusChainFeatureFlagEnabled = this.cacheService.getGlobalStateKey("focusChainFeatureFlagEnabled")
const preferredLanguage = this.cacheService.getGlobalStateKey("preferredLanguage")
const openaiReasoningEffort = this.cacheService.getGlobalStateKey("openaiReasoningEffort")
const mode = this.cacheService.getGlobalStateKey("mode")
const shellIntegrationTimeout = this.cacheService.getGlobalStateKey("shellIntegrationTimeout")
const terminalReuseEnabled = this.cacheService.getGlobalStateKey("terminalReuseEnabled")
const terminalOutputLineLimit = this.cacheService.getGlobalStateKey("terminalOutputLineLimit")
const defaultTerminalProfile = this.cacheService.getGlobalStateKey("defaultTerminalProfile")
const enableCheckpointsSetting = this.cacheService.getGlobalStateKey("enableCheckpointsSetting")
const isNewUser = this.cacheService.getGlobalStateKey("isNewUser")
const taskHistory = this.cacheService.getGlobalStateKey("taskHistory")
const strictPlanModeEnabled = this.cacheService.getGlobalStateKey("strictPlanModeEnabled")
const useAutoCondense = this.cacheService.getGlobalStateKey("useAutoCondense")
const apiConfiguration = this.stateManager.getApiConfiguration()
const autoApprovalSettings = this.stateManager.getGlobalStateKey("autoApprovalSettings")
const browserSettings = this.stateManager.getGlobalStateKey("browserSettings")
const focusChainSettings = this.stateManager.getGlobalStateKey("focusChainSettings")
const focusChainFeatureFlagEnabled = this.stateManager.getGlobalStateKey("focusChainFeatureFlagEnabled")
const preferredLanguage = this.stateManager.getGlobalStateKey("preferredLanguage")
const openaiReasoningEffort = this.stateManager.getGlobalStateKey("openaiReasoningEffort")
const mode = this.stateManager.getGlobalStateKey("mode")
const shellIntegrationTimeout = this.stateManager.getGlobalStateKey("shellIntegrationTimeout")
const terminalReuseEnabled = this.stateManager.getGlobalStateKey("terminalReuseEnabled")
const terminalOutputLineLimit = this.stateManager.getGlobalStateKey("terminalOutputLineLimit")
const defaultTerminalProfile = this.stateManager.getGlobalStateKey("defaultTerminalProfile")
const enableCheckpointsSetting = this.stateManager.getGlobalStateKey("enableCheckpointsSetting")
const isNewUser = this.stateManager.getGlobalStateKey("isNewUser")
const taskHistory = this.stateManager.getGlobalStateKey("taskHistory")
const strictPlanModeEnabled = this.stateManager.getGlobalStateKey("strictPlanModeEnabled")
const useAutoCondense = this.stateManager.getGlobalStateKey("useAutoCondense")
const NEW_USER_TASK_COUNT_THRESHOLD = 10
// Check if the user has completed enough tasks to no longer be considered a "new user"
if (isNewUser && !historyItem && taskHistory && taskHistory.length >= NEW_USER_TASK_COUNT_THRESHOLD) {
this.cacheService.setGlobalState("isNewUser", false)
this.stateManager.setGlobalState("isNewUser", false)
await this.postStateToWebview()
}
@@ -189,7 +189,7 @@ export class Controller {
...autoApprovalSettings,
version: (autoApprovalSettings.version ?? 1) + 1,
}
this.cacheService.setGlobalState("autoApprovalSettings", updatedAutoApprovalSettings)
this.stateManager.setGlobalState("autoApprovalSettings", updatedAutoApprovalSettings)
}
// Apply remote feature flag gate to focus chain settings
const effectiveFocusChainSettings = {
@@ -219,7 +219,7 @@ export class Controller {
defaultTerminalProfile ?? "default",
enableCheckpointsSetting ?? true,
await getCwd(getDesktopDir()),
this.cacheService,
this.stateManager,
task,
images,
files,
@@ -235,7 +235,7 @@ export class Controller {
}
async updateTelemetrySetting(telemetrySetting: TelemetrySetting) {
this.cacheService.setGlobalState("telemetrySetting", telemetrySetting)
this.stateManager.setGlobalState("telemetrySetting", telemetrySetting)
const isOptedIn = telemetrySetting !== "disabled"
telemetryService.updateTelemetryState(isOptedIn)
await this.postStateToWebview()
@@ -245,14 +245,14 @@ export class Controller {
const didSwitchToActMode = modeToSwitchTo === "act"
// Store mode to global state
this.cacheService.setGlobalState("mode", modeToSwitchTo)
this.stateManager.setGlobalState("mode", modeToSwitchTo)
// Capture mode switch telemetry | Capture regardless of if we know the taskId
telemetryService.captureModeSwitch(this.task?.ulid ?? "0", modeToSwitchTo)
// Update API handler with new mode (buildApiHandler now selects provider based on mode)
if (this.task) {
const apiConfiguration = this.cacheService.getApiConfiguration()
const apiConfiguration = this.stateManager.getApiConfiguration()
this.task.api = buildApiHandler({ ...apiConfiguration, ulid: this.task.ulid }, modeToSwitchTo)
}
@@ -317,12 +317,12 @@ export class Controller {
const clineProvider: ApiProvider = "cline"
// Get current settings to determine how to update providers
const planActSeparateModelsSetting = this.cacheService.getGlobalStateKey("planActSeparateModelsSetting")
const planActSeparateModelsSetting = this.stateManager.getGlobalStateKey("planActSeparateModelsSetting")
const currentMode = await this.getCurrentMode()
// Get current API configuration from cache
const currentApiConfiguration = this.cacheService.getApiConfiguration()
const currentApiConfiguration = this.stateManager.getApiConfiguration()
const updatedConfig = { ...currentApiConfiguration }
@@ -340,10 +340,10 @@ export class Controller {
}
// Update the API configuration through cache service
this.cacheService.setApiConfiguration(updatedConfig)
this.stateManager.setApiConfiguration(updatedConfig)
// Mark welcome view as completed since user has successfully logged in
this.cacheService.setGlobalState("welcomeViewCompleted", true)
this.stateManager.setGlobalState("welcomeViewCompleted", true)
if (this.task) {
this.task.api = buildApiHandler({ ...updatedConfig, ulid: this.task.ulid }, currentMode)
@@ -384,7 +384,7 @@ export class Controller {
}
// Store in global state
this.cacheService.setGlobalState("mcpMarketplaceCatalog", catalog)
this.stateManager.setGlobalState("mcpMarketplaceCatalog", catalog)
return catalog
} catch (error) {
console.error("Failed to fetch MCP marketplace:", error)
@@ -422,7 +422,7 @@ export class Controller {
}
// Store in global state
this.cacheService.setGlobalState("mcpMarketplaceCatalog", catalog)
this.stateManager.setGlobalState("mcpMarketplaceCatalog", catalog)
return catalog
} catch (error) {
console.error("Failed to fetch MCP marketplace:", error)
@@ -479,14 +479,14 @@ export class Controller {
const currentMode = await this.getCurrentMode()
// Update API configuration through cache service
const currentApiConfiguration = this.cacheService.getApiConfiguration()
const currentApiConfiguration = this.stateManager.getApiConfiguration()
const updatedConfig = {
...currentApiConfiguration,
planModeApiProvider: openrouter,
actModeApiProvider: openrouter,
openRouterApiKey: apiKey,
}
this.cacheService.setApiConfiguration(updatedConfig)
this.stateManager.setApiConfiguration(updatedConfig)
await this.postStateToWebview()
if (this.task) {
@@ -537,7 +537,7 @@ export class Controller {
taskMetadataFilePath: string
apiConversationHistory: Anthropic.MessageParam[]
}> {
const history = this.cacheService.getGlobalStateKey("taskHistory")
const history = this.stateManager.getGlobalStateKey("taskHistory")
const historyItem = history.find((item) => item.id === id)
if (historyItem) {
const taskDirPath = path.join(this.context.globalStorageUri.fsPath, "tasks", id)
@@ -572,9 +572,9 @@ export class Controller {
async deleteTaskFromState(id: string) {
// Remove the task from history
const taskHistory = this.cacheService.getGlobalStateKey("taskHistory")
const taskHistory = this.stateManager.getGlobalStateKey("taskHistory")
const updatedTaskHistory = taskHistory.filter((task) => task.id !== id)
this.cacheService.setGlobalState("taskHistory", updatedTaskHistory)
this.stateManager.setGlobalState("taskHistory", updatedTaskHistory)
// Notify the webview that the task has been deleted
await this.postStateToWebview()
@@ -589,39 +589,40 @@ export class Controller {
async getStateToPostToWebview(): Promise<ExtensionState> {
// Get API configuration from cache for immediate access
const apiConfiguration = this.cacheService.getApiConfiguration()
const lastShownAnnouncementId = this.cacheService.getGlobalStateKey("lastShownAnnouncementId")
const taskHistory = this.cacheService.getGlobalStateKey("taskHistory")
const autoApprovalSettings = this.cacheService.getGlobalStateKey("autoApprovalSettings")
const browserSettings = this.cacheService.getGlobalStateKey("browserSettings")
const focusChainSettings = this.cacheService.getGlobalStateKey("focusChainSettings")
const focusChainFeatureFlagEnabled = this.cacheService.getGlobalStateKey("focusChainFeatureFlagEnabled")
const preferredLanguage = this.cacheService.getGlobalStateKey("preferredLanguage")
const openaiReasoningEffort = this.cacheService.getGlobalStateKey("openaiReasoningEffort")
const mode = this.cacheService.getGlobalStateKey("mode")
const strictPlanModeEnabled = this.cacheService.getGlobalStateKey("strictPlanModeEnabled")
const useAutoCondense = this.cacheService.getGlobalStateKey("useAutoCondense")
const userInfo = this.cacheService.getGlobalStateKey("userInfo")
const mcpMarketplaceEnabled = this.cacheService.getGlobalStateKey("mcpMarketplaceEnabled")
const mcpDisplayMode = this.cacheService.getGlobalStateKey("mcpDisplayMode")
const telemetrySetting = this.cacheService.getGlobalStateKey("telemetrySetting")
const planActSeparateModelsSetting = this.cacheService.getGlobalStateKey("planActSeparateModelsSetting")
const enableCheckpointsSetting = this.cacheService.getGlobalStateKey("enableCheckpointsSetting")
const globalClineRulesToggles = this.cacheService.getGlobalStateKey("globalClineRulesToggles")
const globalWorkflowToggles = this.cacheService.getGlobalStateKey("globalWorkflowToggles")
const shellIntegrationTimeout = this.cacheService.getGlobalStateKey("shellIntegrationTimeout")
const terminalReuseEnabled = this.cacheService.getGlobalStateKey("terminalReuseEnabled")
const defaultTerminalProfile = this.cacheService.getGlobalStateKey("defaultTerminalProfile")
const isNewUser = this.cacheService.getGlobalStateKey("isNewUser")
const apiConfiguration = this.stateManager.getApiConfiguration()
const lastShownAnnouncementId = this.stateManager.getGlobalStateKey("lastShownAnnouncementId")
const taskHistory = this.stateManager.getGlobalStateKey("taskHistory")
const autoApprovalSettings = this.stateManager.getGlobalStateKey("autoApprovalSettings")
const browserSettings = this.stateManager.getGlobalStateKey("browserSettings")
const focusChainSettings = this.stateManager.getGlobalStateKey("focusChainSettings")
const focusChainFeatureFlagEnabled = this.stateManager.getGlobalStateKey("focusChainFeatureFlagEnabled")
const preferredLanguage = this.stateManager.getGlobalStateKey("preferredLanguage")
const openaiReasoningEffort = this.stateManager.getGlobalStateKey("openaiReasoningEffort")
const mode = this.stateManager.getGlobalStateKey("mode")
const strictPlanModeEnabled = this.stateManager.getGlobalStateKey("strictPlanModeEnabled")
const useAutoCondense = this.stateManager.getGlobalStateKey("useAutoCondense")
const userInfo = this.stateManager.getGlobalStateKey("userInfo")
const mcpMarketplaceEnabled = this.stateManager.getGlobalStateKey("mcpMarketplaceEnabled")
const mcpDisplayMode = this.stateManager.getGlobalStateKey("mcpDisplayMode")
const telemetrySetting = this.stateManager.getGlobalStateKey("telemetrySetting")
const planActSeparateModelsSetting = this.stateManager.getGlobalStateKey("planActSeparateModelsSetting")
const enableCheckpointsSetting = this.stateManager.getGlobalStateKey("enableCheckpointsSetting")
const globalClineRulesToggles = this.stateManager.getGlobalStateKey("globalClineRulesToggles")
const globalWorkflowToggles = this.stateManager.getGlobalStateKey("globalWorkflowToggles")
const shellIntegrationTimeout = this.stateManager.getGlobalStateKey("shellIntegrationTimeout")
const terminalReuseEnabled = this.stateManager.getGlobalStateKey("terminalReuseEnabled")
const defaultTerminalProfile = this.stateManager.getGlobalStateKey("defaultTerminalProfile")
const isNewUser = this.stateManager.getGlobalStateKey("isNewUser")
const welcomeViewCompleted = Boolean(
this.cacheService.getGlobalStateKey("welcomeViewCompleted") || this.authService.getInfo()?.user?.uid,
this.stateManager.getGlobalStateKey("welcomeViewCompleted") || this.authService.getInfo()?.user?.uid,
)
const mcpResponsesCollapsed = this.cacheService.getGlobalStateKey("mcpResponsesCollapsed")
const terminalOutputLineLimit = this.cacheService.getGlobalStateKey("terminalOutputLineLimit")
const localClineRulesToggles = this.cacheService.getWorkspaceStateKey("localClineRulesToggles")
const localWindsurfRulesToggles = this.cacheService.getWorkspaceStateKey("localWindsurfRulesToggles")
const localCursorRulesToggles = this.cacheService.getWorkspaceStateKey("localCursorRulesToggles")
const workflowToggles = this.cacheService.getWorkspaceStateKey("workflowToggles")
const customPrompt = this.stateManager.getGlobalStateKey("customPrompt")
const mcpResponsesCollapsed = this.stateManager.getGlobalStateKey("mcpResponsesCollapsed")
const terminalOutputLineLimit = this.stateManager.getGlobalStateKey("terminalOutputLineLimit")
const localClineRulesToggles = this.stateManager.getWorkspaceStateKey("localClineRulesToggles")
const localWindsurfRulesToggles = this.stateManager.getWorkspaceStateKey("localWindsurfRulesToggles")
const localCursorRulesToggles = this.stateManager.getWorkspaceStateKey("localCursorRulesToggles")
const workflowToggles = this.stateManager.getWorkspaceStateKey("workflowToggles")
const currentTaskItem = this.task?.taskId ? (taskHistory || []).find((item) => item.id === this.task?.taskId) : undefined
const checkpointTrackerErrorMessage = this.task?.taskState.checkpointTrackerErrorMessage
@@ -679,6 +680,7 @@ export class Controller {
welcomeViewCompleted: welcomeViewCompleted as boolean, // Can be undefined but is set to either true or false by the migration that runs on extension launch in extension.ts
mcpResponsesCollapsed,
terminalOutputLineLimit,
customPrompt,
}
}
@@ -708,14 +710,14 @@ export class Controller {
*/
async updateTaskHistory(item: HistoryItem): Promise<HistoryItem[]> {
const history = this.cacheService.getGlobalStateKey("taskHistory")
const history = this.stateManager.getGlobalStateKey("taskHistory")
const existingItemIndex = history.findIndex((h) => h.id === item.id)
if (existingItemIndex !== -1) {
history[existingItemIndex] = item
} else {
history.push(item)
}
this.cacheService.setGlobalState("taskHistory", history)
this.stateManager.setGlobalState("taskHistory", history)
return history
}
}
@@ -22,7 +22,7 @@ export async function refreshBasetenModels(
const basetenModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.basetenModels)
// Get the Baseten API key from the controller's state
const basetenApiKey = controller.cacheService.getSecretKey("basetenApiKey")
const basetenApiKey = controller.stateManager.getSecretKey("basetenApiKey")
const models: Record<string, Partial<OpenRouterModelInfo>> = {}
try {
@@ -18,7 +18,7 @@ import { Controller } from ".."
export async function refreshGroqModels(controller: Controller, _request: EmptyRequest): Promise<OpenRouterCompatibleModelInfo> {
const groqModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.groqModels)
const groqApiKey = controller.cacheService.getSecretKey("groqApiKey")
const groqApiKey = controller.stateManager.getSecretKey("groqApiKey")
let models: Record<string, Partial<OpenRouterModelInfo>> = {}
try {
@@ -181,6 +181,7 @@ export async function refreshOpenRouterModels(
case "openai/gpt-5-mini":
case "openai/gpt-5-nano":
modelInfo.maxTokens = 8_192 // 128000 breaks context window truncation
modelInfo.contextWindow = 272_000 // openrouter reports 400k but the input limit is actually 400k-128k
break
default:
if (rawModel.id.startsWith("openai/")) {
@@ -19,7 +19,7 @@ export async function refreshRequestyModels(controller: Controller, _: EmptyRequ
const models: Record<string, OpenRouterModelInfo> = {}
try {
const apiKey = controller.cacheService.getSecretKey("requestyApiKey")
const apiKey = controller.stateManager.getSecretKey("requestyApiKey")
const headers = {
Authorization: `Bearer ${apiKey}`,
}
@@ -24,7 +24,7 @@ export async function updateApiConfigurationProto(
const appApiConfiguration = convertProtoToApiConfiguration(request.apiConfiguration)
// Update the API configuration in storage
controller.cacheService.setApiConfiguration(appApiConfiguration)
controller.stateManager.setApiConfiguration(appApiConfiguration)
// Update the task's API handler if there's an active task
if (controller.task) {
@@ -11,7 +11,7 @@ import type { Controller } from "../index"
export async function setWelcomeViewCompleted(controller: Controller, request: BooleanRequest): Promise<Empty> {
try {
// Update the global state to set welcomeViewCompleted to the requested value
controller.cacheService.setGlobalState("welcomeViewCompleted", request.value)
controller.stateManager.setGlobalState("welcomeViewCompleted", request.value)
await controller.postStateToWebview()
@@ -15,7 +15,7 @@ export async function toggleFavoriteModel(controller: Controller, request: Strin
}
const modelId = request.value
const apiConfiguration = controller.cacheService.getApiConfiguration()
const apiConfiguration = controller.stateManager.getApiConfiguration()
const favoritedModelIds = apiConfiguration.favoritedModelIds || []
@@ -29,7 +29,7 @@ export async function toggleFavoriteModel(controller: Controller, request: Strin
...apiConfiguration,
favoritedModelIds: updatedFavorites,
}
controller.cacheService.setApiConfiguration(updatedApiConfiguration)
controller.stateManager.setApiConfiguration(updatedApiConfiguration)
// Capture telemetry for model favorite toggle
const isFavorited = !favoritedModelIds.includes(modelId)

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