mirror of
https://github.com/cline/cline.git
synced 2026-09-09 15:02:23 +08:00
Compare commits
89
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98342b1af3 | ||
|
|
274349f944 | ||
|
|
6d24e22bf6 | ||
|
|
8fae4e64d5 | ||
|
|
fc5adcf8eb | ||
|
|
9eaf023bac | ||
|
|
cf9ce1d103 | ||
|
|
675b5e1bed | ||
|
|
2fe24055c0 | ||
|
|
e4d26bef97 | ||
|
|
27715132e1 | ||
|
|
b792143514 | ||
|
|
1c7d33a495 | ||
|
|
eb6e4818d3 | ||
|
|
61d2f42955 | ||
|
|
c78fe237e2 | ||
|
|
0ffb7dd56b | ||
|
|
e9ce38472f | ||
|
|
5802b6847e | ||
|
|
4a768702aa | ||
|
|
4565e067af | ||
|
|
4650ffa86b | ||
|
|
f6d50ead3f | ||
|
|
70cc437d71 | ||
|
|
bdfda6f908 | ||
|
|
c5de50fdd2 | ||
|
|
77c9863b50 | ||
|
|
03d44105cc | ||
|
|
79b76fd783 | ||
|
|
19cc8bc9f8 | ||
|
|
08c04a3c67 | ||
|
|
41ae7326c0 | ||
|
|
b0961f4538 | ||
|
|
26242f6378 | ||
|
|
13228ed46f | ||
|
|
d162a4b420 | ||
|
|
1704684af8 | ||
|
|
c63d9a13a5 | ||
|
|
65243adb24 | ||
|
|
e35f7b4e21 | ||
|
|
82449dabd6 | ||
|
|
74ec823017 | ||
|
|
91e222fe37 | ||
|
|
14230e7221 | ||
|
|
4b697d8695 | ||
|
|
deeda6e273 | ||
|
|
7e7844529f | ||
|
|
4196c14c9c | ||
|
|
5294e78dde | ||
|
|
d97424fcab | ||
|
|
2b3c0bb633 | ||
|
|
b8e2fd669d | ||
|
|
df7f9fcba4 | ||
|
|
db0b022b6e | ||
|
|
459adf0450 | ||
|
|
df37f29746 | ||
|
|
60c210b017 | ||
|
|
d4bd755e60 | ||
|
|
aed152b530 | ||
|
|
5a8e9d8fa8 | ||
|
|
7610cecde6 | ||
|
|
bb26b3b64d | ||
|
|
aabdeba0f3 | ||
|
|
fd68a81a26 | ||
|
|
0e07b92be2 | ||
|
|
4addffe94c | ||
|
|
0836e4d45a | ||
|
|
4a57e5a075 | ||
|
|
29458d7675 | ||
|
|
90b0d6a73b | ||
|
|
547051bfa8 | ||
|
|
93595af09f | ||
|
|
3828c0d1bc | ||
|
|
a2263de7cb | ||
|
|
e53fa8307d | ||
|
|
b8cfb87121 | ||
|
|
76a64ef77d | ||
|
|
29bdb6c981 | ||
|
|
9bbc0da821 | ||
|
|
d2080c1f93 | ||
|
|
044dd686a0 | ||
|
|
ea4f571463 | ||
|
|
8cddbcfd99 | ||
|
|
4e0cb64e77 | ||
|
|
4aa3764beb | ||
|
|
a525d6dd5e | ||
|
|
59dd3236e4 | ||
|
|
5439426ff6 | ||
|
|
fffcc80477 |
@@ -2,4 +2,4 @@
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
update prompt for new task
|
||||
add open ai cache to ui
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
menu fix for slash commands
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Move updateMcpTimeout message to protobus
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Add aliasing to imports in the extension
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add the o1 to the isReasoningModelFamily to avoid temperature be passed to the azure api
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Fix add new rule file button not working
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add !include .file directive support for .clineignore
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
updated gemini caching for OR and cline provider
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix Ollama provider timeout by increasing it from 30 sec to 120 seconds to accommodate model loading time
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix Non-UTF-8 File Handling: Improve Encoding Detection to Prevent Garbled Text and Binary Misclassification
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Make Previous Updates in the Announcement a dropdown
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Migrate prompting section to new docs
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Update the extension import paths to use aliasing
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fixed bug causing saved settings to get reset by changing providers
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fixes auto approve settings becoming unset
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
add ui for windsurf and cursor rules
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Batch selection and deletion of tasks in history
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Migrate the toggleMcpServer message to protobus
|
||||
@@ -30,7 +30,7 @@ graph TB
|
||||
CheckpointSystem[Git-based Checkpoints]
|
||||
end
|
||||
|
||||
subgraph APIProviders[API Providers]
|
||||
subgraph apiProviders[API Providers]
|
||||
AnthropicAPI[Anthropic]
|
||||
OpenRouterAPI[OpenRouter]
|
||||
BedrockAPI[AWS Bedrock]
|
||||
@@ -51,7 +51,7 @@ graph TB
|
||||
Task --> SecretsStorage
|
||||
Task --> TaskStorage
|
||||
Task --> CheckpointSystem
|
||||
Task --> |API Requests| APIProviders
|
||||
Task --> |API Requests| apiProviders
|
||||
McpHub --> |Connects to| ExternalMcpServers
|
||||
Task --> |Uses| McpHub
|
||||
|
||||
@@ -67,7 +67,7 @@ graph TB
|
||||
style ExtStateContext fill:#bbf,stroke:#333,stroke-width:2px
|
||||
style WebviewProvider fill:#bfb,stroke:#333,stroke-width:2px
|
||||
style McpHub fill:#bfb,stroke:#333,stroke-width:2px
|
||||
style APIProviders fill:#fdb,stroke:#333,stroke-width:2px
|
||||
style apiProviders fill:#fdb,stroke:#333,stroke-width:2px
|
||||
```
|
||||
|
||||
## Definitions
|
||||
|
||||
@@ -13,6 +13,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
codespell:
|
||||
if: false
|
||||
name: Check for spelling errors
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -23,3 +24,5 @@ jobs:
|
||||
uses: codespell-project/codespell-problem-matcher@v1
|
||||
- name: Codespell
|
||||
uses: codespell-project/actions-codespell@v2
|
||||
with:
|
||||
only_warn: 1
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.15.1
|
||||
node-version: "lts/*"
|
||||
|
||||
# Cache root dependencies - only reuse if package-lock.json exactly matches
|
||||
- name: Cache root dependencies
|
||||
@@ -54,14 +54,14 @@ jobs:
|
||||
|
||||
- name: Install root dependencies
|
||||
if: steps.root-cache.outputs.cache-hit != 'true'
|
||||
run: npm ci
|
||||
run: npm ci --include=optional
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
if: steps.webview-cache.outputs.cache-hit != 'true'
|
||||
run: cd webview-ui && npm ci
|
||||
run: cd webview-ui && npm ci --include=optional
|
||||
|
||||
- name: Install Publishing Tools
|
||||
run: npm install -g vsce ovsx
|
||||
run: npm install -g @vscode/vsce ovsx
|
||||
|
||||
- name: Get Version
|
||||
id: get_version
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.15.1
|
||||
node-version: 22
|
||||
|
||||
# Setup Python for coverage script
|
||||
- name: Setup Python
|
||||
@@ -70,19 +70,18 @@ jobs:
|
||||
run: npm run format
|
||||
|
||||
# Build the extension before running tests
|
||||
- name: Build Extension
|
||||
run: npm run compile
|
||||
- name: Build Tests and Extension
|
||||
run: npm run pretest
|
||||
|
||||
# Disabling due to compatability with test framework and ESM modules
|
||||
# - name: Unit Tests
|
||||
# run: npm run test:unit
|
||||
- name: Unit Tests
|
||||
run: npm run test:unit
|
||||
|
||||
# Run extension tests with coverage
|
||||
- name: Extension Tests with Coverage
|
||||
id: extension_coverage
|
||||
continue-on-error: true
|
||||
run: |
|
||||
xvfb-run -a npm run test:coverage > extension_coverage.txt 2>&1 || true
|
||||
xvfb-run -a npm run test:coverage > extension_coverage.txt 2>&1
|
||||
PYTHONPATH=.github/scripts python -m coverage_check extract-coverage extension_coverage.txt --type=extension --github-output --verbose
|
||||
|
||||
# Run webview tests with coverage
|
||||
@@ -107,6 +106,18 @@ jobs:
|
||||
webview-ui/webview_coverage.txt
|
||||
retention-period: workflow # Artifacts are automatically deleted when the workflow completes
|
||||
|
||||
# Set the check as failed if any of the tests failed
|
||||
- name: Check for test failures
|
||||
run: |
|
||||
# Check if any of the test steps failed
|
||||
# https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs#steps-context
|
||||
if [ "${{ steps.extension_coverage.outcome }}" != "success" ] || [ "${{ steps.webview_coverage.outcome }}" != "success" ]; then
|
||||
echo "Tests failed."
|
||||
cat extension_coverage.txt
|
||||
cat webview-ui/webview_coverage.txt
|
||||
exit 1
|
||||
fi
|
||||
|
||||
coverage:
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
@@ -132,7 +143,7 @@ jobs:
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.15.1
|
||||
node-version: 22
|
||||
|
||||
# Cache root dependencies - only reuse if package-lock.json exactly matches
|
||||
- name: Cache root dependencies
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"extension": ["ts"],
|
||||
"spec": "src/**/__tests__/*.ts",
|
||||
"require": ["ts-node/register", "source-map-support/register"],
|
||||
"require": ["ts-node/register", "source-map-support/register", "./src/test/requires.ts"],
|
||||
"recursive": true
|
||||
}
|
||||
|
||||
@@ -6,6 +6,10 @@ export default defineConfig({
|
||||
mocha: {
|
||||
ui: "bdd",
|
||||
timeout: 20000, // Maximum time (in ms) that a test can run before failing
|
||||
/** Set up alias path resolution during tests
|
||||
* @See {@link file://./test-setup.js}
|
||||
*/
|
||||
require: ["./test-setup.js"],
|
||||
},
|
||||
workspaceFolder: "test-workspace",
|
||||
version: "stable",
|
||||
|
||||
Vendored
+1
-1
@@ -9,7 +9,7 @@
|
||||
"name": "Run Extension",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
|
||||
"args": ["--extensionDevelopmentPath=${workspaceFolder}", "--disable-workspace-trust", "${workspaceFolder}"],
|
||||
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||
"preLaunchTask": "${defaultBuildTask}",
|
||||
"env": {
|
||||
|
||||
Vendored
+41
-19
@@ -4,10 +4,25 @@
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "watch",
|
||||
"dependsOn": ["npm: build:webview", "npm: dev:webview", "npm: watch:tsc", "npm: watch:esbuild"],
|
||||
"label": "npm: protos",
|
||||
"type": "npm",
|
||||
"script": "protos",
|
||||
"problemMatcher": [],
|
||||
"isBackground": false,
|
||||
"presentation": {
|
||||
"reveal": "never"
|
||||
"reveal": "always"
|
||||
},
|
||||
"options": {
|
||||
"env": {
|
||||
"IS_DEV": "true"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "watch",
|
||||
"dependsOn": ["npm: protos", "npm: build:webview", "npm: dev:webview", "npm: watch:tsc", "npm: watch:esbuild"],
|
||||
"presentation": {
|
||||
"reveal": "always"
|
||||
},
|
||||
"group": {
|
||||
"kind": "build",
|
||||
@@ -16,9 +31,15 @@
|
||||
},
|
||||
{
|
||||
"label": "watch:test",
|
||||
"dependsOn": ["npm: build:webview:test", "npm: dev:webview", "npm: watch:tsc", "npm: watch:esbuild:test"],
|
||||
"dependsOn": [
|
||||
"npm: protos",
|
||||
"npm: build:webview:test",
|
||||
"npm: dev:webview",
|
||||
"npm: watch:tsc",
|
||||
"npm: watch:esbuild:test"
|
||||
],
|
||||
"presentation": {
|
||||
"reveal": "never"
|
||||
"reveal": "always"
|
||||
},
|
||||
"group": "build"
|
||||
},
|
||||
@@ -29,10 +50,10 @@
|
||||
"problemMatcher": [],
|
||||
"isBackground": true,
|
||||
"label": "npm: build:webview",
|
||||
"dependsOn": ["npm: protos"],
|
||||
"presentation": {
|
||||
"group": "watch",
|
||||
"reveal": "never",
|
||||
"close": true
|
||||
"reveal": "always"
|
||||
},
|
||||
"options": {
|
||||
"env": {
|
||||
@@ -47,10 +68,10 @@
|
||||
"problemMatcher": [],
|
||||
"isBackground": true,
|
||||
"label": "npm: build:webview:test",
|
||||
"dependsOn": ["npm: protos"],
|
||||
"presentation": {
|
||||
"group": "watch",
|
||||
"reveal": "never",
|
||||
"close": true
|
||||
"reveal": "always"
|
||||
},
|
||||
"options": {
|
||||
"env": {
|
||||
@@ -82,10 +103,10 @@
|
||||
],
|
||||
"isBackground": true,
|
||||
"label": "npm: dev:webview",
|
||||
"dependsOn": ["npm: protos"],
|
||||
"presentation": {
|
||||
"group": "watch",
|
||||
"reveal": "never",
|
||||
"close": true
|
||||
"reveal": "always"
|
||||
},
|
||||
"options": {
|
||||
"env": {
|
||||
@@ -100,10 +121,10 @@
|
||||
"problemMatcher": "$esbuild-watch",
|
||||
"isBackground": true,
|
||||
"label": "npm: watch:esbuild",
|
||||
"dependsOn": ["npm: protos"],
|
||||
"presentation": {
|
||||
"group": "watch",
|
||||
"reveal": "never",
|
||||
"close": true
|
||||
"reveal": "always"
|
||||
},
|
||||
"options": {
|
||||
"env": {
|
||||
@@ -118,10 +139,10 @@
|
||||
"problemMatcher": "$esbuild-watch",
|
||||
"isBackground": true,
|
||||
"label": "npm: watch:esbuild:test",
|
||||
"dependsOn": ["npm: protos"],
|
||||
"presentation": {
|
||||
"group": "watch",
|
||||
"reveal": "never",
|
||||
"close": true
|
||||
"reveal": "always"
|
||||
},
|
||||
"options": {
|
||||
"env": {
|
||||
@@ -137,10 +158,10 @@
|
||||
"problemMatcher": "$tsc-watch",
|
||||
"isBackground": true,
|
||||
"label": "npm: watch:tsc",
|
||||
"dependsOn": ["npm: protos"],
|
||||
"presentation": {
|
||||
"group": "watch",
|
||||
"reveal": "never",
|
||||
"close": true
|
||||
"reveal": "always"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -148,15 +169,16 @@
|
||||
"script": "watch-tests",
|
||||
"problemMatcher": "$tsc-watch",
|
||||
"isBackground": true,
|
||||
"dependsOn": ["npm: protos"],
|
||||
"presentation": {
|
||||
"reveal": "never",
|
||||
"reveal": "always",
|
||||
"group": "watchers"
|
||||
},
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"label": "tasks: watch-tests",
|
||||
"dependsOn": ["npm: watch", "npm: watch-tests"],
|
||||
"dependsOn": ["npm: protos", "npm: watch", "npm: watch-tests"],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
|
||||
+5
-1
@@ -37,8 +37,12 @@ docs/**
|
||||
!node_modules/@vscode/codicons/dist/codicon.css
|
||||
!node_modules/@vscode/codicons/dist/codicon.ttf
|
||||
|
||||
# Include KaTeX CSS and fonts for LaTeX rendering
|
||||
!webview-ui/node_modules/katex/dist/katex.min.css
|
||||
!webview-ui/node_modules/katex/dist/fonts/**
|
||||
|
||||
# Include default themes JSON files used in getTheme
|
||||
!src/integrations/theme/default-themes/**
|
||||
|
||||
# Include icons
|
||||
!assets/icons/**
|
||||
!assets/icons/**
|
||||
|
||||
@@ -1,5 +1,60 @@
|
||||
# Changelog
|
||||
|
||||
## [3.14.0]
|
||||
|
||||
- Add support for custom model ID in AWS Bedrock provider, enabling use of Application Inference Profile (Thanks @clicube!)
|
||||
- Add more robust caching & cache tracking for gemini & vertex providers
|
||||
- Add support for LaTeX rendering
|
||||
- Add support for custom API request timeout. Timeouts were 15-30s, but can now be configured via settings for OpenRouter/Cline & Ollama (Thanks @WingsDrafterwork!)
|
||||
- Add truncation notice when truncating manually
|
||||
- Add a timeout setting for the terminal connection, allowing users to set a time to wait for terminal startup
|
||||
- Add copy button to code blocks
|
||||
- Add copy button to markdown blocks (Thanks @weshoke!)
|
||||
- Add checkpoints to more messages
|
||||
- Add slash command to create a new rules file (/newrule)
|
||||
- Add cache ui for open router and cline provider
|
||||
- Add Amazon Nova Premier model to Bedrock (Thanks @watany!)
|
||||
- Add support for cursorrules and windsurfrules
|
||||
- Add support for batch history deletion (Thanks @danix800!)
|
||||
- Improve Drag & Drop experience
|
||||
- Create clinerules folder when creating new rule if it's needed
|
||||
- Enable pricing calculation for gemini and vertex providers
|
||||
- Refactor message handling to not show the MCP View of the server modal
|
||||
- Migrate the addRemoteServer to protobus (Thanks @DaveFres!)
|
||||
- Update task header to be expanded by default
|
||||
- Update Gemini cache TTL time to 15 minutes
|
||||
- Fix race condition in terminal command usage
|
||||
- Fix to correctly handle `import.meta.url`, avoiding leading slash in pathname for Windows (Thanks @DaveFres!)
|
||||
- Fix @withRetry() decoration syntax error when running extension locally (Thanks @DaveFres!)
|
||||
- Fix for git commit mentions in repos with no git commits
|
||||
- Fix cost calculation (Thanks @BarreiroT!)
|
||||
|
||||
## [3.13.3]
|
||||
|
||||
- Add download counts to MCP marketplace items
|
||||
- Add `/compact` command
|
||||
- Add prompt caching to gemini models in cline / openrouter providers
|
||||
- Add tooltips to bottom row menu
|
||||
|
||||
## [3.13.2]
|
||||
|
||||
- Add Gemini 2.5 Flash model to Vertex and Gemini Providers (Thanks monotykamary!)
|
||||
- Add Caching to gemini provider (Thanks arafatkatze!)
|
||||
- Add thinking budget support to Gemini Models (Thanks monotykamary!)
|
||||
- Add !include .file directive support for .clineignore (Thanks watany-dev!)
|
||||
- Improve slash command functionality
|
||||
- Improve prompting for new task tool
|
||||
- Fix o1 temperature being passed to the azure api (Thanks treeleaves30760!)
|
||||
- Fix to make "add new rule file" button functional
|
||||
- Fix Ollama provider timeout, allowing for a larger loading time (Thanks suvarchal!)
|
||||
- Fix Non-UTF-8 File Handling: Improve Encoding Detection to Prevent Garbled Text and Binary Misclassification (Thanks yt3trees!)
|
||||
- Fix settings to not reset by changing providers
|
||||
- Fix terminal outputs missing commas
|
||||
- Fix terminal errors caused by starting non-alphanumeric outputs
|
||||
- Fix auto approve settings becoming unset
|
||||
- Fix Mermaid syntax error in documentation (Thanks tuki0918!)
|
||||
- Remove supportsComputerUse restriction and support browser use through any model that supports images (Thanks arafatkatze!)
|
||||
|
||||
## [3.13.1]
|
||||
|
||||
- Fix bug where task cancellation during thinking stream would result in error state
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 902 B |
Binary file not shown.
|
After Width: | Height: | Size: 666 B |
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"$schema": "https://mintlify.com/docs.json",
|
||||
"theme": "linden",
|
||||
"name": "Cline",
|
||||
"description": "AI-powered coding assistant for VSCode",
|
||||
"colors": {
|
||||
"primary": "#9D4EDD",
|
||||
"light": "#F0E6FF",
|
||||
"dark": "#000000"
|
||||
},
|
||||
"logo": {
|
||||
"light": "/assets/robot_panel_light.png",
|
||||
"dark": "/assets/robot_panel_dark.png"
|
||||
},
|
||||
"favicon": {
|
||||
"light": "/assets/robot_panel_light.png",
|
||||
"dark": "/assets/robot_panel_dark.png"
|
||||
},
|
||||
"background": {
|
||||
"color": {
|
||||
"light": "#F0E6FF",
|
||||
"dark": "#000000"
|
||||
},
|
||||
"decoration": "gradient"
|
||||
},
|
||||
"styling": {
|
||||
"eyebrows": "breadcrumbs",
|
||||
"codeblocks": "system"
|
||||
},
|
||||
"appearance": {
|
||||
"default": "system",
|
||||
"strict": false
|
||||
},
|
||||
"fonts": {
|
||||
"family": "Roboto",
|
||||
"weight": 400
|
||||
},
|
||||
"navbar": {
|
||||
"links": [
|
||||
{
|
||||
"label": "GitHub",
|
||||
"href": "https://github.com/cline/cline"
|
||||
},
|
||||
{
|
||||
"label": "Discord",
|
||||
"href": "https://discord.gg/cline"
|
||||
}
|
||||
],
|
||||
"primary": {
|
||||
"type": "button",
|
||||
"label": "Install Cline",
|
||||
"href": "https://cline.bot/install?utm_source=website&utm_medium=header"
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"groups": [
|
||||
{
|
||||
"group": "Exploring Cline's Tools",
|
||||
"pages": [
|
||||
"exploring-clines-tools/cline-tools-guide",
|
||||
"exploring-clines-tools/plan-and-act-modes-a-guide-to-effective-ai-development",
|
||||
"exploring-clines-tools/checkpoints",
|
||||
"exploring-clines-tools/new-task-tool",
|
||||
"exploring-clines-tools/remote-browser-support",
|
||||
"exploring-clines-tools/slash-commands"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Improving Your Prompting Skills",
|
||||
"pages": ["prompting/prompt-engineering-guide", "prompting/cline-memory-bank"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"footer": {
|
||||
"socials": {
|
||||
"x": "https://x.com/cline",
|
||||
"github": "https://github.com/cline/cline",
|
||||
"discord": "https://discord.gg/cline"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"prompt": "Search Cline documentation..."
|
||||
},
|
||||
"contextual": {
|
||||
"options": ["copy"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
---
|
||||
title: "Checkpoints and Messages"
|
||||
description: "When working with AI coding assistants, it's easy to lose control as they make rapid changes to your codebase. That's why we built Checkpoints - your safety net for experimenting confidently."
|
||||
---
|
||||
|
||||
Checkpoints automatically save snapshots of your workspace after each step in a task. This powerful feature lets you:
|
||||
|
||||
- Track and review changes made during a task
|
||||
- Roll back to any previous point if needed
|
||||
- Experiment confidently with auto-approve mode
|
||||
- Maintain full control over your workspace
|
||||
|
||||
### ⚙️ How Checkpoints Work
|
||||
|
||||
Cline creates a checkpoint after each tool use (file edits, commands, etc.). These checkpoints:
|
||||
|
||||
- Work alongside your Git workflow without interference
|
||||
- Maintain context between restores
|
||||
- Use a shadow Git repository to track changes
|
||||
|
||||
For example, if you're working on a feature and Cline makes multiple file changes, each change creates a checkpoint. This means you can review each modification and, if needed, roll back to any point without affecting your main Git repository.
|
||||
|
||||
#### Viewing Changes & Restoring to Checkpoint
|
||||
|
||||
After each tool use, you can:
|
||||
|
||||
1. Click the "Compare" button to see modified files
|
||||
2. Click the "Restore" button to open restore options
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Checkpoint comparison and restore options" />
|
||||
</Frame>
|
||||
|
||||
#### Rolling Back
|
||||
|
||||
To restore to a previous point:
|
||||
|
||||
1. Click the "Restore" button next to any step
|
||||
2. Choose from three options:
|
||||
- **Restore Task and Workspace**: Reset both codebase and task to that point
|
||||
- **Restore Task Only**: Keep codebase changes but revert task context
|
||||
- **Restore Workspace Only**: Reset codebase while preserving task context
|
||||
|
||||
Example: If Cline makes changes you don't like while styling a component, you can use "Restore Workspace Only" to revert the code changes while keeping the conversation context, allowing you to try a different approach.
|
||||
|
||||
### 💡 Use Cases
|
||||
|
||||
Checkpoints let you be more experimental with Cline. While human coding is often methodical and iterative, AI can make substantial changes quickly. Checkpoints help you track these changes and revert if needed.
|
||||
|
||||
#### 1. Using Auto-Approve Mode
|
||||
|
||||
- Provides safety net for rapid iterations
|
||||
- Makes it easy to undo unexpected results
|
||||
|
||||
#### 2. Testing Different Approaches
|
||||
|
||||
- Try multiple solutions confidently
|
||||
- Compare different implementations
|
||||
- Quickly revert to working states
|
||||
- Ideal for exploring different design patterns or architectural approaches
|
||||
|
||||
<Frame caption="In this case, I didn't like the changes Cline made to my robot dog-walking website (still working on the robots) and I wanted to revert both the codebase and the task to before any changes were made so I could start fresh.">
|
||||
<img src="/assets/robot_panel_dark.png" alt="Checkpoint restore demo" />
|
||||
</Frame>
|
||||
|
||||
### ✨ Best Practices
|
||||
|
||||
1. Use checkpoints as safety nets when experimenting
|
||||
2. Leverage auto-approve mode more confidently, knowing you can always roll back
|
||||
3. Restore selectively based on needs:
|
||||
|
||||
- Use "Restore Task and Workspace" for a fresh start, reversing changes to files and the task conversation.
|
||||
- Use "Restore Task Only" to try different prompts, but leave all files as they exist
|
||||
- Use "Restore Workspace Only" to attempt different implementations, or prune context from the task
|
||||
|
||||
🛟 Checkpoints are your safety net when working with Cline, enabling you to experiment freely while maintaining full control over your codebase. Whether you're refactoring a complex component, trying different implementation approaches, or using auto-approve mode for rapid development, checkpoints ensure you can always review changes and roll back if needed.
|
||||
|
||||
#### 🗑️ Deleting Checkpoints
|
||||
|
||||
You can delete all checkpoints by using the **"Delete All History"** button in the task history menu. Note that this will also delete all tasks. Checkpoints are stored in VS Code's globalStorage.
|
||||
|
||||
---
|
||||
|
||||
## Editing Messages
|
||||
|
||||
Cline allows you to edit chat messages in a task after they've been submitted (with the exception of the message that started the task).
|
||||
|
||||
Perhaps you didn't get the results you wanted, thought of a better way to phrase your request, or need to add more information. Editing your message allows you to re-submit a request without starting over or restoring your files or workspace with checkpoints. There are two Restore options:
|
||||
|
||||
- **"Restore Chat"** restores just the task state and re-submits an API request to your provider with your edited message.
|
||||
|
||||
- **"Restore All"** restores both the task state and workspace state before re-submitting an API request. "Workspace state" refers to the condition of your workspace (files, content, etc.) at different points in the conversation.
|
||||
|
||||
**Interactive Editing:**
|
||||
|
||||
- Messages can be clicked to enter edit mode
|
||||
- Cline automatically selects all text when entering edit mode
|
||||
|
||||
**Keyboard Shortcuts:**
|
||||
|
||||
- Escape: Exit edit mode
|
||||
- Enter: Restore just the task
|
||||
- Cmd/Ctrl + Enter: Restore the task and workspace
|
||||
- Shift + Enter: Insert new line / line break
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Message editing interface" />
|
||||
</Frame>
|
||||
@@ -0,0 +1,139 @@
|
||||
---
|
||||
title: "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, or URLs
|
||||
- Example: "@file:src/components/App.tsx"
|
||||
|
||||
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/Cline.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
|
||||
|
||||
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>
|
||||
```
|
||||
|
||||
## 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
|
||||
@@ -0,0 +1,381 @@
|
||||
---
|
||||
title: "New Task Tool"
|
||||
---
|
||||
|
||||
### The `new_task` Tool & Context Management Strategies
|
||||
|
||||
#### Overview
|
||||
|
||||
Cline includes a powerful internal tool, `new_task`, designed to help manage workflow continuity and context preservation, especially during complex or long-running tasks. This tool, combined with Cline's awareness of its own context window usage and the flexibility of `.clinerules`, enables sophisticated strategies for breaking down work and ensuring seamless transitions between task sessions.
|
||||
|
||||
Understanding the core capabilities and how they interact with custom rules is key to leveraging this feature effectively.
|
||||
|
||||
#### Core Capabilities
|
||||
|
||||
Two fundamental capabilities enable advanced context management:
|
||||
|
||||
1. **The `new_task` Tool:**
|
||||
- **Function:** Allows Cline, upon user approval, to end the current task session and immediately start a new one.
|
||||
- **Context Preloading:** Crucially, Cline can **preload** this new task session with specific context provided within the tool's `<context>` block. This context can be anything Cline or a `.clinerules` file defines – summaries, code snippets, next steps, project state, etc.
|
||||
2. **Context Window Awareness:**
|
||||
- **Tracking:** Cline internally tracks the percentage of its available context window currently being used during a task.
|
||||
- **Visibility:** This information is visible in the `environment_details` provided to Cline in its prompt.
|
||||
|
||||
#### Using the `/newtask` Slash Command
|
||||
|
||||
As a quick alternative to Cline suggesting the `newtask` tool or defining complex rules, you can directly initiate the process using a Slash Command.
|
||||
|
||||
- **How:** Simply type `/newtask` in the chat input field.
|
||||
- **Action:** Cline will propose creating a new task, typically suggesting context based on the current session (similar to its default behavior when using the tool). You will still get the `ask_followup_question` prompt to confirm and potentially modify the context before the new task is created.
|
||||
- **Benefit:** Provides a fast, user-initiated way to leverage the `new_task` functionality for branching explorations or managing long sessions without waiting for Cline to suggest it.
|
||||
|
||||
#### Default Behavior (Without `.clinerules`)
|
||||
|
||||
By default, without specific `.clinerules` dictating its behavior:
|
||||
|
||||
- **Tool Availability:** The `new_task` tool exists, and Cline _can_ choose to use it.
|
||||
- **Context Awareness:** Cline _is_ aware of its context usage percentage.
|
||||
- **No Automatic Trigger:** Cline **will not** automatically initiate a task handoff _solely_ based on context usage reaching a specific percentage (like 50%). The decision to suggest using `new_task` comes from the AI model's reasoning based on the overall task progress and prompt instructions.
|
||||
- **Basic Context Preloading:** If `new_task` is used without specific rules defining the `<context>` block structure, Cline will attempt to preload relevant information based on its current understanding (e.g., a basic summary of progress and next steps), but this may be less comprehensive than a rule-driven approach.
|
||||
|
||||
#### The Power of `.clinerules`: Enabling Custom Workflows
|
||||
|
||||
While the core capabilities exist by default, the true power, automation, and customization emerge when you combine `new_task` and context awareness with custom workflows defined in `.clinerules`. This allows you to precisely control _when_ and _how_ Cline manages context and task continuity.
|
||||
|
||||
Key benefits of using `.clinerules` with `new_task`:
|
||||
|
||||
- **Automated Context Management:** Define rules to automatically trigger handoffs at specific context percentages (e.g., >50%, >70%) or token counts, ensuring optimal performance and preventing context loss.
|
||||
- **Model-Specific Optimization:** Tailor handoff triggers based on known thresholds for different LLMs (e.g., trigger earlier for models known to degrade past a certain token count).
|
||||
- **Intelligent Breakpoints:** Instruct Cline via rules to find logical stopping points (e.g., after completing a function or test) _after_ a context threshold is passed, ensuring cleaner handoffs.
|
||||
- **Structured Task Decomposition:** Use Plan Mode to define subtasks, then use `.clinerules` to have Cline automatically create a new task via `new_task` upon completing each subtask, preloading the context for the _next_ subtask.
|
||||
- **Custom Context Packaging:** Mandate the exact structure and content of the `<context>` block in `.clinerules` for highly detailed and consistent handoffs (see example below).
|
||||
- **Improved Memory Persistence:** Use `new_task` context blocks as a primary, integrated way to persist information across sessions, potentially replacing or supplementing file-based memory systems.
|
||||
- **Workflow Automation:** Define rules for specific scenarios, like always preloading certain setup instructions or project boilerplate when starting tasks of a particular type.
|
||||
|
||||
#### Example Rule-Driven Workflow: Task Handoff Process
|
||||
|
||||
A common workflow, **driven by specific `.clinerules` like the example below**, involves these steps:
|
||||
|
||||
1. **Trigger Identification (Rule-Based):** Cline monitors for handoff points defined in the rules (e.g., context usage > 50%, task completion).
|
||||
2. **User Confirmation:** Cline uses `ask_followup_question` to propose creating a new task, often showing the intended context defined by the rules.
|
||||
|
||||
```xml
|
||||
<ask_followup_question>
|
||||
<question>I've completed [specific accomplishment] and context usage is high (XX%). Would you like me to create a new task to continue with [remaining work], preloading the following context?</question>
|
||||
<options>["Yes, create new task", "Modify context first", "No, continue this session"]</options>
|
||||
</ask_followup_question>
|
||||
```
|
||||
|
||||
3. **User Control:** You can approve, deny, or ask Cline to modify the context before the new task is created.
|
||||
4. **Context Packaging (`new_task` Tool):** If approved, Cline uses `new_task`, packaging the context according to the structure mandated by the `.clinerules`.
|
||||
5. **New Task Creation:** The current task ends, and a new session begins immediately, preloaded with the specified context.
|
||||
|
||||
#### The Handoff Context Block (Rule-Defined Structure)
|
||||
|
||||
The effectiveness of rule-driven handoffs depends heavily on how `.clinerules` define the `<context>` block. A comprehensive structure often includes:
|
||||
|
||||
- **`## Completed Work`**: Detailed list of accomplishments, files modified/created, key decisions.
|
||||
- **`## Current State`**: Project status, running processes, key file states.
|
||||
- **`## Next Steps`**: Clear, prioritized list of remaining tasks, implementation details, known challenges.
|
||||
- **`## Reference Information`**: Links, code snippets, patterns, user preferences.
|
||||
- **Actionable Start:** A clear instruction for the immediate next action.
|
||||
|
||||
#### Potential Use Cases & Workflows
|
||||
|
||||
The flexibility of `new_task` combined with `.clinerules` opens up many possibilities:
|
||||
|
||||
- **Proactive Context Window Management:** Automatically trigger handoffs at specific percentages (e.g., 50%, 70%) or token counts to maintain optimal performance.
|
||||
- **Intelligent Breakpoints:** Instruct Cline to find logical stopping points (e.g., after completing a function or test) _after_ a context threshold is passed, ensuring cleaner handoffs.
|
||||
- **Structured Task Decomposition:** Use Plan Mode to define subtasks, then use `.clinerules` to have Cline automatically create a new task via `new_task` upon completing each subtask.
|
||||
- **Automated Session Summaries:** Configure the `<context>` block to always include a summary of the previous session's key discussion points.
|
||||
- **Preloading Boilerplate/Setup:** Start new tasks related to specific projects preloaded with standard setup instructions or file templates.
|
||||
- **"Memory Bank" Alternative:** Use `new_task` context blocks as the primary way to persist information across sessions, potentially replacing file-based memory systems.
|
||||
|
||||
Experimenting with `.clinerules` is encouraged to discover workflows that best suit your needs!
|
||||
|
||||
#### Example `.clinerules`: Task Handoff Strategy Guide
|
||||
|
||||
Below is an example `.clinerules` file focused specifically on using `new_task` for context window management. **Remember, this is just one specific strategy; the core `new_task` tool can be used differently with other custom rules.**
|
||||
|
||||
````markdown
|
||||
# You MUST use the `new_task` tool: Task Handoff Strategy Guide
|
||||
|
||||
**⚠️ CRITICAL INSTRUCTIONS - YOU MUST FOLLOW THESE GUIDELINES ⚠️**
|
||||
|
||||
This guide provides **MANDATORY** instructions for effectively breaking down complex tasks and implementing a smooth handoff process between tasks. You **MUST** follow these guidelines to ensure continuity, context preservation, and efficient task completion.
|
||||
|
||||
## ⚠️ CONTEXT WINDOW MONITORING - MANDATORY ACTION REQUIRED ⚠️
|
||||
|
||||
You **MUST** monitor the context window usage displayed in the environment details. When usage exceeds 50% of the available context window, you **MUST** initiate a task handoff using the `new_task` tool.
|
||||
|
||||
Example of context window usage over 50% with a 200K context window:
|
||||
|
||||
\`\`\`text
|
||||
|
||||
# Context Window Usage
|
||||
|
||||
105,000 / 200,000 tokens (53%)
|
||||
Model: anthropic/claude-3.7-sonnet (200K context window)
|
||||
\`\`\`
|
||||
|
||||
**IMPORTANT**: When you see context window usage at or above 50%, you MUST:
|
||||
|
||||
1. Complete your current logical step
|
||||
2. Use the `ask_followup_question` tool to offer creating a new task
|
||||
3. If approved, use the `new_task` tool with comprehensive handoff instructions
|
||||
|
||||
## Task Breakdown in Plan Mode - REQUIRED PROCESS
|
||||
|
||||
Plan Mode is specifically designed for analyzing complex tasks and breaking them into manageable subtasks. When in Plan Mode, you **MUST**:
|
||||
|
||||
### 1. Initial Task Analysis - REQUIRED
|
||||
|
||||
- **MUST** begin by thoroughly understanding the full scope of the user's request
|
||||
- **MUST** identify all major components and dependencies of the task
|
||||
- **MUST** consider potential challenges, edge cases, and prerequisites
|
||||
|
||||
### 2. Strategic Task Decomposition - REQUIRED
|
||||
|
||||
- **MUST** break the overall task into logical, discrete subtasks
|
||||
- **MUST** prioritize subtasks based on dependencies (what must be completed first)
|
||||
- **MUST** aim for subtasks that can be completed within a single session (15-30 minutes of work)
|
||||
- **MUST** consider natural breaking points where context switching makes sense
|
||||
|
||||
### 3. Creating a Task Roadmap - REQUIRED
|
||||
|
||||
- **MUST** present a clear, numbered list of subtasks to the user
|
||||
- **MUST** explain dependencies between subtasks
|
||||
- **MUST** provide time estimates for each subtask when possible
|
||||
- **MUST** use Mermaid diagrams to visualize task flow and dependencies when helpful
|
||||
|
||||
\`\`\`mermaid
|
||||
graph TD
|
||||
A[Main Task] --> B[Subtask 1: Setup]
|
||||
A --> C[Subtask 2: Core Implementation]
|
||||
A --> D[Subtask 3: Testing]
|
||||
A --> E[Subtask 4: Documentation]
|
||||
B --> C
|
||||
C --> D
|
||||
\`\`\`
|
||||
|
||||
### 4. Getting User Approval - REQUIRED
|
||||
|
||||
- **MUST** ask for user feedback on the proposed task breakdown
|
||||
- **MUST** adjust the plan based on user priorities or additional requirements
|
||||
- **MUST** confirm which subtask to begin with
|
||||
- **MUST** request the user to toggle to Act Mode when ready to implement
|
||||
|
||||
## Task Implementation and Handoff Process - MANDATORY PROCEDURES
|
||||
|
||||
When implementing tasks in Act Mode, you **MUST** follow these guidelines for effective task handoff:
|
||||
|
||||
### 1. Focused Implementation - REQUIRED
|
||||
|
||||
- **MUST** focus on completing the current subtask fully
|
||||
- **MUST** document progress clearly through comments and commit messages
|
||||
- **MUST** create checkpoints at logical completion points
|
||||
|
||||
### 2. Recognizing Completion Points - CRITICAL
|
||||
|
||||
You **MUST** identify natural handoff points when:
|
||||
|
||||
- The current subtask is fully completed
|
||||
- You've reached a logical stopping point in a larger subtask
|
||||
- The implementation is taking longer than expected and can be continued later
|
||||
- The task scope has expanded beyond the original plan
|
||||
- **CRITICAL**: The context window usage exceeds 50% (e.g., 100,000+ tokens for a 200K context window)
|
||||
|
||||
### 3. Initiating the Handoff Process - MANDATORY ACTION
|
||||
|
||||
When you've reached a completion point, you **MUST**:
|
||||
|
||||
1. Summarize what has been accomplished so far
|
||||
2. Clearly state what remains to be done
|
||||
3. **MANDATORY**: Use the `ask_followup_question` tool to offer creating a new task:
|
||||
|
||||
\`\`\`xml
|
||||
<ask_followup_question>
|
||||
<question>I've completed [specific accomplishment]. Would you like me to create a new task to continue with [remaining work]?</question>
|
||||
<options>["Yes, create a new task", "No, continue in this session", "Let me think about it"]</options>
|
||||
</ask_followup_question>
|
||||
\`\`\`
|
||||
|
||||
### 4. Creating a New Task with Context - REQUIRED ACTION
|
||||
|
||||
If the user agrees to create a new task, you **MUST** use the `new_task` tool with comprehensive handoff instructions:
|
||||
|
||||
\`\`\`xml
|
||||
<new_task>
|
||||
<context>
|
||||
|
||||
# Task Continuation: [Brief Task Title]
|
||||
|
||||
## Completed Work
|
||||
|
||||
- [Detailed list of completed items]
|
||||
- [Include specific files modified/created]
|
||||
- [Note any important decisions made]
|
||||
|
||||
## Current State
|
||||
|
||||
- [Description of the current state of the project]
|
||||
- [Any running processes or environment setup]
|
||||
- [Key files and their current state]
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Detailed list of remaining tasks]
|
||||
- [Specific implementation details to address]
|
||||
- [Any known challenges to be aware of]
|
||||
|
||||
## Reference Information
|
||||
|
||||
- [Links to relevant documentation]
|
||||
- [Important code snippets or patterns to follow]
|
||||
- [Any user preferences noted during the current session]
|
||||
|
||||
Please continue the implementation by [specific next action].
|
||||
</context>
|
||||
</new_task>
|
||||
\`\`\`
|
||||
|
||||
### 5. Detailed Context Transfer - MANDATORY COMPONENTS
|
||||
|
||||
When creating a new task, you **MUST** always include:
|
||||
|
||||
#### Project Context - REQUIRED
|
||||
|
||||
- **MUST** include the overall goal and purpose of the project
|
||||
- **MUST** include key architectural decisions and patterns
|
||||
- **MUST** include technology stack and dependencies
|
||||
|
||||
#### Implementation Details - REQUIRED
|
||||
|
||||
- **MUST** list files created or modified in the current session
|
||||
- **MUST** describe specific functions, classes, or components implemented
|
||||
- **MUST** explain design patterns being followed
|
||||
- **MUST** outline testing approach
|
||||
|
||||
#### Progress Tracking - REQUIRED
|
||||
|
||||
- **MUST** provide checklist of completed items
|
||||
- **MUST** provide checklist of remaining items
|
||||
- **MUST** note any blockers or challenges encountered
|
||||
|
||||
#### User Preferences - REQUIRED
|
||||
|
||||
- **MUST** note coding style preferences mentioned by the user
|
||||
- **MUST** document specific approaches requested by the user
|
||||
- **MUST** highlight priority areas identified by the user
|
||||
|
||||
## Best Practices for Effective Handoffs - MANDATORY GUIDELINES
|
||||
|
||||
### 1. Maintain Continuity - REQUIRED
|
||||
|
||||
- **MUST** use consistent terminology between tasks
|
||||
- **MUST** reference previous decisions and their rationale
|
||||
- **MUST** maintain the same architectural approach unless explicitly changing direction
|
||||
|
||||
### 2. Preserve Context - REQUIRED
|
||||
|
||||
- **MUST** include relevant code snippets in the handoff
|
||||
- **MUST** summarize key discussions from the previous session
|
||||
- **MUST** reference specific files and line numbers when applicable
|
||||
|
||||
### 3. Set Clear Next Actions - REQUIRED
|
||||
|
||||
- **MUST** begin the handoff with a clear, actionable next step
|
||||
- **MUST** prioritize remaining tasks
|
||||
- **MUST** highlight any decisions that need to be made
|
||||
|
||||
### 4. Document Assumptions - REQUIRED
|
||||
|
||||
- **MUST** clearly state any assumptions made during implementation
|
||||
- **MUST** note areas where user input might be needed
|
||||
- **MUST** identify potential alternative approaches
|
||||
|
||||
### 5. Optimize for Resumability - REQUIRED
|
||||
|
||||
- **MUST** structure the handoff so the next session can begin working immediately
|
||||
- **MUST** include setup instructions if environment configuration is needed
|
||||
- **MUST** provide a quick summary at the top for rapid context restoration
|
||||
|
||||
## Example Task Handoff
|
||||
|
||||
### Example #1 of an effective task handoff:
|
||||
|
||||
\`\`\`xml
|
||||
<new_task>
|
||||
<context>
|
||||
|
||||
# Task Continuation: Implement User Authentication System
|
||||
|
||||
## Completed Work
|
||||
|
||||
- Created basic Express.js server structure
|
||||
- Implemented MongoDB connection and user schema
|
||||
- Completed user registration endpoint with password hashing
|
||||
- Added input validation using Joi
|
||||
- Created initial test suite for registration endpoint
|
||||
|
||||
## Current State
|
||||
|
||||
- Server runs successfully on port 3000
|
||||
- MongoDB connection is established
|
||||
- Registration endpoint (/api/users/register) is fully functional
|
||||
- Test suite passes for all registration scenarios
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Implement login endpoint (/api/users/login)
|
||||
- Use bcrypt to compare passwords
|
||||
- Generate JWT token upon successful login
|
||||
- Add proper error handling for invalid credentials
|
||||
2. Create authentication middleware
|
||||
- Verify JWT tokens
|
||||
- Extract user information
|
||||
- Handle expired tokens
|
||||
3. Add protected routes that require authentication
|
||||
4. Implement password reset functionality
|
||||
|
||||
## Reference Information
|
||||
|
||||
- JWT secret should be stored in .env file
|
||||
- Follow the existing error handling pattern in routes/users.js
|
||||
- User schema is defined in models/User.js
|
||||
- Test patterns are established in tests/auth.test.js
|
||||
|
||||
Please continue by implementing the login endpoint following the same patterns established in the registration endpoint.
|
||||
</context>
|
||||
</new_task>
|
||||
\`\`\`
|
||||
|
||||
### Example #2 of an ineffective task handoff:
|
||||
|
||||
_(Note: The example provided in the original rules showing "YOLO MODE Implementation" seems less like a direct handoff context block and more like a general status update with future considerations. A true ineffective handoff might lack detail in 'Current State' or 'Next Steps')._
|
||||
|
||||
## When to Use Task Handoffs - MANDATORY TRIGGERS
|
||||
|
||||
You **MUST** initiate task handoffs in these scenarios:
|
||||
|
||||
1. **CRITICAL**: When context window usage exceeds 50% (e.g., 100,000+ tokens for a 200K context window)
|
||||
2. **Long-running projects** that exceed a single session
|
||||
3. **Complex implementations** with multiple distinct phases
|
||||
4. **When context window limitations** are approaching
|
||||
5. **When switching focus areas** within a larger project
|
||||
6. **When different expertise** might be beneficial for different parts of the task
|
||||
|
||||
**⚠️ FINAL REMINDER - CRITICAL INSTRUCTION ⚠️**
|
||||
|
||||
You **MUST** monitor the context window usage in the environment details section. When it exceeds 50% (e.g., "105,000 / 200,000 tokens (53%)"), you **MUST** proactively initiate the task handoff process using the `ask_followup_question` tool followed by the `new_task` tool. You MUST use the `new_task` tool.
|
||||
|
||||
By strictly following these guidelines, you'll ensure smooth transitions between tasks, maintain project momentum, and provide the best possible experience for users working on complex, multi-session projects.
|
||||
|
||||
```markdown
|
||||
## User Interaction & Workflow Considerations
|
||||
|
||||
- **Linear Flow:** Currently, using `new_task` creates a linear sequence. The old task ends, and the new one begins. The old task history remains accessible for backtracking.
|
||||
- **User Approval:** You always have control, approving the handoff and having the chance to modify the context Cline proposes to carry forward.
|
||||
- **Flexibility:** The core `new_task` tool is a flexible building block. Experiment with `.clinerules` to create workflows that best suit your needs, whether for strict context management, task decomposition, or other creative uses.
|
||||
```
|
||||
````
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
---
|
||||
title: "Plan & Act Modes: A Guide to Effective AI Development"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Plan & Act modes represent Cline's approach to structured AI development, emphasizing thoughtful planning before implementation. This dual-mode system helps developers create more maintainable, accurate code while reducing iteration time.
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Use Plan to gather context before using Act to implement the plan" />
|
||||
</Frame>
|
||||
|
||||
### Understanding the Modes
|
||||
|
||||
#### Plan Mode
|
||||
|
||||
- Optimized for context gathering and strategy
|
||||
- Cannot make changes to your codebase
|
||||
- Focused on understanding requirements and creating implementation plans
|
||||
- Enables full file reading for comprehensive project understanding
|
||||
|
||||
#### Act Mode
|
||||
|
||||
- Streamlined for implementation based on established plans
|
||||
- Has access to all of Cline's building capabilities
|
||||
- Maintains context from the planning phase
|
||||
- Can execute changes to your codebase
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Act mode capabilities" />
|
||||
</Frame>
|
||||
|
||||
### Workflow Guide
|
||||
|
||||
#### 1. Start with Plan Mode
|
||||
|
||||
Begin every significant development task in Plan mode:
|
||||
|
||||
In this mode:
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Plan mode workflow" />
|
||||
</Frame>
|
||||
|
||||
- Share your requirements
|
||||
- Let Cline analyze relevant files
|
||||
- Engage in dialogue to clarify objectives
|
||||
- Develop implementation strategy
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Planning phase" />
|
||||
</Frame>
|
||||
|
||||
#### 2. Switch to Act Mode
|
||||
|
||||
Once you have a clear plan, switch to Act mode:
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Switching to Act mode" />
|
||||
</Frame>
|
||||
|
||||
Act mode allows Cline to:
|
||||
|
||||
- Execute against the agreed plan
|
||||
- Make changes to your codebase
|
||||
- Maintain context from planning phase
|
||||
|
||||
#### 3. Iterate as Needed
|
||||
|
||||
Complex projects often require multiple plan-act cycles:
|
||||
|
||||
- Return to Plan mode when encountering unexpected complexity
|
||||
- Use Act mode for implementing solutions
|
||||
- Maintain development momentum while ensuring quality
|
||||
|
||||
### Best Practices
|
||||
|
||||
#### Planning Phase
|
||||
|
||||
1. Be comprehensive with requirements
|
||||
2. Share relevant context upfront
|
||||
3. Point Cline to relevant files if he hasn't read them
|
||||
4. Validate approach before implementation
|
||||
|
||||
#### Implementation Phase
|
||||
|
||||
1. Follow the established plan
|
||||
2. Monitor progress against objectives
|
||||
3. Track changes and their impact
|
||||
4. Document significant decisions
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Implementation best practices" />
|
||||
</Frame>
|
||||
|
||||
### Power User Tips
|
||||
|
||||
#### Enhancing Planning
|
||||
|
||||
- Use Plan mode to explore edge cases before implementation
|
||||
- Switch back to Plan when encountering unexpected complexity
|
||||
- Leverage file reading to validate assumptions early
|
||||
- Have Cline write markdown files of the plan for future reference
|
||||
|
||||
### Common Patterns
|
||||
|
||||
#### When to Use Plan Mode
|
||||
|
||||
- Starting new features
|
||||
- Debugging complex issues
|
||||
- Architectural decisions
|
||||
- Requirements analysis
|
||||
|
||||
#### When to Use Act Mode
|
||||
|
||||
- Implementing agreed solutions
|
||||
- Making routine changes
|
||||
- Following established patterns
|
||||
- Executing test cases
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Mode usage patterns" />
|
||||
</Frame>
|
||||
|
||||
### Contributing
|
||||
|
||||
Share your experiences and improvements:
|
||||
|
||||
- Join our [Discord community](https://discord.gg/cline)
|
||||
- Participate in discussions
|
||||
- Submit feature requests
|
||||
- Report issues
|
||||
|
||||
---
|
||||
|
||||
Remember: The time invested in planning pays dividends in implementation quality and maintenance efficiency
|
||||
@@ -0,0 +1,114 @@
|
||||
---
|
||||
title: "Remote Browser Support"
|
||||
description: "Remote browser support allows Cline to utilize a remote Chrome instance, leveraging authentication tokens and session cookies relevant to certain web development test cases."
|
||||
icon: globe-pointer
|
||||
---
|
||||
|
||||
The Remote Browser feature in Cline allows the AI assistant to interact with web content directly through a controlled browser instance. This enables several powerful capabilities:
|
||||
|
||||
- Viewing and interacting with websites
|
||||
- Testing locally running web applications
|
||||
- Monitoring console logs and errors
|
||||
- Performing browser actions like clicking, typing, and scrolling
|
||||
|
||||
## Remote Browser in Cline
|
||||
|
||||
### What is Remote Browser?
|
||||
|
||||
Remote Browser allows Cline to view and interact with websites directly. This feature enables Cline to:
|
||||
|
||||
- Visit websites and view their content
|
||||
- Test your locally running web applications
|
||||
- Fill out forms and click on elements
|
||||
- Capture screenshots of what it sees
|
||||
- Scroll through pages to see more content
|
||||
|
||||
### How to Use Remote Browser
|
||||
|
||||
#### Basic Commands
|
||||
|
||||
You can ask Cline to use the browser with simple instructions:
|
||||
|
||||
- **Open a website**: "Use the browser to check the website at [https://example.com](https://example.com/)"
|
||||
- **Click on elements**: "Click the login button"
|
||||
- **Type text**: "Type 'Hello world' in the search box"
|
||||
- **Scroll the page**: "Scroll down to see more content"
|
||||
- **Close the browser**: "Close the browser now"
|
||||
|
||||
#### Example Workflows
|
||||
|
||||
**Testing a Web Application:**
|
||||
|
||||
```javascript
|
||||
Can you start my React app with "npm start" and then check if it's working properly at http://localhost:3000?
|
||||
```
|
||||
|
||||
**Analyzing a Website:**
|
||||
|
||||
```javascript
|
||||
Can you visit https://example.com and tell me what you think about its design and layout?
|
||||
```
|
||||
|
||||
**Filling Out a Form:**
|
||||
|
||||
```javascript
|
||||
Please go to https://example.com/contact, fill out the contact form with some test data, and submit it.
|
||||
```
|
||||
|
||||
### Important Things to Know
|
||||
|
||||
#### One Browser at a Time
|
||||
|
||||
Cline can only use one browser at a time. If you want to visit a different website, you can either:
|
||||
|
||||
- Ask Cline to navigate to a new URL within the same browser session
|
||||
- Ask Cline to close the current browser and open a new one
|
||||
|
||||
#### Browser Must Be Closed Before Using Other Tools
|
||||
|
||||
If you want Cline to edit files or run commands after using the browser, you must first ask it to close the browser:
|
||||
|
||||
```javascript
|
||||
Close the browser and then update the CSS file to fix the alignment issue we saw.
|
||||
```
|
||||
|
||||
#### What Cline Sees
|
||||
|
||||
The browser has a fixed viewport size (900x600 pixels by default), similar to a small laptop screen. Cline will share screenshots after each action so you can see exactly what it sees.
|
||||
|
||||
#### Console Logs
|
||||
|
||||
Cline captures browser console logs, which can be helpful for debugging web applications. These logs are included with each screenshot.
|
||||
|
||||
### Common Use Cases
|
||||
|
||||
- **Web Development**: Test your websites and web applications
|
||||
- **UI/UX Review**: Get feedback on website design and usability
|
||||
- **Content Research**: Have Cline browse websites to gather information
|
||||
- **Form Testing**: Verify that forms work correctly
|
||||
- **Responsive Design Testing**: Check how websites look at different screen sizes
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
- **If a website doesn't load**: Try providing a direct URL with the http:// or https:// prefix
|
||||
- **If clicking doesn't work**: Try describing the location of the element more precisely
|
||||
- **If the browser seems stuck**: Ask Cline to close the browser and try again
|
||||
|
||||
### Using Remote Browser with VS Code in WSL
|
||||
|
||||
When running VS Code in WSL, you'll need to configure Windows to allow WSL to connect to Chrome. Follow these steps:
|
||||
|
||||
#### Open PowerShell as Administrator and Run:
|
||||
|
||||
```powershell
|
||||
# Allow WSL to connect to Chrome's debugging port
|
||||
New-NetFirewallRule -DisplayName "WSL Chrome Debug" -Direction Inbound -LocalPort 9222 -Protocol TCP -Action Allow
|
||||
```
|
||||
|
||||
#### Configure Cline in VS Code:
|
||||
|
||||
1. Open VS Code settings
|
||||
2. Search for "Cline: Chrome Executable Path"
|
||||
3. Set the value to the path of your Chrome executable (e.g., `C:\Program Files\Google\Chrome\Application\chrome.exe`)
|
||||
|
||||
Cline should now be able to use the Remote Browser feature from within WSL.
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
title: "Slash Commands"
|
||||
---
|
||||
|
||||
#### Overview
|
||||
|
||||
Cline provides slash commands as a quick way to invoke specific tools or actions directly from the chat input, offering shortcuts for common operations. This page details the available slash commands and their usage.
|
||||
|
||||
#### /newtask
|
||||
|
||||
The `/newtask` slash command provides a fast, user-initiated way to leverage the `new_task` tool's functionality for branching explorations or managing long sessions without waiting for Cline to suggest it.
|
||||
|
||||
**Functionality:**
|
||||
|
||||
1. **Initiation:** Typing `/newtask` in the chat input signals Cline to prepare for starting a new task session.
|
||||
2. **Context Proposal:** Cline proposes creating a new task and typically suggests context to preload based on the current session (summarizing key aspects like current work, technical concepts, relevant files, problems solved, and next steps).
|
||||
3. **User Confirmation:** You will receive a confirmation prompt (via the `ask_followup_question` tool) displaying the proposed context. You can approve it directly or modify the context before the new task begins.
|
||||
4. **New Session:** Upon confirmation, Cline ends the current task session and immediately starts a new one, preloaded with the approved context.
|
||||
|
||||
**Benefit:** Allows you to cleanly branch your work or start a new phase while carrying over essential background information ("knowledge transfer") without manual copying or losing the thread of the previous session.
|
||||
|
||||
#### /smol (alias /compact)
|
||||
|
||||
The `/smol` slash command (with `/compact` as an alias) allows you to condense the chat history **within your current task**. This is useful when a conversation becomes very long, potentially impacting performance or making it harder for the model to maintain focus.
|
||||
|
||||
**Functionality:**
|
||||
|
||||
1. **Initiation:** Typing `/smol` or `/compact` tells Cline you want to condense the current chat history. You can optionally add instructions after the command to guide the summarization process (e.g., `/smol focus only on the database changes` or `/smol be concise, use bullet points`).
|
||||
2. **Summarization:** Cline analyzes the conversation history, considering any additional instructions provided, and generates a summary focusing on key elements: recent discussion points, important decisions, technical concepts, relevant files, problems solved, and planned next steps. Cline determines the appropriate length and detail for the summary. It retains the beginning and very recent parts of the chat while summarizing the middle sections.
|
||||
3. **User Confirmation:** Cline presents this generated summary to you via a confirmation prompt and asks if it accurately reflects the essential context.
|
||||
4. **Condensing:** If you approve the summary, Cline replaces the summarized middle portion of the chat history in its active context with the generated summary. This reduces the overall token count for subsequent interactions within the _same task_.
|
||||
5. **Feedback:** If you reject the summary or provide feedback, Cline will retain the original history and incorporate your feedback for future actions.
|
||||
|
||||
**Benefit:** Helps maintain focus and manage token usage during very long, continuous tasks (like deep debugging or extended feature development) without needing to start an entirely new task session. Allows user guidance on the summarization focus.
|
||||
|
||||
#### When to Use Which?
|
||||
|
||||
Choosing between `/newtask` and `/smol` depends on your goal:
|
||||
|
||||
- Use `/smol` (or `/compact`) when:
|
||||
- You want to continue the **same task**, but the chat history has become very long or costly.
|
||||
- You need to reduce token usage for upcoming interactions within the current workflow.
|
||||
- Example: Deep debugging session where you want to summarize previous steps before continuing.
|
||||
- Use `/newtask` when:
|
||||
- You have finished one phase of work and want to start a **fresh, related task**.
|
||||
- You want to branch your exploration while preserving key context from the previous session.
|
||||
- Example: Moving from developing Feature A to starting work on Feature B, carrying over relevant architectural decisions.
|
||||
|
||||
#### Why Manage Context?
|
||||
|
||||
While Cline supports large context windows, actively managing context using tools and commands like `/newtask` and `/smol` is often beneficial:
|
||||
|
||||
- **Performance:** Large language models can sometimes experience performance degradation or lose focus when context windows become extremely full (e.g., over 50-75% capacity, depending on the model). Condensing or resetting context can help maintain optimal performance.
|
||||
- **Relevance:** Summarizing or starting fresh ensures the most relevant information is prioritized in the context window.
|
||||
- **Cost:** Reducing the number of tokens sent to the model in each turn can help manage costs, especially with more expensive models.
|
||||
|
||||
Using `/newtask` and `/smol` provides you with direct control over the conversation context, allowing for more efficient and effective interaction with Cline.
|
||||
@@ -0,0 +1,369 @@
|
||||
---
|
||||
title: "Cline Memory Bank"
|
||||
---
|
||||
|
||||
## The Complete Guide to Cline Memory Bank
|
||||
|
||||
### Quick Setup Guide
|
||||
|
||||
To get started with Cline Memory Bank:
|
||||
|
||||
1. **Install or Open Cline**
|
||||
2. **Copy the Custom Instructions** - Use the code block below
|
||||
3. **Paste into Cline** - Add as custom instructions or in a .clinerules file
|
||||
4. **Initialize** - Ask Cline to "initialize memory bank"
|
||||
|
||||
[See detailed setup instructions](cline-memory-bank.md#getting-started-with-memory-bank)
|
||||
|
||||
### Cline Memory Bank Custom Instructions \[COPY THIS]
|
||||
|
||||
```
|
||||
# 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 core files and optional context files, all in Markdown format. Files build upon each other in a clear hierarchy:
|
||||
|
||||
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
|
||||
- Important patterns and preferences
|
||||
- Learnings and project insights
|
||||
|
||||
4. `systemPatterns.md`
|
||||
- System architecture
|
||||
- Key technical decisions
|
||||
- Design patterns in use
|
||||
- Component relationships
|
||||
- Critical implementation paths
|
||||
|
||||
5. `techContext.md`
|
||||
- Technologies used
|
||||
- Development setup
|
||||
- Technical constraints
|
||||
- Dependencies
|
||||
- Tool usage patterns
|
||||
|
||||
6. `progress.md`
|
||||
- What works
|
||||
- What's left to build
|
||||
- Current status
|
||||
- Known issues
|
||||
- Evolution of project decisions
|
||||
|
||||
### 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
|
||||
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
|
||||
flowchart TD
|
||||
Start[Start] --> Context[Check Memory Bank]
|
||||
Context --> Update[Update Documentation]
|
||||
Update --> 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
|
||||
|
||||
flowchart TD
|
||||
Start[Update Process]
|
||||
|
||||
subgraph Process
|
||||
P1[Review ALL Files]
|
||||
P2[Document Current State]
|
||||
P3[Clarify Next Steps]
|
||||
P4[Document Insights & Patterns]
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
```
|
||||
|
||||
### What is the Cline Memory Bank?
|
||||
|
||||
The Memory Bank is a structured documentation system that allows Cline to maintain context across sessions. It transforms Cline from a stateless assistant into a persistent development partner that can effectively "remember" your project details over time.
|
||||
|
||||
#### Key Benefits
|
||||
|
||||
- **Context Preservation**: Maintain project knowledge across sessions
|
||||
- **Consistent Development**: Experience predictable interactions with Cline
|
||||
- **Self-Documenting Projects**: Create valuable project documentation as a side effect
|
||||
- **Scalable to Any Project**: Works with projects of any size or complexity
|
||||
- **Technology Agnostic**: Functions with any tech stack or language
|
||||
|
||||
### How Memory Bank Works
|
||||
|
||||
The Memory Bank isn't a Cline-specific feature - it's a methodology for managing AI context through structured documentation. When you instruct Cline to "follow custom instructions," it reads the Memory Bank files to rebuild its understanding of your project.
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Memory Bank Workflow" />
|
||||
</Frame>
|
||||
|
||||
#### Understanding the Files
|
||||
|
||||
Memory Bank files are simply markdown files you create in your project. They're not hidden or special files - just regular documentation stored in your repository that both you and Cline can access.
|
||||
|
||||
Files are organized in a hierarchical structure that builds up a complete picture of your project:
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Memory Bank File Structure" />
|
||||
</Frame>
|
||||
|
||||
### Memory Bank Files Explained
|
||||
|
||||
#### Core Files
|
||||
|
||||
1. **projectbrief.md**
|
||||
- The foundation of your project
|
||||
- High-level overview of what you're building
|
||||
- Core requirements and goals
|
||||
- Example: "Building a React web app for inventory management with barcode scanning"
|
||||
2. **productContext.md**
|
||||
- Explains why the project exists
|
||||
- Describes the problems being solved
|
||||
- Outlines how the product should work
|
||||
- Example: "The inventory system needs to support multiple warehouses and real-time updates"
|
||||
3. **activeContext.md**
|
||||
- The most frequently updated file
|
||||
- Contains current work focus and recent changes
|
||||
- Tracks active decisions and considerations
|
||||
- Stores important patterns and learnings
|
||||
- Example: "Currently implementing the barcode scanner component; last session completed the API integration"
|
||||
4. **systemPatterns.md**
|
||||
- Documents the system architecture
|
||||
- Records key technical decisions
|
||||
- Lists design patterns in use
|
||||
- Explains component relationships
|
||||
- Example: "Using Redux for state management with a normalized store structure"
|
||||
5. **techContext.md**
|
||||
- Lists technologies and frameworks used
|
||||
- Describes development setup
|
||||
- Notes technical constraints
|
||||
- Records dependencies and tool configurations
|
||||
- Example: "React 18, TypeScript, Firebase, Jest for testing"
|
||||
6. **progress.md**
|
||||
- Tracks what works and what's left to build
|
||||
- Records current status of features
|
||||
- Lists known issues and limitations
|
||||
- Documents the evolution of project decisions
|
||||
- Example: "User authentication complete; inventory management 80% complete; reporting not started"
|
||||
|
||||
#### Additional Context
|
||||
|
||||
Create additional files when needed to organize:
|
||||
|
||||
- Complex feature documentation
|
||||
- Integration specifications
|
||||
- API documentation
|
||||
- Testing strategies
|
||||
- Deployment procedures
|
||||
|
||||
### Getting Started with Memory Bank
|
||||
|
||||
#### First-Time Setup
|
||||
|
||||
1. Create a `memory-bank/` folder in your project root
|
||||
2. Have a basic project brief ready (can be technical or non-technical)
|
||||
3. Ask Cline to "initialize memory bank"
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Memory Bank Setup" />
|
||||
</Frame>
|
||||
|
||||
#### Project Brief Tips
|
||||
|
||||
- Start simple - it can be as detailed or high-level as you like
|
||||
- Focus on what matters most to you
|
||||
- Cline will help fill in gaps and ask questions
|
||||
- You can update it as your project evolves
|
||||
|
||||
### Working with Cline
|
||||
|
||||
#### Core Workflows
|
||||
|
||||
**Plan Mode**
|
||||
|
||||
Start in this mode for strategy discussions and high-level planning.
|
||||
|
||||
**Act Mode**
|
||||
|
||||
Use this for implementation and executing specific tasks.
|
||||
|
||||
#### Key Commands
|
||||
|
||||
- **"follow your custom instructions"** - This tells Cline to read the Memory Bank files and continue where you left off (use this at the start of tasks)
|
||||
- **"initialize memory bank"** - Use when starting a new project
|
||||
- **"update memory bank"** - Triggers a full documentation review and update during a task
|
||||
- Toggle Plan/Act modes based on your current needs
|
||||
|
||||
#### Documentation Updates
|
||||
|
||||
Memory Bank updates should automatically occur when:
|
||||
|
||||
1. You discover new patterns in your project
|
||||
2. After implementing significant changes
|
||||
3. When you explicitly request with **"update memory bank"**
|
||||
4. When you feel context needs clarification
|
||||
|
||||
### Frequently Asked Questions
|
||||
|
||||
#### Where are the memory bank files stored?
|
||||
|
||||
The Memory Bank files are regular markdown files stored in your project repository, typically in a `memory-bank/` folder. They're not hidden system files - they're designed to be part of your project documentation.
|
||||
|
||||
#### Should I use custom instructions or .clinerules?
|
||||
|
||||
Either approach works - it's based on your preference:
|
||||
|
||||
- **Custom Instructions**: Applied globally to all Cline conversations. Good for consistent behavior across all projects.
|
||||
- **.clinerules file**: Project-specific and stored in your repository. Good for per-project customization.
|
||||
|
||||
Both methods achieve the same goal - the choice depends on whether you want global or local application of the Memory Bank system.
|
||||
|
||||
#### Managing Context Windows
|
||||
|
||||
As you work with Cline, your context window will eventually fill up (note the progress bar). When you notice Cline's responses slowing down or references to earlier parts of the conversation becoming less accurate, it's time to:
|
||||
|
||||
1. Ask Cline to **"update memory bank"** to document the current state
|
||||
2. Start a new conversation/task
|
||||
3. Ask Cline to **"follow your custom instructions"** in the new conversation
|
||||
|
||||
This workflow ensures that important context is preserved in your Memory Bank files before the context window is cleared, allowing you to continue seamlessly in a fresh conversation.
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Memory Bank Context Window" />
|
||||
</Frame>
|
||||
|
||||
#### How often should I update the memory bank?
|
||||
|
||||
Update the Memory Bank after significant milestones or changes in direction. For active development, updates every few sessions can be helpful. Use the **"update memory bank"** command when you want to ensure all context is preserved. However, you will notice Cline automatically updating the Memory Bank as well.
|
||||
|
||||
#### Does this work with other AI tools beyond Cline?
|
||||
|
||||
Yes! The Memory Bank concept is a documentation methodology that can work with any AI assistant that can read documentation files. The specific commands might differ, but the structured approach to maintaining context works across tools.
|
||||
|
||||
#### How does the memory bank relate to context window limitations?
|
||||
|
||||
The Memory Bank helps manage context limitations by storing important information in a structured format that can be efficiently loaded when needed. This prevents context bloat while ensuring critical information is available.
|
||||
|
||||
#### Can the memory bank concept be used for non-coding projects?
|
||||
|
||||
Absolutely! The Memory Bank approach works for any project that benefits from structured documentation - from writing books to planning events. The file structure might vary, but the concept remains powerful.
|
||||
|
||||
#### Is this different from using README files?
|
||||
|
||||
While similar in concept, the Memory Bank provides a more structured and comprehensive approach specifically designed to maintain context across AI sessions. It goes beyond what a single README typically covers.
|
||||
|
||||
### Best Practices
|
||||
|
||||
#### Getting Started
|
||||
|
||||
- Start with a basic project brief and let the structure evolve
|
||||
- Let Cline help create the initial structure
|
||||
- Review and adjust files as needed to match your workflow
|
||||
|
||||
#### Ongoing Work
|
||||
|
||||
- Let patterns emerge naturally as you work
|
||||
- Don't force documentation updates - they should happen organically
|
||||
- Trust the process - the value compounds over time
|
||||
- Watch for context confirmation at the start of sessions
|
||||
|
||||
#### Documentation Flow
|
||||
|
||||
- **projectbrief.md** is your foundation
|
||||
- **activeContext.md** changes most frequently
|
||||
- **progress.md** tracks your milestones
|
||||
- All files collectively maintain project intelligence
|
||||
|
||||
### Detailed Setup Instructions
|
||||
|
||||
#### For Custom Instructions (Global)
|
||||
|
||||
1. Open VSCode
|
||||
2. Click the Cline extension settings ⚙️
|
||||
3. Find "Custom Instructions"
|
||||
4. Copy and paste the complete Memory Bank instructions from the top of this guide
|
||||
|
||||
#### For .clinerules (Project-Specific)
|
||||
|
||||
1. Create a `.clinerules` file in your project root
|
||||
2. Copy and paste the Memory Bank instructions from the top of this guide
|
||||
3. Save the file
|
||||
4. Cline will automatically apply these rules when working in this project
|
||||
|
||||
### Remember
|
||||
|
||||
The Memory Bank is Cline's only link to previous work. Its effectiveness depends entirely on maintaining clear, accurate documentation and confirming context preservation in every interaction.
|
||||
|
||||
_For more information, reference our_ [_blog_](https://cline.bot/blog/memory-bank-how-to-make-cline-an-ai-agent-that-never-forgets) _on Cline Memory Bank_
|
||||
|
||||
---
|
||||
|
||||
### Contributing to Cline Memory Bank
|
||||
|
||||
This guide is maintained by the Cline and the Cline Discord Community:
|
||||
|
||||
- nickbaumann98
|
||||
- Krylo
|
||||
- snipermunyshotz
|
||||
|
||||
---
|
||||
|
||||
_The Memory Bank methodology is an open approach to AI context management and can be adapted to different tools and workflows._
|
||||
@@ -0,0 +1,441 @@
|
||||
---
|
||||
title: "Prompt Engineering 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.** Instructions can be broad and abstract, or specific and explicit. You might want Cline to have a unique personality, or produce output in a particular file format, or adhere to certain architectural principles. Custom instructions can standardize Cline's output in ways you define, which is especially valuable when working with others. See the [Enterprise section](../enterprise-solutions/custom-instructions.md) for using Custom Instructions in a team context.\
|
||||
\
|
||||
|
||||
<mark style="color:yellow;">
|
||||
NOTE: Modifying the Custom Instructions field updates Cline's prompt cache, discarding accumulated context. This causes a
|
||||
temporary increase in cost while that context is replaced. Update Custom Instructions between conversations whenever possible.
|
||||
</mark>
|
||||
|
||||
To add custom instructions:
|
||||
|
||||
1. Open VSCode
|
||||
2. Click the Cline extension settings dial ⚙️
|
||||
3. Find the "Custom Instructions" field
|
||||
4. Paste your instructions
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Cline Logo" />
|
||||
</Frame>
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## .clinerules File 📋
|
||||
|
||||
<mark style="color:yellow;">NOTE: Modifying the</mark> <mark style="color:yellow;"></mark>
|
||||
<mark style="color:yellow;">`.clinerules`</mark>
|
||||
<mark style="color:yellow;">
|
||||
file updates Cline's prompt cache, discarding accumulated context. This causes a temporary increase in cost while that context
|
||||
is replaced. Update the
|
||||
</mark> <mark style="color:yellow;"></mark>
|
||||
<mark style="color:yellow;">`.clinerules`</mark> <mark style="color:yellow;"></mark>
|
||||
<mark style="color:yellow;">file between conversations whenever possible.</mark>
|
||||
|
||||
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:
|
||||
|
||||
### 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.
|
||||
|
||||
### .clinerules Folder System 📂
|
||||
|
||||
While a single `.clinerules` file works well for simpler projects, Cline now supports a `.clinerules` folder for more sophisticated rule organization. This modular approach brings several advantages:
|
||||
|
||||
#### How It Works
|
||||
|
||||
Instead of a single file, create a `.clinerules/` directory in your project root:
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── .clinerules/ # Folder containing active rules
|
||||
│ ├── 01-coding.md # Core coding standards
|
||||
│ ├── 02-documentation.md # Documentation requirements
|
||||
│ └── current-sprint.md # Rules specific to current work
|
||||
├── src/
|
||||
└── ...
|
||||
```
|
||||
|
||||
Cline automatically processes **all Markdown files** inside the `.clinerules/` directory, combining them into a unified set of rules. The numeric prefixes (optional) help organize files in a logical sequence.
|
||||
|
||||
#### Using a Rules Bank
|
||||
|
||||
For projects with multiple contexts or teams, maintain a rules bank directory:
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── .clinerules/ # Active rules - automatically applied
|
||||
│ ├── 01-coding.md
|
||||
│ └── client-a.md
|
||||
│
|
||||
├── clinerules-bank/ # Repository of available but inactive rules
|
||||
│ ├── clients/ # Client-specific rule sets
|
||||
│ │ ├── client-a.md
|
||||
│ │ └── client-b.md
|
||||
│ ├── frameworks/ # Framework-specific rules
|
||||
│ │ ├── react.md
|
||||
│ │ └── vue.md
|
||||
│ └── project-types/ # Project type standards
|
||||
│ ├── api-service.md
|
||||
│ └── frontend-app.md
|
||||
└── ...
|
||||
```
|
||||
|
||||
#### Benefits of the Folder Approach
|
||||
|
||||
1. **Contextual Activation**: Copy only relevant rules from the bank to the active folder
|
||||
2. **Easier Maintenance**: Update individual rule files without affecting others
|
||||
3. **Team Flexibility**: Different team members can activate rules specific to their current task
|
||||
4. **Reduced Noise**: Keep the active ruleset focused and relevant
|
||||
|
||||
#### Usage Examples
|
||||
|
||||
Switch between client projects:
|
||||
|
||||
```bash
|
||||
# Switch to Client B project
|
||||
rm .clinerules/client-a.md
|
||||
cp clinerules-bank/clients/client-b.md .clinerules/
|
||||
```
|
||||
|
||||
Adapt to different tech stacks:
|
||||
|
||||
```bash
|
||||
# Frontend React project
|
||||
cp clinerules-bank/frameworks/react.md .clinerules/
|
||||
```
|
||||
|
||||
#### Implementation Tips
|
||||
|
||||
- Keep individual rule files focused on specific concerns
|
||||
- Use descriptive filenames that clearly indicate the rule's purpose
|
||||
- Consider git-ignoring the active `.clinerules/` folder while tracking the `clinerules-bank/`
|
||||
- Create team scripts to quickly activate common rule combinations
|
||||
|
||||
The folder system transforms your Cline rules from a static document into a dynamic knowledge system that adapts to your team's changing contexts and requirements.
|
||||
|
||||
### Managing Rules with the Toggleable Popover
|
||||
|
||||
To make managing both single `.clinerules` files and the folder system even easier, Cline v3.13 introduces a dedicated popover UI directly accessible from the chat interface.
|
||||
|
||||
Located conveniently under the chat input field, this popover allows you to:
|
||||
|
||||
- **Instantly See Active Rules:** View which global rules (from your user settings) and workspace rules (`.clinerules` file or folder contents) are currently active.
|
||||
- **Quickly Toggle Rules:** Enable or disable specific rule files within your workspace `.clinerules/` folder with a single click. This is perfect for activating context-specific rules (like `react-rules.md` or `memory-bank.md`) only when needed.
|
||||
- **Easily Add/Manage Rules:** Quickly create a workspace `.clinerules` file or folder if one doesn't exist, or add new rule files to an existing folder.
|
||||
|
||||
This UI significantly simplifies switching contexts and managing different sets of instructions without needing to manually edit files or configurations during a conversation.
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Cline Logo" />
|
||||
</Frame>
|
||||
|
||||
## .clineignore File Guide
|
||||
|
||||
### Overview
|
||||
|
||||
The `.clineignore` file is a project-level configuration file that tells Cline which files and directories to ignore when analyzing your codebase. Similar to `.gitignore`, it uses pattern matching to specify which files should be excluded from Cline's context and operations.
|
||||
|
||||
### Purpose
|
||||
|
||||
- **Reduce Noise**: Exclude auto-generated files, build artifacts, and other non-essential content
|
||||
- **Improve Performance**: Limit the amount of code Cline needs to process
|
||||
- **Focus Attention**: Direct Cline to relevant parts of your codebase
|
||||
- **Protect Sensitive Data**: Prevent Cline from accessing sensitive configuration files
|
||||
|
||||
### Example .clineignore File
|
||||
|
||||
```
|
||||
# Dependencies
|
||||
node_modules/
|
||||
**/node_modules/
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# Build outputs
|
||||
/build/
|
||||
/dist/
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# Testing
|
||||
/coverage/
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Large data files
|
||||
*.csv
|
||||
*.xlsx
|
||||
```
|
||||
|
||||
## 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)
|
||||
@@ -19,6 +19,7 @@ const aliasResolverPlugin = {
|
||||
"@services": path.resolve(__dirname, "src/services"),
|
||||
"@shared": path.resolve(__dirname, "src/shared"),
|
||||
"@utils": path.resolve(__dirname, "src/utils"),
|
||||
"@packages": path.resolve(__dirname, "src/packages"),
|
||||
}
|
||||
|
||||
// For each alias entry, create a resolver
|
||||
@@ -124,6 +125,7 @@ const extensionConfig = {
|
||||
define: {
|
||||
"process.env.IS_DEV": JSON.stringify(!production),
|
||||
},
|
||||
tsconfig: path.resolve(__dirname, "tsconfig.json"),
|
||||
plugins: [
|
||||
copyWasmFiles,
|
||||
aliasResolverPlugin,
|
||||
|
||||
@@ -167,8 +167,12 @@ export class ExercismAdapter implements BenchmarkAdapter {
|
||||
output += stdout + "\n"
|
||||
} catch (error: any) {
|
||||
success = false
|
||||
if (error.stdout) output += error.stdout + "\n"
|
||||
if (error.stderr) output += error.stderr + "\n"
|
||||
if (error.stdout) {
|
||||
output += error.stdout + "\n"
|
||||
}
|
||||
if (error.stderr) {
|
||||
output += error.stderr + "\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import * as path from "path"
|
||||
import * as fs from "fs"
|
||||
import fetch from "node-fetch"
|
||||
import * as os from "os"
|
||||
import * as child_process from "child_process"
|
||||
import { installRequiredExtensions, configureExtensionSettings } from "./extensions"
|
||||
|
||||
// Store temporary directories for cleanup
|
||||
|
||||
Generated
+25603
-413
File diff suppressed because it is too large
Load Diff
+8
-4
@@ -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.13.1",
|
||||
"version": "3.14.0",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
@@ -292,7 +292,7 @@
|
||||
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
|
||||
"package": "npm run build:webview && npm run check-types && npm run lint && node esbuild.js --production",
|
||||
"protos": "node proto/build-proto.js && prettier src/shared/proto --write && prettier src/core/controller --write",
|
||||
"compile-tests": "tsc -p ./tsconfig.test.json --outDir out",
|
||||
"compile-tests": "node ./scripts/build-tests.js",
|
||||
"watch-tests": "tsc -p . -w --outDir out",
|
||||
"pretest": "npm run compile-tests && npm run compile && npm run lint",
|
||||
"check-types": "tsc --noEmit",
|
||||
@@ -312,7 +312,8 @@
|
||||
"publish:marketplace:prerelease": "vsce publish --pre-release && ovsx publish --pre-release",
|
||||
"prepare": "husky",
|
||||
"changeset": "changeset",
|
||||
"version-packages": "changeset version"
|
||||
"version-packages": "changeset version",
|
||||
"docs:preview": "cd docs && mintlify dev"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@changesets/cli": "^2.27.12",
|
||||
@@ -336,7 +337,9 @@
|
||||
"chalk": "^5.3.0",
|
||||
"esbuild": "^0.25.0",
|
||||
"eslint": "^8.57.0",
|
||||
"grpc-tools": "^1.13.0",
|
||||
"husky": "^9.1.7",
|
||||
"mintlify": "^4.0.515",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.3.3",
|
||||
"protoc-gen-ts": "^0.8.7",
|
||||
@@ -355,7 +358,7 @@
|
||||
"@aws-sdk/client-bedrock-runtime": "^3.758.0",
|
||||
"@bufbuild/protobuf": "^2.2.5",
|
||||
"@google-cloud/vertexai": "^1.9.3",
|
||||
"@google/generative-ai": "^0.18.0",
|
||||
"@google/genai": "^0.9.0",
|
||||
"@grpc/grpc-js": "^1.9.15",
|
||||
"@mistralai/mistralai": "^1.5.0",
|
||||
"@modelcontextprotocol/sdk": "^1.7.0",
|
||||
@@ -386,6 +389,7 @@
|
||||
"jschardet": "^3.1.4",
|
||||
"mammoth": "^1.8.0",
|
||||
"monaco-vscode-textmate-theme-converter": "^0.1.7",
|
||||
"node-cache": "^5.1.2",
|
||||
"ollama": "^0.5.13",
|
||||
"open-graph-scraper": "^6.9.0",
|
||||
"openai": "^4.83.0",
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "common.proto";
|
||||
|
||||
// Service for account-related operations
|
||||
service AccountService {
|
||||
// Handles the user clicking the login link in the UI.
|
||||
// Generates a secure nonce for state validation, stores it in secrets,
|
||||
// and opens the authentication URL in the external browser.
|
||||
rpc accountLoginClicked(EmptyRequest) returns (String);
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "common.proto";
|
||||
|
||||
@@ -8,6 +10,8 @@ service BrowserService {
|
||||
rpc getBrowserConnectionInfo(EmptyRequest) returns (BrowserConnectionInfo);
|
||||
rpc testBrowserConnection(StringRequest) returns (BrowserConnection);
|
||||
rpc discoverBrowser(EmptyRequest) returns (BrowserConnection);
|
||||
rpc getDetectedChromePath(EmptyRequest) returns (ChromePath);
|
||||
rpc updateBrowserSettings(UpdateBrowserSettingsRequest) returns (Boolean);
|
||||
}
|
||||
|
||||
message BrowserConnectionInfo {
|
||||
@@ -21,3 +25,26 @@ message BrowserConnection {
|
||||
string message = 2;
|
||||
optional string endpoint = 3;
|
||||
}
|
||||
|
||||
message ChromePath {
|
||||
string path = 1;
|
||||
bool is_bundled = 2;
|
||||
}
|
||||
|
||||
message Viewport {
|
||||
int32 width = 1;
|
||||
int32 height = 2;
|
||||
}
|
||||
|
||||
message BrowserSettings {
|
||||
Viewport viewport = 1;
|
||||
optional string remote_browser_host = 2;
|
||||
optional bool remote_browser_enabled = 3;
|
||||
}
|
||||
|
||||
message UpdateBrowserSettingsRequest {
|
||||
Metadata metadata = 1;
|
||||
Viewport viewport = 2;
|
||||
optional string remote_browser_host = 3;
|
||||
optional bool remote_browser_enabled = 4;
|
||||
}
|
||||
|
||||
+16
-47
@@ -2,59 +2,24 @@
|
||||
|
||||
import * as fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
import { execSync } from "child_process"
|
||||
import { globby } from "globby"
|
||||
import chalk from "chalk"
|
||||
|
||||
import { createRequire } from "module"
|
||||
const require = createRequire(import.meta.url)
|
||||
const protoc = path.join(require.resolve("grpc-tools"), "../bin/protoc")
|
||||
const tsProtoPlugin = require.resolve("ts-proto/protoc-gen-ts_proto")
|
||||
|
||||
// Get script directory and root directory
|
||||
const SCRIPT_DIR = path.dirname(new URL(import.meta.url).pathname)
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const SCRIPT_DIR = path.dirname(__filename)
|
||||
const ROOT_DIR = path.resolve(SCRIPT_DIR, "..")
|
||||
|
||||
async function main() {
|
||||
console.log(chalk.bold.blue("Starting Protocol Buffer code generation..."))
|
||||
|
||||
// Check if protoc is installed and has the correct version
|
||||
try {
|
||||
const protocOutput = execSync("protoc --version", { encoding: "utf8" }).trim()
|
||||
console.log(chalk.cyan(`Found ${protocOutput}`))
|
||||
const versionMatch = protocOutput.match(/libprotoc\s+(\d+\.\d+)/)
|
||||
if (!versionMatch) {
|
||||
console.warn(chalk.yellow("Warning: Could not determine protoc version. Continuing anyway..."))
|
||||
} else {
|
||||
const version = versionMatch[1]
|
||||
const requiredVersion = "30.1"
|
||||
if (version !== requiredVersion) {
|
||||
console.warn(
|
||||
chalk.yellow(`Warning: protoc version ${version} found, but version ${requiredVersion} is required.`),
|
||||
)
|
||||
console.warn(
|
||||
chalk.yellow(
|
||||
`To install the correct version, visit: https://github.com/protocolbuffers/protobuf/releases/tag/v${requiredVersion}`,
|
||||
),
|
||||
)
|
||||
process.exit(0) // Exit with success as requested
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(chalk.yellow("Warning: protoc is not installed. Skipping proto generation."))
|
||||
console.warn(
|
||||
chalk.yellow(
|
||||
"To install Protocol Buffers compiler, visit: https://github.com/protocolbuffers/protobuf/releases/tag/v30.1",
|
||||
),
|
||||
)
|
||||
process.exit(0) // Exit with success as requested
|
||||
}
|
||||
|
||||
// Check if ts-proto plugin is available
|
||||
const TS_PROTO_PLUGIN = path.join(ROOT_DIR, "node_modules", ".bin", "protoc-gen-ts_proto")
|
||||
try {
|
||||
await fs.access(TS_PROTO_PLUGIN)
|
||||
} catch (error) {
|
||||
console.error(chalk.red("Error: ts-proto plugin not found at"), TS_PROTO_PLUGIN)
|
||||
console.error(chalk.red('Please run "npm install" to install the required dependencies.'))
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Define output directories
|
||||
const TS_OUT_DIR = path.join(ROOT_DIR, "src", "shared", "proto")
|
||||
|
||||
@@ -70,15 +35,15 @@ async function main() {
|
||||
|
||||
// Process all proto files
|
||||
console.log(chalk.cyan("Processing proto files from"), SCRIPT_DIR)
|
||||
const protoFiles = await globby("**/*.proto", { cwd: SCRIPT_DIR })
|
||||
const protoFiles = await globby("*.proto", { cwd: SCRIPT_DIR })
|
||||
|
||||
for (const protoFile of protoFiles) {
|
||||
console.log(chalk.cyan(`Generating TypeScript code for ${protoFile}...`))
|
||||
|
||||
// Build the protoc command with proper path handling for cross-platform
|
||||
const protocCommand = [
|
||||
"protoc",
|
||||
`--plugin=protoc-gen-ts_proto="${TS_PROTO_PLUGIN}"`,
|
||||
protoc,
|
||||
`--plugin=protoc-gen-ts_proto="${tsProtoPlugin}"`,
|
||||
`--ts_proto_out="${TS_OUT_DIR}"`,
|
||||
"--ts_proto_opt=outputServices=generic-definitions,env=node,esModuleInterop=true,useDate=false,useOptionals=messages",
|
||||
`--proto_path="${SCRIPT_DIR}"`,
|
||||
@@ -114,9 +79,13 @@ async function generateMethodRegistrations() {
|
||||
console.log(chalk.cyan("Generating method registration files..."))
|
||||
|
||||
const serviceDirs = [
|
||||
path.join(ROOT_DIR, "src", "core", "controller", "mcp"),
|
||||
path.join(ROOT_DIR, "src", "core", "controller", "account"),
|
||||
path.join(ROOT_DIR, "src", "core", "controller", "browser"),
|
||||
path.join(ROOT_DIR, "src", "core", "controller", "checkpoints"),
|
||||
path.join(ROOT_DIR, "src", "core", "controller", "file"),
|
||||
path.join(ROOT_DIR, "src", "core", "controller", "mcp"),
|
||||
path.join(ROOT_DIR, "src", "core", "controller", "task"),
|
||||
path.join(ROOT_DIR, "src", "core", "controller", "web-content"),
|
||||
// Add more service directories here as needed
|
||||
]
|
||||
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "common.proto";
|
||||
|
||||
service CheckpointsService {
|
||||
rpc checkpointDiff(Int64Request) returns (Empty);
|
||||
rpc checkpointRestore(CheckpointRestoreRequest) returns (Empty);
|
||||
}
|
||||
|
||||
message CheckpointRestoreRequest {
|
||||
Metadata metadata = 1;
|
||||
int64 number = 2;
|
||||
string restore_type = 3;
|
||||
optional int64 offset = 4;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
message Metadata {
|
||||
}
|
||||
@@ -38,3 +40,12 @@ message BytesRequest {
|
||||
message Bytes {
|
||||
bytes value = 1;
|
||||
}
|
||||
|
||||
message BooleanRequest {
|
||||
Metadata metadata = 1;
|
||||
bool value = 2;
|
||||
}
|
||||
|
||||
message Boolean {
|
||||
bool value = 1;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "common.proto";
|
||||
|
||||
// Service for file-related operations
|
||||
service FileService {
|
||||
// Opens a file in the editor
|
||||
rpc openFile(StringRequest) returns (Empty);
|
||||
|
||||
// Opens an image in the system viewer
|
||||
rpc openImage(StringRequest) returns (Empty);
|
||||
|
||||
// Deletes a rule file from either global or workspace rules directory
|
||||
rpc deleteRuleFile(RuleFileRequest) returns (RuleFile);
|
||||
|
||||
// Creates a rule file from either global or workspace rules directory
|
||||
rpc createRuleFile(RuleFileRequest) returns (RuleFile);
|
||||
|
||||
// Search git commits in the workspace
|
||||
rpc searchCommits(StringRequest) returns (GitCommits);
|
||||
}
|
||||
|
||||
// Response for searchCommits
|
||||
message GitCommits {
|
||||
repeated GitCommit commits = 1;
|
||||
}
|
||||
|
||||
// Represents a Git commit
|
||||
message GitCommit {
|
||||
string hash = 1;
|
||||
string short_hash = 2;
|
||||
string subject = 3;
|
||||
string author = 4;
|
||||
string date = 5;
|
||||
}
|
||||
|
||||
// Unified request for all rule file operations
|
||||
message RuleFileRequest {
|
||||
Metadata metadata = 1;
|
||||
bool is_global = 2; // Common field for all operations
|
||||
optional string rule_path = 3; // Path field for deleteRuleFile (optional)
|
||||
optional string filename = 4; // Filename field for createRuleFile (optional)
|
||||
}
|
||||
|
||||
// Result for rule file operations with meaningful data only
|
||||
message RuleFile {
|
||||
string file_path = 1; // Path to the rule file
|
||||
string display_name = 2; // Filename for display purposes
|
||||
bool already_exists = 3; // For createRuleFile, indicates if file already existed
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "common.proto";
|
||||
|
||||
service McpService {
|
||||
rpc toggleMcpServer(ToggleMcpServerRequest) returns (McpServers);
|
||||
rpc updateMcpTimeout(UpdateMcpTimeoutRequest) returns (McpServers);
|
||||
rpc addRemoteMcpServer(AddRemoteMcpServerRequest) returns (McpServers);
|
||||
}
|
||||
|
||||
message ToggleMcpServerRequest {
|
||||
@@ -21,6 +24,12 @@ message UpdateMcpTimeoutRequest {
|
||||
int32 timeout = 3;
|
||||
}
|
||||
|
||||
message AddRemoteMcpServerRequest {
|
||||
Metadata metadata = 1;
|
||||
string server_name = 2;
|
||||
string server_url = 3;
|
||||
}
|
||||
|
||||
message McpTool {
|
||||
string name = 1;
|
||||
optional string description = 2;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "common.proto";
|
||||
|
||||
service TaskService {
|
||||
// Cancels the currently running task
|
||||
rpc cancelTask(EmptyRequest) returns (Empty);
|
||||
// Clears the current task
|
||||
rpc clearTask(EmptyRequest) returns (Empty);
|
||||
// Creates a new task with the given text and optional images
|
||||
rpc newTask(NewTaskRequest) returns (Empty);
|
||||
}
|
||||
|
||||
// Request message for creating a new task
|
||||
message NewTaskRequest {
|
||||
Metadata metadata = 1;
|
||||
string text = 2;
|
||||
repeated string images = 3;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "common.proto";
|
||||
|
||||
service WebContentService {
|
||||
rpc checkIsImageUrl(StringRequest) returns (IsImageUrl);
|
||||
}
|
||||
|
||||
message IsImageUrl {
|
||||
bool is_image = 1;
|
||||
string url = 2;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
const { execSync } = require("child_process")
|
||||
const esbuild = require("esbuild")
|
||||
|
||||
const watch = process.argv.includes("--watch")
|
||||
|
||||
/**
|
||||
* @type {import('esbuild').Plugin}
|
||||
*/
|
||||
const esbuildProblemMatcherPlugin = {
|
||||
name: "esbuild-problem-matcher",
|
||||
|
||||
setup(build) {
|
||||
build.onStart(() => {
|
||||
console.log("[watch] build started")
|
||||
})
|
||||
build.onEnd((result) => {
|
||||
result.errors.forEach(({ text, location }) => {
|
||||
console.error(`✘ [ERROR] ${text}`)
|
||||
console.error(` ${location.file}:${location.line}:${location.column}:`)
|
||||
})
|
||||
console.log("[watch] build finished")
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
const srcConfig = {
|
||||
bundle: true,
|
||||
minify: false,
|
||||
sourcemap: true,
|
||||
sourcesContent: true,
|
||||
logLevel: "silent",
|
||||
entryPoints: ["src/packages/**/*.ts"],
|
||||
outdir: "out/packages",
|
||||
format: "cjs",
|
||||
platform: "node",
|
||||
define: {
|
||||
"process.env.IS_DEV": "true",
|
||||
"process.env.IS_TEST": "true",
|
||||
},
|
||||
external: ["vscode"],
|
||||
plugins: [esbuildProblemMatcherPlugin],
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const srcCtx = await esbuild.context(srcConfig)
|
||||
|
||||
if (watch) {
|
||||
await srcCtx.watch()
|
||||
} else {
|
||||
await srcCtx.rebuild()
|
||||
|
||||
await srcCtx.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
execSync("tsc -p ./tsconfig.test.json --outDir out", { encoding: "utf-8" })
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -28,20 +28,26 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
const modelId = await this.getModelId()
|
||||
const model = this.getModel()
|
||||
|
||||
// This baseModelId is used to indicate the capabilities of the model.
|
||||
// If the user selects a custom model, baseModelId will be set to the base model ID of the custom model.
|
||||
// Otherwise, baseModelId will be the same as modelId.
|
||||
const baseModelId =
|
||||
(this.options.awsBedrockCustomSelected ? this.options.awsBedrockCustomModelBaseId : modelId) || modelId
|
||||
|
||||
// Check if this is an Amazon Nova model
|
||||
if (modelId.includes("amazon.nova")) {
|
||||
if (baseModelId.includes("amazon.nova")) {
|
||||
yield* this.createNovaMessage(systemPrompt, messages, modelId, model)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if this is a Deepseek model
|
||||
if (modelId.includes("deepseek")) {
|
||||
if (baseModelId.includes("deepseek")) {
|
||||
yield* this.createDeepseekMessage(systemPrompt, messages, modelId, model)
|
||||
return
|
||||
}
|
||||
|
||||
const budget_tokens = this.options.thinkingBudgetTokens || 0
|
||||
const reasoningOn = modelId.includes("3-7") && budget_tokens !== 0 ? true : false
|
||||
const reasoningOn = baseModelId.includes("3-7") && budget_tokens !== 0 ? true : false
|
||||
|
||||
// Get model info and message indices for caching
|
||||
const userMsgIndices = messages.reduce((acc, msg, index) => (msg.role === "user" ? [...acc, index] : acc), [] as number[])
|
||||
@@ -167,12 +173,23 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
getModel(): { id: BedrockModelId; info: ModelInfo } {
|
||||
getModel(): { id: string; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
if (modelId && modelId in bedrockModels) {
|
||||
const id = modelId as BedrockModelId
|
||||
return { id, info: bedrockModels[id] }
|
||||
}
|
||||
|
||||
const customSelected = this.options.awsBedrockCustomSelected
|
||||
const baseModel = this.options.awsBedrockCustomModelBaseId
|
||||
if (customSelected && modelId && baseModel && baseModel in bedrockModels) {
|
||||
// Use the user-input model ID but inherit capabilities from the base model
|
||||
return {
|
||||
id: modelId,
|
||||
info: bedrockModels[baseModel],
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: bedrockDefaultModelId,
|
||||
info: bedrockModels[bedrockDefaultModelId],
|
||||
@@ -290,7 +307,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
modelId: string,
|
||||
model: { id: BedrockModelId; info: ModelInfo },
|
||||
model: { id: string; info: ModelInfo },
|
||||
): ApiStream {
|
||||
// Get Bedrock client with proper credentials
|
||||
const client = await this.getBedrockClient()
|
||||
@@ -476,13 +493,13 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
|
||||
/**
|
||||
* Creates a message using Amazon Nova models through AWS Bedrock
|
||||
* Implements support for Nova Micro, Nova Lite, and Nova Pro models
|
||||
* Implements support for Amazon Nova models
|
||||
*/
|
||||
private async *createNovaMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
modelId: string,
|
||||
model: { id: BedrockModelId; info: ModelInfo },
|
||||
model: { id: string; info: ModelInfo },
|
||||
): ApiStream {
|
||||
// Get Bedrock client with proper credentials
|
||||
const client = await this.getBedrockClient()
|
||||
|
||||
@@ -74,6 +74,8 @@ export class ClineHandler implements ApiHandler {
|
||||
if (!didOutputUsage && chunk.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
@@ -105,6 +107,9 @@ export class ClineHandler implements ApiHandler {
|
||||
const generation = response.data
|
||||
return {
|
||||
type: "usage",
|
||||
// at this time there's no support for gatting cached_tokens from generation endpoint
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
inputTokens: generation?.native_tokens_prompt || 0,
|
||||
outputTokens: generation?.native_tokens_completion || 0,
|
||||
totalCost: generation?.total_cost || 0,
|
||||
|
||||
+562
-29
@@ -1,55 +1,531 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { GoogleGenerativeAI } from "@google/generative-ai"
|
||||
import type { Anthropic } from "@anthropic-ai/sdk"
|
||||
// Restore GenerateContentConfig import and add GenerateContentResponseUsageMetadata
|
||||
import { GoogleGenAI, type Content, type GenerateContentConfig, type GenerateContentResponseUsageMetadata } from "@google/genai"
|
||||
import NodeCache from "node-cache"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiHandler } from "../"
|
||||
import { ApiHandlerOptions, geminiDefaultModelId, GeminiModelId, geminiModels, ModelInfo } from "@shared/api"
|
||||
import { convertAnthropicMessageToGemini } from "../transform/gemini-format"
|
||||
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 extends ApiHandlerOptions {
|
||||
isVertex?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for Google's Gemini API with optimized caching strategy and accurate cost accounting.
|
||||
*
|
||||
* Key features:
|
||||
* - One cache per task: Creates a single cache per task and reuses it for subsequent turns
|
||||
* - Stable cache keys: Uses taskId as a stable identifier for caches
|
||||
* - Efficient cache updates: Only updates caches when there's new content to add
|
||||
* - Split cost accounting: Separates immediate costs from ongoing cache storage costs
|
||||
*
|
||||
* Cost accounting approach:
|
||||
* - Immediate costs (per message): Input tokens, output tokens, and cache read costs
|
||||
* - Ongoing costs (per task): Cache storage costs for the TTL period
|
||||
*
|
||||
* Gemini's caching system is unique in that it charges for holding tokens in cache by the hour.
|
||||
* This implementation optimizes for both performance and cost by:
|
||||
* 1. Minimizing redundant cache creations
|
||||
* 2. Properly accounting for cache costs in the billing calculations
|
||||
* 3. Using a stable cache key to ensure cache reuse across turns
|
||||
* 4. Separating immediate costs from ongoing costs to avoid double-counting
|
||||
*/
|
||||
export class GeminiHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private client: GoogleGenerativeAI
|
||||
private client: GoogleGenAI
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
if (!options.geminiApiKey) {
|
||||
throw new Error("API key is required for Google Gemini")
|
||||
}
|
||||
// Enhanced caching system
|
||||
private contentCaches: NodeCache // Stores cache details (key, count, etc.)
|
||||
private isCacheBusy = false
|
||||
private taskCacheNames: Map<string, string> = new Map() // Maps taskId to cache name for stable lookup
|
||||
private taskCacheTokens: Map<string, number> = new Map() // Maps taskId to total tokens in cache
|
||||
|
||||
constructor(options: GeminiHandlerOptions) {
|
||||
// Store the options
|
||||
this.options = options
|
||||
this.client = new GoogleGenerativeAI(options.geminiApiKey)
|
||||
|
||||
if (options.isVertex) {
|
||||
// Initialize with Vertex AI configuration
|
||||
const project = this.options.vertexProjectId ?? "not-provided"
|
||||
const location = this.options.vertexRegion ?? "not-provided"
|
||||
|
||||
this.client = new GoogleGenAI({
|
||||
vertexai: true,
|
||||
project,
|
||||
location,
|
||||
})
|
||||
} else {
|
||||
// Initialize with standard API key
|
||||
if (!options.geminiApiKey) {
|
||||
throw new Error("API key is required for Google Gemini when not using Vertex AI")
|
||||
}
|
||||
|
||||
this.client = new GoogleGenAI({ apiKey: options.geminiApiKey })
|
||||
}
|
||||
|
||||
// Initialize cache with TTL and check period
|
||||
this.contentCaches = new NodeCache({
|
||||
stdTTL: DEFAULT_CACHE_TTL_SECONDS,
|
||||
checkperiod: DEFAULT_CACHE_TTL_SECONDS,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a message using the Gemini API with optimized caching and split cost accounting.
|
||||
*
|
||||
* This method implements a task-based caching strategy:
|
||||
* 1. Each task gets its own cache, identified by taskId
|
||||
* 2. On first call for a task, a new cache is created
|
||||
* 3. On subsequent calls, the existing cache is reused and only new messages are sent
|
||||
* 4. Cache operations are tracked for accurate cost accounting
|
||||
*
|
||||
* Cost accounting:
|
||||
* - Immediate costs (returned in the usage object): Input tokens, output tokens, cache read costs
|
||||
* - Ongoing costs (tracked at task level): Cache storage costs for the TTL period
|
||||
*
|
||||
* @param systemPrompt The system prompt to use for the message
|
||||
* @param messages The conversation history to include in the message
|
||||
* @returns An async generator that yields chunks of the response with accurate immediate costs
|
||||
*/
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const modelOptions = {
|
||||
model: this.getModel().id,
|
||||
systemInstruction: systemPrompt,
|
||||
const { id: model, info } = this.getModel()
|
||||
const contents = messages.map(convertAnthropicMessageToGemini)
|
||||
|
||||
// Ensure we have a stable cache key (taskId)
|
||||
if (!this.options.taskId) {
|
||||
console.warn("[GeminiHandler] No taskId provided, caching will be disabled")
|
||||
}
|
||||
|
||||
const clientOptions = this.options.geminiBaseUrl ? { baseUrl: this.options.geminiBaseUrl } : undefined
|
||||
const model = this.client.getGenerativeModel(modelOptions, clientOptions)
|
||||
const result = await model.generateContentStream({
|
||||
contents: messages.map(convertAnthropicMessageToGemini),
|
||||
generationConfig: {
|
||||
// maxOutputTokens: this.getModel().info.maxTokens,
|
||||
temperature: 0,
|
||||
},
|
||||
})
|
||||
const taskId = this.options.taskId
|
||||
|
||||
for await (const chunk of result.stream) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: chunk.text(),
|
||||
// Calculate total content length for cache eligibility check
|
||||
const contentsLength = systemPrompt.length + this.getMessagesLength(contents)
|
||||
|
||||
// Minimum token threshold for caching (approx 4096 tokens)
|
||||
const CONTEXT_CACHE_TOKEN_MINIMUM = 4096
|
||||
|
||||
let uncachedContent: Content[] | undefined = undefined
|
||||
let cachedContent: string | undefined = undefined
|
||||
|
||||
// Check if caching is available and content is large enough to benefit from caching
|
||||
// We only enable caching for conversations above a certain size to avoid overhead for small requests
|
||||
const isCacheAvailable = info.supportsPromptCache && contentsLength > 4 * CONTEXT_CACHE_TOKEN_MINIMUM && taskId
|
||||
|
||||
// This flag tracks whether this operation involves a cache write/update
|
||||
// It's used to track task-level ongoing costs, not immediate costs
|
||||
let cacheWrite = false
|
||||
|
||||
if (isCacheAvailable) {
|
||||
// Check if we already have a cache for this task
|
||||
const existingCacheName = this.taskCacheNames.get(taskId)
|
||||
const cacheEntry = existingCacheName ? this.contentCaches.get<{ key: string; count: number }>(taskId) : undefined
|
||||
|
||||
if (cacheEntry) {
|
||||
// Use existing cache
|
||||
uncachedContent = contents.slice(cacheEntry.count, contents.length)
|
||||
cachedContent = cacheEntry.key
|
||||
console.log(
|
||||
`[GeminiHandler] using existing cache for task ${taskId}: ${cacheEntry.count} cached messages (${cacheEntry.key}) and ${uncachedContent.length} uncached messages`,
|
||||
)
|
||||
}
|
||||
|
||||
// Create or update cache only if there's new content to add
|
||||
const shouldUpdateCache = !existingCacheName || (cacheEntry && uncachedContent && uncachedContent.length > 0)
|
||||
|
||||
if (shouldUpdateCache) {
|
||||
// If we should update the cache, then there will be a cache write
|
||||
cacheWrite = true
|
||||
}
|
||||
}
|
||||
const isCacheUsed = !!cachedContent
|
||||
|
||||
// Configure thinking budget if supported
|
||||
const thinkingBudget = this.options.thinkingBudgetTokens ?? 0
|
||||
const maxBudget = info.thinkingConfig?.maxBudget ?? 0
|
||||
|
||||
// Set up base generation config
|
||||
const requestConfig: GenerateContentConfig = {
|
||||
// Add base URL if configured
|
||||
httpOptions: this.options.geminiBaseUrl ? { baseUrl: this.options.geminiBaseUrl } : undefined,
|
||||
|
||||
// Only include systemInstruction if NOT using the cache
|
||||
...(isCacheUsed ? {} : { systemInstruction: systemPrompt }),
|
||||
|
||||
// Set temperature (default to 0)
|
||||
temperature: 0,
|
||||
}
|
||||
|
||||
// Add thinking config if the model supports it
|
||||
if (info.thinkingConfig?.outputPrice !== undefined && maxBudget > 0) {
|
||||
requestConfig.thinkingConfig = {
|
||||
thinkingBudget: thinkingBudget,
|
||||
}
|
||||
}
|
||||
|
||||
const response = await result.response
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: response.usageMetadata?.promptTokenCount ?? 0,
|
||||
outputTokens: response.usageMetadata?.candidatesTokenCount ?? 0,
|
||||
// Generate content using the configured parameters
|
||||
const result = await this.client.models.generateContentStream({
|
||||
model,
|
||||
contents: uncachedContent ?? contents,
|
||||
config: {
|
||||
...requestConfig,
|
||||
...(isCacheUsed ? { cachedContent } : {}),
|
||||
},
|
||||
})
|
||||
|
||||
// Update the cache after the LLM request is already sent to avoid blocking
|
||||
// We only update the cache if we have a taskId and the cache write flag is set
|
||||
// This is a non-blocking operation and will not affect the response time
|
||||
if (cacheWrite && taskId) {
|
||||
this.updateCacheContent(taskId, model, contents, systemPrompt)
|
||||
}
|
||||
// Track usage metadata
|
||||
let lastUsageMetadata: GenerateContentResponseUsageMetadata | undefined
|
||||
|
||||
// Process the stream
|
||||
for await (const chunk of result) {
|
||||
if (chunk.text) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: chunk.text,
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.usageMetadata) {
|
||||
lastUsageMetadata = chunk.usageMetadata
|
||||
}
|
||||
}
|
||||
|
||||
// Yield usage information at the end
|
||||
if (lastUsageMetadata) {
|
||||
const inputTokens = lastUsageMetadata.promptTokenCount ?? 0
|
||||
const outputTokens = lastUsageMetadata.candidatesTokenCount ?? 0
|
||||
const cacheReadTokens = lastUsageMetadata.cachedContentTokenCount
|
||||
|
||||
// Calculate immediate costs only (excluding cache write/storage costs)
|
||||
const totalCost = this.calculateCost({
|
||||
info,
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
cacheReadTokens,
|
||||
})
|
||||
|
||||
// Store the token count for task-level ongoing cost tracking
|
||||
// This is not included in the immediate costs returned to the user
|
||||
const cacheWriteTokens = cacheWrite ? inputTokens : undefined
|
||||
|
||||
// If this is a cache write operation, update the task's ongoing costs
|
||||
if (cacheWrite && this.options.taskId && inputTokens > 0) {
|
||||
// Log the ongoing costs for debugging
|
||||
const ongoingCosts = this.getTaskOngoingCosts(this.options.taskId)
|
||||
console.log(
|
||||
`[GeminiHandler] Task ${this.options.taskId} ongoing costs: $${ongoingCosts?.toFixed(6) ?? "unknown"}`,
|
||||
)
|
||||
}
|
||||
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
cacheReadTokens,
|
||||
cacheWriteTokens,
|
||||
totalCost,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all caches for the current API key.
|
||||
*
|
||||
* According to the Gemini API documentation, you can retrieve metadata for all uploaded caches
|
||||
* using the caches.list() method. This is useful for monitoring cache usage and cleanup.
|
||||
*
|
||||
* @param pageSize Optional number of caches to return per page (default: 10)
|
||||
* @returns A promise that resolves to an array of cache metadata objects
|
||||
*/
|
||||
public async listCaches(pageSize: number = 10): Promise<any[]> {
|
||||
try {
|
||||
const caches: any[] = []
|
||||
const pager = await this.client.caches.list({ config: { pageSize } })
|
||||
|
||||
let page = pager.page
|
||||
while (true) {
|
||||
for (const cache of page) {
|
||||
caches.push(cache)
|
||||
}
|
||||
|
||||
if (!pager.hasNextPage()) {
|
||||
break
|
||||
}
|
||||
page = await pager.nextPage()
|
||||
}
|
||||
|
||||
return caches
|
||||
} catch (error) {
|
||||
console.error(`[GeminiHandler] Failed to list caches:`, error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the content of a cache for a specific task.
|
||||
*
|
||||
* Since the Gemini API doesn't support incremental updates to cache content,
|
||||
* this method:
|
||||
* 1. Creates a new cache with the full content (old + new)
|
||||
* 2. Deletes the old cache if it exists
|
||||
* 3. Updates our local tracking to point to the new cache
|
||||
*
|
||||
* @param taskId The ID of the task whose cache should be updated
|
||||
* @param model The model to use for the cache
|
||||
* @param contents The full content to cache (including both old and new messages)
|
||||
* @param systemInstruction The system instruction to include in the cache
|
||||
*/
|
||||
private async updateCacheContent(
|
||||
taskId: string,
|
||||
model: string,
|
||||
contents: Content[],
|
||||
systemInstruction: string,
|
||||
): Promise<void> {
|
||||
if (this.isCacheBusy) {
|
||||
console.log(`[GeminiHandler] Cache is busy, skipping update for task ${taskId}`)
|
||||
return
|
||||
}
|
||||
|
||||
this.isCacheBusy = true
|
||||
const timestamp = Date.now()
|
||||
const existingCacheName = this.taskCacheNames.get(taskId)
|
||||
|
||||
try {
|
||||
// 1. Create a new cache with the full content
|
||||
const result = await this.client.caches.create({
|
||||
model,
|
||||
config: {
|
||||
contents,
|
||||
systemInstruction,
|
||||
ttl: `${DEFAULT_CACHE_TTL_SECONDS}s`,
|
||||
httpOptions: { timeout: 120_000 },
|
||||
},
|
||||
})
|
||||
|
||||
const { name, usageMetadata } = result
|
||||
|
||||
if (name) {
|
||||
// 2. Delete the old cache if it exists (non-blocking)
|
||||
// We don't await this operation to avoid blocking the main flow if deletion fails
|
||||
if (existingCacheName) {
|
||||
// Schedule cache deletion in the background
|
||||
setTimeout(() => {
|
||||
this.client.caches
|
||||
.delete({ name: existingCacheName })
|
||||
.then(() => {
|
||||
console.log(`[GeminiHandler] Deleted old cache ${existingCacheName} for task ${taskId}`)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(`[GeminiHandler] Failed to delete old cache ${existingCacheName}:`, error)
|
||||
console.log(`[GeminiHandler] Continuing without deleting old cache. It will expire after TTL.`)
|
||||
})
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 3. Update our local tracking
|
||||
this.contentCaches.set<{ key: string; count: number }>(taskId, {
|
||||
key: name,
|
||||
count: contents.length,
|
||||
})
|
||||
this.taskCacheNames.set(taskId, name)
|
||||
|
||||
// Track total tokens in cache for ongoing cost calculation
|
||||
const totalTokens = usageMetadata?.totalTokenCount ?? 0
|
||||
this.taskCacheTokens.set(taskId, totalTokens)
|
||||
|
||||
const operation = existingCacheName ? "Updated" : "Created new"
|
||||
console.log(
|
||||
`[GeminiHandler] ${operation} cache for task ${taskId}: ${contents.length} messages (${totalTokens} tokens) in ${Date.now() - timestamp}ms`,
|
||||
)
|
||||
|
||||
return // Indicate that a cache write occurred
|
||||
}
|
||||
|
||||
return
|
||||
} catch (error) {
|
||||
console.error(`[GeminiHandler] Failed to update cache for task ${taskId}:`, error)
|
||||
return
|
||||
} finally {
|
||||
this.isCacheBusy = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the TTL of an existing cache.
|
||||
*
|
||||
* According to the Gemini API documentation, you can update the TTL of a cache
|
||||
* using the caches.update() method. This is useful for extending the lifetime
|
||||
* of a cache that's still being used.
|
||||
*
|
||||
* @param taskId The ID of the task whose cache TTL should be updated
|
||||
* @param ttlSeconds The new TTL in seconds
|
||||
* @returns A promise that resolves to the updated cache, or undefined if the update fails
|
||||
*/
|
||||
public async updateCacheTTL(taskId: string, ttlSeconds: number = DEFAULT_CACHE_TTL_SECONDS): Promise<any> {
|
||||
const cacheName = this.taskCacheNames.get(taskId)
|
||||
if (!cacheName) {
|
||||
console.warn(`[GeminiHandler] No cache found for task ${taskId}, cannot update TTL`)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedCache = await this.client.caches.update({
|
||||
name: cacheName,
|
||||
config: { ttl: `${ttlSeconds}s` },
|
||||
})
|
||||
|
||||
console.log(`[GeminiHandler] Updated TTL for cache ${cacheName} to ${ttlSeconds}s`)
|
||||
return updatedCache
|
||||
} catch (error) {
|
||||
console.error(`[GeminiHandler] Failed to update TTL for cache ${cacheName}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the ongoing costs for a task based on cache storage.
|
||||
*
|
||||
* This method calculates the cost of holding tokens in cache for the TTL period.
|
||||
* These costs are separate from the immediate costs of API calls and should be
|
||||
* tracked at the task level rather than the message level.
|
||||
*
|
||||
* TODO: Surface these ongoing costs to the user in the UI, possibly in:
|
||||
* - The task header/summary
|
||||
* - A dedicated "costs" panel or tooltip
|
||||
* - As part of the total cost calculation for the task
|
||||
*
|
||||
* @param taskId The ID of the task to calculate ongoing costs for
|
||||
* @returns The ongoing cost in dollars, or undefined if no cache exists for the task
|
||||
*/
|
||||
public getTaskOngoingCosts(taskId: string): number | undefined {
|
||||
const tokens = this.taskCacheTokens.get(taskId)
|
||||
if (!tokens) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const { info } = this.getModel()
|
||||
if (!info.cacheWritesPrice) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Calculate the cost of holding tokens in cache for the TTL period
|
||||
// (tokens / 1M) * (price per 1M tokens) * (cache TTL in hours)
|
||||
return info.cacheWritesPrice * (tokens / 1_000_000) * (DEFAULT_CACHE_TTL_SECONDS / 3600)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the immediate dollar cost of the API call based on token usage and model pricing.
|
||||
*
|
||||
* This method accounts for the immediate costs of the API call:
|
||||
* - Input token costs (for uncached tokens)
|
||||
* - Output token costs
|
||||
* - Cache read costs
|
||||
*
|
||||
* It does NOT include ongoing costs like cache storage, which are tracked separately
|
||||
* at the task level through getTaskOngoingCosts().
|
||||
*/
|
||||
public calculateCost({
|
||||
info,
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
cacheWriteTokens = 0,
|
||||
cacheReadTokens = 0,
|
||||
}: {
|
||||
info: ModelInfo
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
cacheWriteTokens?: number
|
||||
cacheReadTokens?: number
|
||||
}) {
|
||||
// Exit early if any required pricing information is missing
|
||||
if (!info.inputPrice || !info.outputPrice) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
let inputPrice = info.inputPrice
|
||||
let outputPrice = info.outputPrice
|
||||
let cacheWritesPrice = info.cacheWritesPrice ?? 0
|
||||
// Right now, we only show the immediate costs of caching and not the ongoing costs of storing the cache
|
||||
cacheWritesPrice = 0
|
||||
let cacheReadsPrice = info.cacheReadsPrice ?? 0
|
||||
|
||||
// If there's tiered pricing then adjust prices based on the input tokens used
|
||||
if (info.tiers) {
|
||||
const tier = info.tiers.find((tier) => inputTokens <= tier.contextWindow)
|
||||
if (tier) {
|
||||
inputPrice = tier.inputPrice ?? inputPrice
|
||||
outputPrice = tier.outputPrice ?? outputPrice
|
||||
cacheWritesPrice = tier.cacheWritesPrice ?? cacheWritesPrice
|
||||
cacheReadsPrice = tier.cacheReadsPrice ?? cacheReadsPrice
|
||||
}
|
||||
}
|
||||
|
||||
// Subtract the cached input tokens from the total input tokens
|
||||
const uncachedInputTokens = inputTokens - (cacheReadTokens ?? 0)
|
||||
|
||||
// Calculate immediate costs only
|
||||
|
||||
// 1. Input token costs (for uncached tokens)
|
||||
const inputTokensCost = inputPrice * (uncachedInputTokens / 1_000_000)
|
||||
|
||||
// 2. Output token costs
|
||||
const outputTokensCost = outputPrice * (outputTokens / 1_000_000)
|
||||
|
||||
// 3. Cache read costs (immediate)
|
||||
const cacheReadCost = (cacheReadTokens ?? 0) > 0 ? cacheReadsPrice * ((cacheReadTokens ?? 0) / 1_000_000) : 0
|
||||
|
||||
// Calculate total immediate cost (excluding cache write/storage costs)
|
||||
const totalCost = inputTokensCost + outputTokensCost + cacheReadCost
|
||||
|
||||
// Create the trace object for debugging
|
||||
const trace: Record<string, { price: number; tokens: number; cost: number }> = {
|
||||
input: { price: inputPrice, tokens: uncachedInputTokens, cost: inputTokensCost },
|
||||
output: { price: outputPrice, tokens: outputTokens, cost: outputTokensCost },
|
||||
}
|
||||
|
||||
// Only include cache read costs in the trace (cache write costs are tracked separately)
|
||||
if ((cacheReadTokens ?? 0) > 0) {
|
||||
trace.cacheRead = { price: cacheReadsPrice, tokens: cacheReadTokens ?? 0, cost: cacheReadCost }
|
||||
}
|
||||
|
||||
// console.log(`[GeminiHandler] calculateCost -> ${totalCost}`, trace)
|
||||
return totalCost
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the total length of all messages for cache eligibility check
|
||||
*/
|
||||
private getMessagesLength(contents: Content[]): number {
|
||||
return contents.reduce((total, content) => {
|
||||
if (!content.parts) {
|
||||
return total
|
||||
}
|
||||
|
||||
return (
|
||||
total +
|
||||
content.parts.reduce((partTotal, part) => {
|
||||
if (typeof part.text === "string") {
|
||||
return partTotal + part.text.length
|
||||
}
|
||||
return partTotal
|
||||
}, 0)
|
||||
)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the model ID and info for the current configuration
|
||||
*/
|
||||
getModel(): { id: GeminiModelId; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
if (modelId && modelId in geminiModels) {
|
||||
@@ -61,4 +537,61 @@ export class GeminiHandler implements ApiHandler {
|
||||
info: geminiModels[geminiDefaultModelId],
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Count tokens in content using the Gemini API
|
||||
*/
|
||||
async countTokens(content: Array<any>): Promise<number> {
|
||||
try {
|
||||
const { id: model } = this.getModel()
|
||||
|
||||
// Convert content to Gemini format
|
||||
const geminiContent = content.map((block) => {
|
||||
if (typeof block === "string") {
|
||||
return { text: block }
|
||||
}
|
||||
return { text: JSON.stringify(block) }
|
||||
})
|
||||
|
||||
// Use Gemini's token counting API
|
||||
const response = await this.client.models.countTokens({
|
||||
model,
|
||||
contents: [{ parts: geminiContent }],
|
||||
})
|
||||
|
||||
if (response.totalTokens === undefined) {
|
||||
console.warn("Gemini token counting returned undefined, using fallback")
|
||||
return this.estimateTokens(content)
|
||||
}
|
||||
|
||||
return response.totalTokens
|
||||
} catch (error) {
|
||||
console.warn("Gemini token counting failed, using fallback", error)
|
||||
return this.estimateTokens(content)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback token estimation method
|
||||
*/
|
||||
private estimateTokens(content: Array<any>): number {
|
||||
// Simple estimation: ~4 characters per token
|
||||
const totalChars = content.reduce((total, block) => {
|
||||
if (typeof block === "string") {
|
||||
return total + block.length
|
||||
} else if (block && typeof block === "object") {
|
||||
// Safely stringify the object
|
||||
try {
|
||||
const jsonStr = JSON.stringify(block)
|
||||
return total + jsonStr.length
|
||||
} catch (e) {
|
||||
console.warn("Failed to stringify block for token estimation", e)
|
||||
return total
|
||||
}
|
||||
}
|
||||
return total
|
||||
}, 0)
|
||||
|
||||
return Math.ceil(totalChars / 4)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,9 @@ export class OllamaHandler implements ApiHandler {
|
||||
|
||||
try {
|
||||
// Create a promise that rejects after timeout
|
||||
const timeoutMs = this.options.requestTimeoutMs || 30000
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
setTimeout(() => reject(new Error("Ollama request timed out after 120 seconds")), 120000)
|
||||
setTimeout(() => reject(new Error(`Ollama request timed out after ${timeoutMs / 1000} seconds`)), timeoutMs)
|
||||
})
|
||||
|
||||
// Create the actual API request promise
|
||||
@@ -63,7 +64,8 @@ export class OllamaHandler implements ApiHandler {
|
||||
} catch (error: any) {
|
||||
// Check if it's a timeout error
|
||||
if (error.message && error.message.includes("timed out")) {
|
||||
throw new Error("Ollama request timed out after 120 seconds")
|
||||
const timeoutMs = this.options.requestTimeoutMs || 30000
|
||||
throw new Error(`Ollama request timed out after ${timeoutMs / 1000} seconds`)
|
||||
}
|
||||
|
||||
// Enhance error reporting
|
||||
|
||||
@@ -76,6 +76,8 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
if (!didOutputUsage && chunk.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
@@ -103,8 +105,9 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
// console.log("OpenRouter generation details:", generation)
|
||||
return {
|
||||
type: "usage",
|
||||
// cacheWriteTokens: 0,
|
||||
// cacheReadTokens: 0,
|
||||
// at this time there's no support for gatting cached_tokens from generation endpoint
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
// openrouter generation endpoint fails often
|
||||
inputTokens: generation?.native_tokens_prompt || 0,
|
||||
outputTokens: generation?.native_tokens_completion || 0,
|
||||
|
||||
+174
-234
@@ -4,26 +4,28 @@ import { withRetry } from "../retry"
|
||||
import { ApiHandler } from "../"
|
||||
import { ApiHandlerOptions, ModelInfo, vertexDefaultModelId, VertexModelId, vertexModels } from "@shared/api"
|
||||
import { ApiStream } from "@api/transform/stream"
|
||||
import { VertexAI } from "@google-cloud/vertexai"
|
||||
import { calculateApiCostOpenAI } from "@utils/cost"
|
||||
import { GeminiHandler } from "./gemini"
|
||||
|
||||
// https://docs.anthropic.com/en/api/claude-on-vertex-ai
|
||||
export class VertexHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private geminiHandler: GeminiHandler
|
||||
private clientAnthropic: AnthropicVertex
|
||||
private clientVertex: VertexAI
|
||||
private options: ApiHandlerOptions
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
|
||||
// Create a GeminiHandler with isVertex flag for Gemini models
|
||||
this.geminiHandler = new GeminiHandler({
|
||||
...options,
|
||||
isVertex: true,
|
||||
})
|
||||
|
||||
// Initialize Anthropic client for Claude models
|
||||
this.clientAnthropic = new AnthropicVertex({
|
||||
projectId: this.options.vertexProjectId,
|
||||
// https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude#regions
|
||||
region: this.options.vertexRegion,
|
||||
})
|
||||
this.clientVertex = new VertexAI({
|
||||
project: this.options.vertexProjectId,
|
||||
location: this.options.vertexRegion,
|
||||
})
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
@@ -31,66 +33,46 @@ export class VertexHandler implements ApiHandler {
|
||||
const model = this.getModel()
|
||||
const modelId = model.id
|
||||
|
||||
if (modelId.includes("claude")) {
|
||||
let budget_tokens = this.options.thinkingBudgetTokens || 0
|
||||
const reasoningOn = modelId.includes("3-7") && budget_tokens !== 0 ? true : false
|
||||
// For Gemini models, use the GeminiHandler
|
||||
if (!modelId.includes("claude")) {
|
||||
yield* this.geminiHandler.createMessage(systemPrompt, messages)
|
||||
return
|
||||
}
|
||||
|
||||
let stream
|
||||
switch (modelId) {
|
||||
case "claude-3-7-sonnet@20250219":
|
||||
case "claude-3-5-sonnet-v2@20241022":
|
||||
case "claude-3-5-sonnet@20240620":
|
||||
case "claude-3-5-haiku@20241022":
|
||||
case "claude-3-opus@20240229":
|
||||
case "claude-3-haiku@20240307": {
|
||||
// Find indices of user messages for cache control
|
||||
const userMsgIndices = messages.reduce(
|
||||
(acc, msg, index) => (msg.role === "user" ? [...acc, index] : acc),
|
||||
[] as number[],
|
||||
)
|
||||
const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
|
||||
const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
|
||||
// Claude implementation
|
||||
let budget_tokens = this.options.thinkingBudgetTokens || 0
|
||||
const reasoningOn = modelId.includes("3-7") && budget_tokens !== 0 ? true : false
|
||||
let stream
|
||||
|
||||
stream = await this.clientAnthropic.beta.messages.create(
|
||||
{
|
||||
model: modelId,
|
||||
max_tokens: model.info.maxTokens || 8192,
|
||||
thinking: reasoningOn ? { type: "enabled", budget_tokens: budget_tokens } : undefined,
|
||||
temperature: reasoningOn ? undefined : 0,
|
||||
system: [
|
||||
{
|
||||
text: systemPrompt,
|
||||
type: "text",
|
||||
cache_control: { type: "ephemeral" },
|
||||
},
|
||||
],
|
||||
messages: messages.map((message, index) => {
|
||||
if (index === lastUserMsgIndex || index === secondLastMsgUserIndex) {
|
||||
return {
|
||||
...message,
|
||||
content:
|
||||
typeof message.content === "string"
|
||||
? [
|
||||
{
|
||||
type: "text",
|
||||
text: message.content,
|
||||
cache_control: {
|
||||
type: "ephemeral",
|
||||
},
|
||||
},
|
||||
]
|
||||
: message.content.map((content, contentIndex) =>
|
||||
contentIndex === message.content.length - 1
|
||||
? {
|
||||
...content,
|
||||
cache_control: {
|
||||
type: "ephemeral",
|
||||
},
|
||||
}
|
||||
: content,
|
||||
),
|
||||
}
|
||||
}
|
||||
switch (modelId) {
|
||||
case "claude-3-7-sonnet@20250219":
|
||||
case "claude-3-5-sonnet-v2@20241022":
|
||||
case "claude-3-5-sonnet@20240620":
|
||||
case "claude-3-5-haiku@20241022":
|
||||
case "claude-3-opus@20240229":
|
||||
case "claude-3-haiku@20240307": {
|
||||
// Find indices of user messages for cache control
|
||||
const userMsgIndices = messages.reduce(
|
||||
(acc, msg, index) => (msg.role === "user" ? [...acc, index] : acc),
|
||||
[] as number[],
|
||||
)
|
||||
const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
|
||||
const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
|
||||
stream = await this.clientAnthropic.beta.messages.create(
|
||||
{
|
||||
model: modelId,
|
||||
max_tokens: model.info.maxTokens || 8192,
|
||||
thinking: reasoningOn ? { type: "enabled", budget_tokens: budget_tokens } : undefined,
|
||||
temperature: reasoningOn ? undefined : 0,
|
||||
system: [
|
||||
{
|
||||
text: systemPrompt,
|
||||
type: "text",
|
||||
cache_control: { type: "ephemeral" },
|
||||
},
|
||||
],
|
||||
messages: messages.map((message, index) => {
|
||||
if (index === lastUserMsgIndex || index === secondLastMsgUserIndex) {
|
||||
return {
|
||||
...message,
|
||||
content:
|
||||
@@ -99,184 +81,142 @@ export class VertexHandler implements ApiHandler {
|
||||
{
|
||||
type: "text",
|
||||
text: message.content,
|
||||
cache_control: {
|
||||
type: "ephemeral",
|
||||
},
|
||||
},
|
||||
]
|
||||
: message.content,
|
||||
: message.content.map((content, contentIndex) =>
|
||||
contentIndex === message.content.length - 1
|
||||
? {
|
||||
...content,
|
||||
cache_control: {
|
||||
type: "ephemeral",
|
||||
},
|
||||
}
|
||||
: content,
|
||||
),
|
||||
}
|
||||
}),
|
||||
stream: true,
|
||||
},
|
||||
{
|
||||
headers: {},
|
||||
},
|
||||
)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
stream = await this.clientAnthropic.beta.messages.create({
|
||||
model: modelId,
|
||||
max_tokens: model.info.maxTokens || 8192,
|
||||
temperature: 0,
|
||||
system: [
|
||||
{
|
||||
text: systemPrompt,
|
||||
type: "text",
|
||||
},
|
||||
],
|
||||
messages: messages.map((message) => ({
|
||||
...message,
|
||||
content:
|
||||
typeof message.content === "string"
|
||||
? [
|
||||
{
|
||||
type: "text",
|
||||
text: message.content,
|
||||
},
|
||||
]
|
||||
: message.content,
|
||||
})),
|
||||
stream: true,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
for await (const chunk of stream) {
|
||||
switch (chunk.type) {
|
||||
case "message_start":
|
||||
const usage = chunk.message.usage
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: usage.input_tokens || 0,
|
||||
outputTokens: usage.output_tokens || 0,
|
||||
cacheWriteTokens: usage.cache_creation_input_tokens || undefined,
|
||||
cacheReadTokens: usage.cache_read_input_tokens || undefined,
|
||||
}
|
||||
break
|
||||
case "message_delta":
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: 0,
|
||||
outputTokens: chunk.usage.output_tokens || 0,
|
||||
}
|
||||
break
|
||||
case "message_stop":
|
||||
break
|
||||
case "content_block_start":
|
||||
switch (chunk.content_block.type) {
|
||||
case "thinking":
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: chunk.content_block.thinking || "",
|
||||
}
|
||||
break
|
||||
case "redacted_thinking":
|
||||
// Handle redacted thinking blocks - we still mark it as reasoning
|
||||
// but note that the content is encrypted
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: "[Redacted thinking block]",
|
||||
}
|
||||
break
|
||||
|
||||
case "text":
|
||||
if (chunk.index > 0) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: "\n",
|
||||
}
|
||||
}
|
||||
yield {
|
||||
type: "text",
|
||||
text: chunk.content_block.text,
|
||||
}
|
||||
break
|
||||
}
|
||||
break
|
||||
case "content_block_delta":
|
||||
switch (chunk.delta.type) {
|
||||
case "thinking_delta":
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: chunk.delta.thinking,
|
||||
}
|
||||
break
|
||||
case "text_delta":
|
||||
yield {
|
||||
type: "text",
|
||||
text: chunk.delta.text,
|
||||
}
|
||||
break
|
||||
}
|
||||
break
|
||||
case "content_block_stop":
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// gemini
|
||||
const generativeModel = this.clientVertex.getGenerativeModel({
|
||||
model: this.getModel().id,
|
||||
systemInstruction: {
|
||||
role: "system",
|
||||
parts: [{ text: systemPrompt }],
|
||||
},
|
||||
})
|
||||
const request = {
|
||||
contents: [
|
||||
{
|
||||
role: "user",
|
||||
parts: messages.map((m) => {
|
||||
if (typeof m.content === "string") {
|
||||
return { text: m.content }
|
||||
} else if (Array.isArray(m.content)) {
|
||||
return {
|
||||
text: m.content
|
||||
.map((block) => {
|
||||
if (typeof block === "string") {
|
||||
return block
|
||||
} else if (block.type === "text") {
|
||||
return block.text
|
||||
} else {
|
||||
console.log("Unsupported block type", block)
|
||||
return ""
|
||||
}
|
||||
})
|
||||
.join(" "),
|
||||
}
|
||||
} else {
|
||||
return { text: "" }
|
||||
}
|
||||
return {
|
||||
...message,
|
||||
content:
|
||||
typeof message.content === "string"
|
||||
? [
|
||||
{
|
||||
type: "text",
|
||||
text: message.content,
|
||||
},
|
||||
]
|
||||
: message.content,
|
||||
}
|
||||
}),
|
||||
stream: true,
|
||||
},
|
||||
],
|
||||
{
|
||||
headers: {},
|
||||
},
|
||||
)
|
||||
break
|
||||
}
|
||||
const streamingResult = await generativeModel.generateContentStream(request)
|
||||
for await (const chunk of streamingResult.stream) {
|
||||
// If usage data is available, yield it similarly:
|
||||
// yield { type: "usage", inputTokens: 0, outputTokens: 0 }
|
||||
// Otherwise, just yield text:
|
||||
const candidates = chunk.candidates || []
|
||||
for (const candidate of candidates) {
|
||||
for (const part of candidate.content?.parts || []) {
|
||||
if (part.text) {
|
||||
default: {
|
||||
stream = await this.clientAnthropic.beta.messages.create({
|
||||
model: modelId,
|
||||
max_tokens: model.info.maxTokens || 8192,
|
||||
temperature: 0,
|
||||
system: [
|
||||
{
|
||||
text: systemPrompt,
|
||||
type: "text",
|
||||
},
|
||||
],
|
||||
messages: messages.map((message) => ({
|
||||
...message,
|
||||
content:
|
||||
typeof message.content === "string"
|
||||
? [
|
||||
{
|
||||
type: "text",
|
||||
text: message.content,
|
||||
},
|
||||
]
|
||||
: message.content,
|
||||
})),
|
||||
stream: true,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for await (const chunk of stream) {
|
||||
switch (chunk.type) {
|
||||
case "message_start":
|
||||
const usage = chunk.message.usage
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: usage.input_tokens || 0,
|
||||
outputTokens: usage.output_tokens || 0,
|
||||
cacheWriteTokens: usage.cache_creation_input_tokens || undefined,
|
||||
cacheReadTokens: usage.cache_read_input_tokens || undefined,
|
||||
}
|
||||
break
|
||||
case "message_delta":
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: 0,
|
||||
outputTokens: chunk.usage?.output_tokens || 0,
|
||||
}
|
||||
break
|
||||
case "message_stop":
|
||||
break
|
||||
case "content_block_start":
|
||||
switch (chunk.content_block.type) {
|
||||
case "thinking":
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: chunk.content_block.thinking || "",
|
||||
}
|
||||
break
|
||||
case "redacted_thinking":
|
||||
// Handle redacted thinking blocks - we still mark it as reasoning
|
||||
// but note that the content is encrypted
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: "[Redacted thinking block]",
|
||||
}
|
||||
break
|
||||
case "text":
|
||||
if (chunk.index > 0) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: "\n",
|
||||
}
|
||||
}
|
||||
yield {
|
||||
type: "text",
|
||||
text: part.text,
|
||||
text: chunk.content_block.text,
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// Handle token usage metadata
|
||||
const { usageMetadata } = await streamingResult.response
|
||||
if (usageMetadata) {
|
||||
const { promptTokenCount = 0, candidatesTokenCount = 0 } = usageMetadata
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: promptTokenCount,
|
||||
outputTokens: candidatesTokenCount,
|
||||
totalCost: calculateApiCostOpenAI(model.info, promptTokenCount, candidatesTokenCount, 0, 0),
|
||||
}
|
||||
break
|
||||
case "content_block_delta":
|
||||
switch (chunk.delta.type) {
|
||||
case "thinking_delta":
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: chunk.delta.thinking,
|
||||
}
|
||||
break
|
||||
case "text_delta":
|
||||
yield {
|
||||
type: "text",
|
||||
text: chunk.delta.text,
|
||||
}
|
||||
break
|
||||
}
|
||||
break
|
||||
case "content_block_stop":
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { Content, EnhancedGenerateContentResponse, InlineDataPart, Part, TextPart } from "@google/generative-ai"
|
||||
import { Content, GenerateContentResponse, Part } from "@google/genai"
|
||||
|
||||
export function convertAnthropicContentToGemini(content: string | Anthropic.ContentBlockParam[]): Part[] {
|
||||
if (typeof content === "string") {
|
||||
return [{ text: content } as TextPart]
|
||||
return [{ text: content }]
|
||||
}
|
||||
return content.flatMap((block) => {
|
||||
return content.flatMap((block): Part => {
|
||||
switch (block.type) {
|
||||
case "text":
|
||||
return { text: block.text } as TextPart
|
||||
return { text: block.text }
|
||||
case "image":
|
||||
if (block.source.type !== "base64") {
|
||||
throw new Error("Unsupported image source type")
|
||||
@@ -18,7 +18,7 @@ export function convertAnthropicContentToGemini(content: string | Anthropic.Cont
|
||||
data: block.source.data,
|
||||
mimeType: block.source.media_type,
|
||||
},
|
||||
} as InlineDataPart
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unsupported content block type: ${block.type}`)
|
||||
}
|
||||
@@ -39,16 +39,14 @@ export function unescapeGeminiContent(content: string) {
|
||||
return content.replace(/\\n/g, "\n").replace(/\\'/g, "'").replace(/\\"/g, '"').replace(/\\r/g, "\r").replace(/\\t/g, "\t")
|
||||
}
|
||||
|
||||
export function convertGeminiResponseToAnthropic(response: EnhancedGenerateContentResponse): Anthropic.Messages.Message {
|
||||
export function convertGeminiResponseToAnthropic(response: GenerateContentResponse): Anthropic.Messages.Message {
|
||||
const content: Anthropic.Messages.ContentBlock[] = []
|
||||
|
||||
// Add the main text response
|
||||
const text = response.text()
|
||||
const text = response.text
|
||||
if (text) {
|
||||
content.push({ type: "text", text, citations: null })
|
||||
}
|
||||
|
||||
// Determine stop reason
|
||||
let stop_reason: Anthropic.Messages.Message["stop_reason"] = null
|
||||
const finishReason = response.candidates?.[0]?.finishReason
|
||||
if (finishReason) {
|
||||
@@ -64,12 +62,11 @@ export function convertGeminiResponseToAnthropic(response: EnhancedGenerateConte
|
||||
case "OTHER":
|
||||
stop_reason = "stop_sequence"
|
||||
break
|
||||
// Add more cases if needed
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: `msg_${Date.now()}`, // Generate a unique ID
|
||||
id: `msg_${Date.now()}`,
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content,
|
||||
|
||||
@@ -20,7 +20,8 @@ export async function createOpenRouterStream(
|
||||
]
|
||||
|
||||
// prompt caching: https://openrouter.ai/docs/prompt-caching
|
||||
// this is specifically for claude models (some models may 'support prompt caching' automatically without this)
|
||||
// this was initially specifically for claude models (some models may 'support prompt caching' automatically without this)
|
||||
// includes custom support for gemini which does not have iterative caching
|
||||
switch (model.id) {
|
||||
case "anthropic/claude-3.7-sonnet":
|
||||
case "anthropic/claude-3.7-sonnet:beta":
|
||||
@@ -70,6 +71,52 @@ export async function createOpenRouterStream(
|
||||
}
|
||||
})
|
||||
break
|
||||
case "google/gemini-2.5-pro-preview-03-25":
|
||||
case "google/gemini-2.0-flash-001":
|
||||
case "google/gemini-flash-1.5":
|
||||
case "google/gemini-pro-1.5":
|
||||
// gemini only uses the last breakpoint for caching, so the others will be ignored
|
||||
openAiMessages[0] = {
|
||||
role: "system",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: systemPrompt,
|
||||
// @ts-ignore-next-line
|
||||
cache_control: { type: "ephemeral" },
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const GEMINI_CACHE_USER_MESSAGE_INTERVAL = 4 // add new breakpoint every 4 turns
|
||||
const userMessages = openAiMessages.filter((msg) => msg.role === "user")
|
||||
|
||||
const userMessageCount = userMessages.length
|
||||
const targetUserMessageNumber =
|
||||
Math.floor(userMessageCount / GEMINI_CACHE_USER_MESSAGE_INTERVAL) * GEMINI_CACHE_USER_MESSAGE_INTERVAL
|
||||
|
||||
if (targetUserMessageNumber > 0) {
|
||||
// otherwise dont need to add a breakpoint
|
||||
const msg = userMessages[targetUserMessageNumber - 1]
|
||||
|
||||
if (msg) {
|
||||
if (typeof msg.content === "string") {
|
||||
msg.content = [{ type: "text", text: msg.content }]
|
||||
}
|
||||
if (Array.isArray(msg.content)) {
|
||||
// NOTE: this is fine since env details will always be added at the end. but if it weren't there, and the user added a image_url type message, it would pop a text part before it and then move it after to the end.
|
||||
let lastTextPart = msg.content.filter((part) => part.type === "text").pop()
|
||||
|
||||
if (!lastTextPart) {
|
||||
lastTextPart = { type: "text", text: "..." }
|
||||
msg.content.push(lastTextPart)
|
||||
}
|
||||
// @ts-ignore-next-line
|
||||
lastTextPart["cache_control"] = { type: "ephemeral" }
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ export const toolUseNames = [
|
||||
"load_mcp_documentation",
|
||||
"attempt_completion",
|
||||
"new_task",
|
||||
"condense",
|
||||
"new_rule",
|
||||
] as const
|
||||
|
||||
// Converts array of tool call names into a union type ("execute_command" | "read_file" | ...)
|
||||
|
||||
@@ -55,7 +55,10 @@ export function parseAssistantMessage(assistantMessage: string) {
|
||||
|
||||
// special case for write_to_file where file contents could contain the closing tag, in which case the param would have closed and we end up with the rest of the file contents here. To work around this, we get the string between the starting content tag and the LAST content tag.
|
||||
const contentParamName: ToolParamName = "content"
|
||||
if (currentToolUse.name === "write_to_file" && accumulator.endsWith(`</${contentParamName}>`)) {
|
||||
if (
|
||||
(currentToolUse.name === "write_to_file" || currentToolUse.name === "new_rule") &&
|
||||
accumulator.endsWith(`</${contentParamName}>`)
|
||||
) {
|
||||
const toolContent = accumulator.slice(currentToolUseStartIndex)
|
||||
const contentStartTag = `<${contentParamName}>`
|
||||
const contentEndTag = `</${contentParamName}>`
|
||||
|
||||
@@ -193,14 +193,20 @@ export class ContextManager {
|
||||
public getNextTruncationRange(
|
||||
apiMessages: Anthropic.Messages.MessageParam[],
|
||||
currentDeletedRange: [number, number] | undefined,
|
||||
keep: "half" | "quarter",
|
||||
keep: "none" | "lastTwo" | "half" | "quarter",
|
||||
): [number, number] {
|
||||
// We always keep the first user-assistant pairing, and truncate an even number of messages from there
|
||||
const rangeStartIndex = 2 // index 0 and 1 are kept
|
||||
const startOfRest = currentDeletedRange ? currentDeletedRange[1] + 1 : 2 // inclusive starting index
|
||||
|
||||
let messagesToRemove: number
|
||||
if (keep === "half") {
|
||||
if (keep === "none") {
|
||||
// Removes all messages beyond the first core user/assistant message pair
|
||||
messagesToRemove = Math.max(apiMessages.length - startOfRest, 0)
|
||||
} else if (keep === "lastTwo") {
|
||||
// Keep the last user-assistant pair in addition to the first core user/assistant message pair
|
||||
messagesToRemove = Math.max(apiMessages.length - startOfRest - 2, 0)
|
||||
} else if (keep === "half") {
|
||||
// Remove half of remaining user-assistant pairs
|
||||
// We first calculate half of the messages then divide by 2 to get the number of pairs.
|
||||
// After flooring, we multiply by 2 to get the number of messages.
|
||||
@@ -382,6 +388,17 @@ export class ContextManager {
|
||||
return [contextHistoryUpdated, uniqueFileReadIndices]
|
||||
}
|
||||
|
||||
/**
|
||||
* Public function for triggering potentially setting the truncation message
|
||||
* If the truncation message already exists, does nothing, otherwise adds the message
|
||||
*/
|
||||
async triggerApplyStandardContextTruncationNoticeChange(timestamp: number, taskDirectory: string) {
|
||||
const updated = this.applyStandardContextTruncationNoticeChange(timestamp)
|
||||
if (updated) {
|
||||
await this.saveContextHistory(taskDirectory)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* if there is any truncation and there is no other alteration already set, alter the assistant message to indicate this occurred
|
||||
*/
|
||||
|
||||
@@ -34,24 +34,24 @@ describe("ContextManager", () => {
|
||||
const messages = createMessages(11)
|
||||
const result = contextManager.getNextTruncationRange(messages, undefined, "half")
|
||||
|
||||
expect(result).to.deep.equal([1, 4])
|
||||
expect(result).to.deep.equal([2, 5])
|
||||
})
|
||||
|
||||
it("first truncation with quarter keep", () => {
|
||||
const messages = createMessages(11)
|
||||
const result = contextManager.getNextTruncationRange(messages, undefined, "quarter")
|
||||
|
||||
expect(result).to.deep.equal([1, 6])
|
||||
expect(result).to.deep.equal([2, 7])
|
||||
})
|
||||
|
||||
it("sequential truncation with half keep", () => {
|
||||
const messages = createMessages(21)
|
||||
const firstRange = contextManager.getNextTruncationRange(messages, undefined, "half")
|
||||
expect(firstRange).to.deep.equal([1, 10])
|
||||
expect(firstRange).to.deep.equal([2, 9])
|
||||
|
||||
// Pass the previous range for sequential truncation
|
||||
const secondRange = contextManager.getNextTruncationRange(messages, firstRange, "half")
|
||||
expect(secondRange).to.deep.equal([1, 14])
|
||||
expect(secondRange).to.deep.equal([2, 13])
|
||||
})
|
||||
|
||||
it("sequential truncation with quarter keep", () => {
|
||||
@@ -60,7 +60,7 @@ describe("ContextManager", () => {
|
||||
|
||||
const secondRange = contextManager.getNextTruncationRange(messages, firstRange, "quarter")
|
||||
|
||||
expect(secondRange[0]).to.equal(1)
|
||||
expect(secondRange[0]).to.equal(2)
|
||||
expect(secondRange[1]).to.be.greaterThan(firstRange[1])
|
||||
})
|
||||
|
||||
@@ -68,20 +68,20 @@ describe("ContextManager", () => {
|
||||
const messages = createMessages(14)
|
||||
const result = contextManager.getNextTruncationRange(messages, undefined, "half")
|
||||
|
||||
// Check if the message at the end of range is a user message
|
||||
// Check if the message at the end of range is an assistant message
|
||||
const lastRemovedMessage = messages[result[1]]
|
||||
expect(lastRemovedMessage.role).to.equal("user")
|
||||
expect(lastRemovedMessage.role).to.equal("assistant")
|
||||
|
||||
// Check if the next message after the range is an assistant message
|
||||
// Check if the next message after the range is a user message
|
||||
const nextMessage = messages[result[1] + 1]
|
||||
expect(nextMessage.role).to.equal("assistant")
|
||||
expect(nextMessage.role).to.equal("user")
|
||||
})
|
||||
|
||||
it("handles small message arrays", () => {
|
||||
const messages = createMessages(3)
|
||||
const result = contextManager.getNextTruncationRange(messages, undefined, "half")
|
||||
|
||||
expect(result).to.deep.equal([1, 0])
|
||||
expect(result).to.deep.equal([2, 1])
|
||||
})
|
||||
|
||||
it("preserves the message structure when truncating", () => {
|
||||
@@ -120,9 +120,10 @@ describe("ContextManager", () => {
|
||||
const range: [number, number] = [1, 3]
|
||||
const result = contextManager.getTruncatedMessages(messages, range)
|
||||
|
||||
expect(result).to.have.lengthOf(2)
|
||||
expect(result).to.have.lengthOf(3)
|
||||
expect(result[0]).to.deep.equal(messages[0])
|
||||
expect(result[1]).to.deep.equal(messages[4])
|
||||
expect(result[1]).to.deep.equal(messages[1])
|
||||
expect(result[2]).to.deep.equal(messages[4])
|
||||
})
|
||||
|
||||
it("works with a range that starts at the first message after task", () => {
|
||||
@@ -131,20 +132,21 @@ describe("ContextManager", () => {
|
||||
const range: [number, number] = [1, 2]
|
||||
const result = contextManager.getTruncatedMessages(messages, range)
|
||||
|
||||
expect(result).to.have.lengthOf(2)
|
||||
expect(result).to.have.lengthOf(3)
|
||||
expect(result[0]).to.deep.equal(messages[0])
|
||||
expect(result[1]).to.deep.equal(messages[3])
|
||||
expect(result[1]).to.deep.equal(messages[1])
|
||||
expect(result[2]).to.deep.equal(messages[3])
|
||||
})
|
||||
|
||||
it("correctly handles removing a range while preserving alternation pattern", () => {
|
||||
const messages = createMessages(5)
|
||||
|
||||
const range: [number, number] = [1, 2]
|
||||
const range: [number, number] = [2, 3]
|
||||
const result = contextManager.getTruncatedMessages(messages, range)
|
||||
|
||||
expect(result).to.have.lengthOf(3)
|
||||
expect(result[0]).to.deep.equal(messages[0])
|
||||
expect(result[1]).to.deep.equal(messages[3])
|
||||
expect(result[1]).to.deep.equal(messages[1])
|
||||
expect(result[2]).to.deep.equal(messages[4])
|
||||
|
||||
expect(result[0].role).to.equal("user")
|
||||
|
||||
@@ -76,26 +76,6 @@ describe("ModelContextTracker", () => {
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw an error when controller is dereferenced", async () => {
|
||||
// Create a new tracker with a controller that will be garbage collected
|
||||
const weakTracker = new ModelContextTracker(mockContext, taskId)
|
||||
|
||||
// Force the WeakRef to return null by overriding the deref method
|
||||
const weakRef = { deref: sandbox.stub().returns(null) }
|
||||
sandbox.stub(WeakRef.prototype, "deref").callsFake(() => weakRef.deref())
|
||||
|
||||
try {
|
||||
// Try to call the method - this should throw
|
||||
await weakTracker.recordModelUsage("any-provider", "any-model", "any-mode")
|
||||
|
||||
// If we get here, the test should fail
|
||||
expect.fail("Expected an error to be thrown")
|
||||
} catch (error) {
|
||||
// Verify the error message
|
||||
expect(error.message).to.equal("Unable to access extension context")
|
||||
}
|
||||
})
|
||||
|
||||
it("should append model usage to existing entries", async () => {
|
||||
// Add an existing model usage entry
|
||||
const existingTimestamp = 1617200000000
|
||||
|
||||
@@ -6,17 +6,53 @@ import fs from "fs/promises"
|
||||
import { ClineRulesToggles } from "@shared/cline-rules"
|
||||
import { getGlobalState, getWorkspaceState, updateGlobalState, updateWorkspaceState } from "@core/storage/state"
|
||||
import * as vscode from "vscode"
|
||||
import { synchronizeRuleToggles, getRuleFilesTotalContent } from "@core/context/instructions/user-instructions/rule-helpers"
|
||||
|
||||
/**
|
||||
* Converts .clinerules file to directory and places old .clinerule file inside directory, renaming it
|
||||
* Doesn't do anything if .clinerules dir already exists or doesn't exist
|
||||
* Returns whether there are any uncaught errors
|
||||
*/
|
||||
export async function ensureLocalClinerulesDirExists(cwd: string): Promise<boolean> {
|
||||
const clinerulePath = path.resolve(cwd, GlobalFileNames.clineRules)
|
||||
const defaultRuleFilename = "default-rules.md"
|
||||
|
||||
try {
|
||||
const exists = await fileExistsAtPath(clinerulePath)
|
||||
|
||||
if (exists && !(await isDirectory(clinerulePath))) {
|
||||
// logic to convert .clinerules file into directory, and rename the rules file to {defaultRuleFilename}
|
||||
const content = await fs.readFile(clinerulePath, "utf8")
|
||||
const tempPath = clinerulePath + ".bak"
|
||||
await fs.rename(clinerulePath, tempPath) // create backup
|
||||
try {
|
||||
await fs.mkdir(clinerulePath, { recursive: true })
|
||||
await fs.writeFile(path.join(clinerulePath, defaultRuleFilename), content, "utf8")
|
||||
await fs.unlink(tempPath).catch(() => {}) // delete backup
|
||||
|
||||
return false // conversion successful with no errors
|
||||
} catch (conversionError) {
|
||||
// attempt to restore backup on conversion failure
|
||||
try {
|
||||
await fs.rm(clinerulePath, { recursive: true, force: true }).catch(() => {})
|
||||
await fs.rename(tempPath, clinerulePath) // restore backup
|
||||
} catch (restoreError) {}
|
||||
return true // in either case here we consider this an error
|
||||
}
|
||||
}
|
||||
// exists and is a dir or doesn't exist, either of these cases we dont need to handle here
|
||||
return false
|
||||
} catch (error) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export const getGlobalClineRules = async (globalClineRulesFilePath: string, toggles: ClineRulesToggles) => {
|
||||
if (await fileExistsAtPath(globalClineRulesFilePath)) {
|
||||
if (await isDirectory(globalClineRulesFilePath)) {
|
||||
try {
|
||||
const rulesFilePaths = await readDirectory(globalClineRulesFilePath)
|
||||
const rulesFilesTotalContent = await getClineRulesFilesTotalContent(
|
||||
rulesFilePaths,
|
||||
globalClineRulesFilePath,
|
||||
toggles,
|
||||
)
|
||||
const rulesFilesTotalContent = await getRuleFilesTotalContent(rulesFilePaths, globalClineRulesFilePath, toggles)
|
||||
if (rulesFilesTotalContent) {
|
||||
const clineRulesFileInstructions = formatResponse.clineRulesGlobalDirectoryInstructions(
|
||||
globalClineRulesFilePath,
|
||||
@@ -45,7 +81,7 @@ export const getLocalClineRules = async (cwd: string, toggles: ClineRulesToggles
|
||||
if (await isDirectory(clineRulesFilePath)) {
|
||||
try {
|
||||
const rulesFilePaths = await readDirectory(clineRulesFilePath)
|
||||
const rulesFilesTotalContent = await getClineRulesFilesTotalContent(rulesFilePaths, cwd, toggles)
|
||||
const rulesFilesTotalContent = await getRuleFilesTotalContent(rulesFilePaths, cwd, toggles)
|
||||
if (rulesFilesTotalContent) {
|
||||
clineRulesFileInstructions = formatResponse.clineRulesLocalDirectoryInstructions(cwd, rulesFilesTotalContent)
|
||||
}
|
||||
@@ -69,86 +105,6 @@ export const getLocalClineRules = async (cwd: string, toggles: ClineRulesToggles
|
||||
return clineRulesFileInstructions
|
||||
}
|
||||
|
||||
const getClineRulesFilesTotalContent = async (rulesFilePaths: string[], basePath: string, toggles: ClineRulesToggles) => {
|
||||
const ruleFilesTotalContent = await Promise.all(
|
||||
rulesFilePaths.map(async (filePath) => {
|
||||
const ruleFilePath = path.resolve(basePath, filePath)
|
||||
const ruleFilePathRelative = path.relative(basePath, ruleFilePath)
|
||||
|
||||
if (ruleFilePath in toggles && toggles[ruleFilePath] === false) {
|
||||
return null
|
||||
}
|
||||
|
||||
return `${ruleFilePathRelative}\n` + (await fs.readFile(ruleFilePath, "utf8")).trim()
|
||||
}),
|
||||
).then((contents) => contents.filter(Boolean).join("\n\n"))
|
||||
return ruleFilesTotalContent
|
||||
}
|
||||
|
||||
export async function synchronizeRuleToggles(
|
||||
rulesDirectoryPath: string,
|
||||
currentToggles: ClineRulesToggles,
|
||||
): Promise<ClineRulesToggles> {
|
||||
// Create a copy of toggles to modify
|
||||
const updatedToggles = { ...currentToggles }
|
||||
|
||||
try {
|
||||
const pathExists = await fileExistsAtPath(rulesDirectoryPath)
|
||||
|
||||
if (pathExists) {
|
||||
const isDir = await isDirectory(rulesDirectoryPath)
|
||||
|
||||
if (isDir) {
|
||||
// DIRECTORY CASE
|
||||
const filePaths = await readDirectory(rulesDirectoryPath)
|
||||
const existingRulePaths = new Set<string>()
|
||||
|
||||
for (const filePath of filePaths) {
|
||||
const ruleFilePath = path.resolve(rulesDirectoryPath, filePath)
|
||||
existingRulePaths.add(ruleFilePath)
|
||||
|
||||
const pathHasToggle = ruleFilePath in updatedToggles
|
||||
if (!pathHasToggle) {
|
||||
updatedToggles[ruleFilePath] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up toggles for non-existent files
|
||||
for (const togglePath in updatedToggles) {
|
||||
const pathExists = existingRulePaths.has(togglePath)
|
||||
if (!pathExists) {
|
||||
delete updatedToggles[togglePath]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// FILE CASE
|
||||
// Add toggle for this file
|
||||
const pathHasToggle = rulesDirectoryPath in updatedToggles
|
||||
if (!pathHasToggle) {
|
||||
updatedToggles[rulesDirectoryPath] = true
|
||||
}
|
||||
|
||||
// Remove toggles for any other paths
|
||||
for (const togglePath in updatedToggles) {
|
||||
if (togglePath !== rulesDirectoryPath) {
|
||||
delete updatedToggles[togglePath]
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// PATH DOESN'T EXIST CASE
|
||||
// Clear all toggles since the path doesn't exist
|
||||
for (const togglePath in updatedToggles) {
|
||||
delete updatedToggles[togglePath]
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to synchronize rule toggles for path: ${rulesDirectoryPath}`, error)
|
||||
}
|
||||
|
||||
return updatedToggles
|
||||
}
|
||||
|
||||
export async function refreshClineRulesToggles(
|
||||
context: vscode.ExtensionContext,
|
||||
workingDirectory: string,
|
||||
@@ -182,7 +138,14 @@ export const createRuleFile = async (isGlobal: boolean, filename: string, cwd: s
|
||||
filePath = path.join(globalClineRulesFilePath, filename)
|
||||
} else {
|
||||
const localClineRulesFilePath = path.resolve(cwd, GlobalFileNames.clineRules)
|
||||
|
||||
const hasError = await ensureLocalClinerulesDirExists(cwd)
|
||||
if (hasError === true) {
|
||||
return { filePath: null, fileExists: false }
|
||||
}
|
||||
|
||||
await fs.mkdir(localClineRulesFilePath, { recursive: true })
|
||||
|
||||
filePath = path.join(localClineRulesFilePath, filename)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { GlobalFileNames } from "@core/storage/disk"
|
||||
import { fileExistsAtPath, isDirectory } from "@utils/fs"
|
||||
import { formatResponse } from "@core/prompts/responses"
|
||||
import { getWorkspaceState, updateWorkspaceState } from "@core/storage/state"
|
||||
import {
|
||||
synchronizeRuleToggles,
|
||||
combineRuleToggles,
|
||||
getRuleFilesTotalContent,
|
||||
readDirectoryRecursive,
|
||||
} from "@core/context/instructions/user-instructions/rule-helpers"
|
||||
import { ClineRulesToggles } from "@shared/cline-rules"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
/**
|
||||
* Refreshes the toggles for windsurf and cursor rules
|
||||
*/
|
||||
export async function refreshExternalRulesToggles(
|
||||
context: vscode.ExtensionContext,
|
||||
workingDirectory: string,
|
||||
): Promise<{
|
||||
windsurfLocalToggles: ClineRulesToggles
|
||||
cursorLocalToggles: ClineRulesToggles
|
||||
}> {
|
||||
// local windsurf toggles
|
||||
const localWindsurfRulesToggles = ((await getWorkspaceState(context, "localWindsurfRulesToggles")) as ClineRulesToggles) || {}
|
||||
const localWindsurfRulesFilePath = path.resolve(workingDirectory, GlobalFileNames.windsurfRules)
|
||||
const updatedLocalWindsurfToggles = await synchronizeRuleToggles(localWindsurfRulesFilePath, localWindsurfRulesToggles)
|
||||
await updateWorkspaceState(context, "localWindsurfRulesToggles", updatedLocalWindsurfToggles)
|
||||
|
||||
// local cursor toggles
|
||||
const localCursorRulesToggles = ((await getWorkspaceState(context, "localCursorRulesToggles")) as ClineRulesToggles) || {}
|
||||
|
||||
// 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
|
||||
let localCursorRulesFilePath = path.resolve(workingDirectory, GlobalFileNames.cursorRulesDir)
|
||||
const updatedLocalCursorToggles1 = await synchronizeRuleToggles(localCursorRulesFilePath, localCursorRulesToggles, ".mdc")
|
||||
|
||||
localCursorRulesFilePath = path.resolve(workingDirectory, GlobalFileNames.cursorRulesFile)
|
||||
const updatedLocalCursorToggles2 = await synchronizeRuleToggles(localCursorRulesFilePath, localCursorRulesToggles)
|
||||
|
||||
const updatedLocalCursorToggles = combineRuleToggles(updatedLocalCursorToggles1, updatedLocalCursorToggles2)
|
||||
await updateWorkspaceState(context, "localCursorRulesToggles", updatedLocalCursorToggles)
|
||||
|
||||
return {
|
||||
windsurfLocalToggles: updatedLocalWindsurfToggles,
|
||||
cursorLocalToggles: updatedLocalCursorToggles,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather formatted windsurf rules
|
||||
*/
|
||||
export const getLocalWindsurfRules = async (cwd: string, toggles: ClineRulesToggles) => {
|
||||
const windsurfRulesFilePath = path.resolve(cwd, GlobalFileNames.windsurfRules)
|
||||
|
||||
let windsurfRulesFileInstructions: string | undefined
|
||||
|
||||
if (await fileExistsAtPath(windsurfRulesFilePath)) {
|
||||
if (!(await isDirectory(windsurfRulesFilePath))) {
|
||||
try {
|
||||
if (windsurfRulesFilePath in toggles && toggles[windsurfRulesFilePath] !== false) {
|
||||
const ruleFileContent = (await fs.readFile(windsurfRulesFilePath, "utf8")).trim()
|
||||
if (ruleFileContent) {
|
||||
windsurfRulesFileInstructions = formatResponse.windsurfRulesLocalFileInstructions(cwd, ruleFileContent)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
console.error(`Failed to read .windsurfrules file at ${windsurfRulesFilePath}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return windsurfRulesFileInstructions
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather formatted cursor rules, which can come from two sources
|
||||
*/
|
||||
export const getLocalCursorRules = async (cwd: string, toggles: ClineRulesToggles) => {
|
||||
// we first check for the .cursorrules file
|
||||
const cursorRulesFilePath = path.resolve(cwd, GlobalFileNames.cursorRulesFile)
|
||||
let cursorRulesFileInstructions: string | undefined
|
||||
|
||||
if (await fileExistsAtPath(cursorRulesFilePath)) {
|
||||
if (!(await isDirectory(cursorRulesFilePath))) {
|
||||
try {
|
||||
if (cursorRulesFilePath in toggles && toggles[cursorRulesFilePath] !== false) {
|
||||
const ruleFileContent = (await fs.readFile(cursorRulesFilePath, "utf8")).trim()
|
||||
if (ruleFileContent) {
|
||||
cursorRulesFileInstructions = formatResponse.cursorRulesLocalFileInstructions(cwd, ruleFileContent)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
console.error(`Failed to read .cursorrules file at ${cursorRulesFilePath}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// we then check for the .cursor/rules dir
|
||||
const cursorRulesDirPath = path.resolve(cwd, GlobalFileNames.cursorRulesDir)
|
||||
let cursorRulesDirInstructions: string | undefined
|
||||
|
||||
if (await fileExistsAtPath(cursorRulesDirPath)) {
|
||||
if (await isDirectory(cursorRulesDirPath)) {
|
||||
try {
|
||||
const rulesFilePaths = await readDirectoryRecursive(cursorRulesDirPath, ".mdc")
|
||||
const rulesFilesTotalContent = await getRuleFilesTotalContent(rulesFilePaths, cwd, toggles)
|
||||
if (rulesFilesTotalContent) {
|
||||
cursorRulesDirInstructions = formatResponse.cursorRulesLocalDirectoryInstructions(cwd, rulesFilesTotalContent)
|
||||
}
|
||||
} catch {
|
||||
console.error(`Failed to read .cursor/rules directory at ${cursorRulesDirPath}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [cursorRulesFileInstructions, cursorRulesDirInstructions]
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import { fileExistsAtPath, isDirectory, readDirectory } from "@utils/fs"
|
||||
import * as path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { ClineRulesToggles } from "@shared/cline-rules"
|
||||
|
||||
/**
|
||||
* Recursively traverses directory and finds all files, including checking for optional whitelisted file extension
|
||||
*/
|
||||
export async function readDirectoryRecursive(directoryPath: string, allowedFileExtension: string): Promise<string[]> {
|
||||
try {
|
||||
const entries = await readDirectory(directoryPath)
|
||||
let results: string[] = []
|
||||
for (const entry of entries) {
|
||||
if (allowedFileExtension !== "") {
|
||||
const fileExtension = path.extname(entry)
|
||||
if (fileExtension !== allowedFileExtension) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
results.push(entry)
|
||||
}
|
||||
return results
|
||||
} catch (error) {
|
||||
console.error(`Error reading directory ${directoryPath}: ${error}`)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the up to date toggles
|
||||
*/
|
||||
export async function synchronizeRuleToggles(
|
||||
rulesDirectoryPath: string,
|
||||
currentToggles: ClineRulesToggles,
|
||||
allowedFileExtension: string = "",
|
||||
): Promise<ClineRulesToggles> {
|
||||
// Create a copy of toggles to modify
|
||||
const updatedToggles = { ...currentToggles }
|
||||
|
||||
try {
|
||||
const pathExists = await fileExistsAtPath(rulesDirectoryPath)
|
||||
|
||||
if (pathExists) {
|
||||
const isDir = await isDirectory(rulesDirectoryPath)
|
||||
|
||||
if (isDir) {
|
||||
// DIRECTORY CASE
|
||||
const filePaths = await readDirectoryRecursive(rulesDirectoryPath, allowedFileExtension)
|
||||
const existingRulePaths = new Set<string>()
|
||||
|
||||
for (const filePath of filePaths) {
|
||||
const ruleFilePath = path.resolve(rulesDirectoryPath, filePath)
|
||||
existingRulePaths.add(ruleFilePath)
|
||||
|
||||
const pathHasToggle = ruleFilePath in updatedToggles
|
||||
if (!pathHasToggle) {
|
||||
updatedToggles[ruleFilePath] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up toggles for non-existent files
|
||||
for (const togglePath in updatedToggles) {
|
||||
const pathExists = existingRulePaths.has(togglePath)
|
||||
if (!pathExists) {
|
||||
delete updatedToggles[togglePath]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// FILE CASE
|
||||
// Add toggle for this file
|
||||
const pathHasToggle = rulesDirectoryPath in updatedToggles
|
||||
if (!pathHasToggle) {
|
||||
updatedToggles[rulesDirectoryPath] = true
|
||||
}
|
||||
|
||||
// Remove toggles for any other paths
|
||||
for (const togglePath in updatedToggles) {
|
||||
if (togglePath !== rulesDirectoryPath) {
|
||||
delete updatedToggles[togglePath]
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// PATH DOESN'T EXIST CASE
|
||||
// Clear all toggles since the path doesn't exist
|
||||
for (const togglePath in updatedToggles) {
|
||||
delete updatedToggles[togglePath]
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to synchronize rule toggles for path: ${rulesDirectoryPath}`, error)
|
||||
}
|
||||
|
||||
return updatedToggles
|
||||
}
|
||||
|
||||
/**
|
||||
* Certain project rules have more than a single location where rules are allowed to be stored
|
||||
*/
|
||||
export function combineRuleToggles(toggles1: ClineRulesToggles, toggles2: ClineRulesToggles): ClineRulesToggles {
|
||||
return { ...toggles1, ...toggles2 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the content of rules files
|
||||
*/
|
||||
export const getRuleFilesTotalContent = async (rulesFilePaths: string[], basePath: string, toggles: ClineRulesToggles) => {
|
||||
const ruleFilesTotalContent = await Promise.all(
|
||||
rulesFilePaths.map(async (filePath) => {
|
||||
const ruleFilePath = path.resolve(basePath, filePath)
|
||||
const ruleFilePathRelative = path.relative(basePath, ruleFilePath)
|
||||
|
||||
if (ruleFilePath in toggles && toggles[ruleFilePath] === false) {
|
||||
return null
|
||||
}
|
||||
|
||||
return `${ruleFilePathRelative}\n` + (await fs.readFile(ruleFilePath, "utf8")).trim()
|
||||
}),
|
||||
).then((contents) => contents.filter(Boolean).join("\n\n"))
|
||||
return ruleFilesTotalContent
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import * as vscode from "vscode"
|
||||
import crypto from "crypto"
|
||||
import { Controller } from "../index"
|
||||
import { storeSecret } from "../../storage/state"
|
||||
|
||||
/**
|
||||
* Handles the user clicking the login link in the UI.
|
||||
* Generates a secure nonce for state validation, stores it in secrets,
|
||||
* and opens the authentication URL in the external browser.
|
||||
*
|
||||
* @param controller The controller instance.
|
||||
* @returns The login URL as a string.
|
||||
*/
|
||||
export async function accountLoginClicked(controller: Controller): Promise<String> {
|
||||
// Generate nonce for state validation
|
||||
const nonce = crypto.randomBytes(32).toString("hex")
|
||||
await storeSecret(controller.context, "authNonce", nonce)
|
||||
|
||||
// Open browser for authentication with state param
|
||||
console.log("Login button clicked in account page")
|
||||
console.log("Opening auth page with state param")
|
||||
|
||||
const uriScheme = vscode.env.uriScheme
|
||||
|
||||
const authUrl = vscode.Uri.parse(
|
||||
`https://app.cline.bot/auth?state=${encodeURIComponent(nonce)}&callback_url=${encodeURIComponent(`${uriScheme || "vscode"}://saoudrizwan.claude-dev/auth`)}`,
|
||||
)
|
||||
vscode.env.openExternal(authUrl)
|
||||
return authUrl.toString()
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createServiceRegistry, ServiceMethodHandler } from "../grpc-service"
|
||||
import { registerAllMethods } from "./methods"
|
||||
|
||||
const accountService = createServiceRegistry("account")
|
||||
|
||||
// Export the method handler type and registration function
|
||||
export type AccountMethodHandler = ServiceMethodHandler
|
||||
export const registerMethod = accountService.registerMethod
|
||||
|
||||
// Export the request handler
|
||||
export const handleAccountServiceRequest = accountService.handleRequest
|
||||
|
||||
// Register all account methods
|
||||
registerAllMethods()
|
||||
@@ -0,0 +1,12 @@
|
||||
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by proto/build-proto.js
|
||||
|
||||
// Import all method implementations
|
||||
import { registerMethod } from "./index"
|
||||
import { accountLoginClicked } from "./accountLoginClicked"
|
||||
|
||||
// Register all account service methods
|
||||
export function registerAllMethods(): void {
|
||||
// Register each method with the registry
|
||||
registerMethod("accountLoginClicked", accountLoginClicked)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { ChromePath } from "../../../shared/proto/browser"
|
||||
import { EmptyRequest } from "../../../shared/proto/common"
|
||||
import { Controller } from "../index"
|
||||
import { getAllExtensionState } from "../../storage/state"
|
||||
import { BrowserSession } from "../../../services/browser/BrowserSession"
|
||||
|
||||
/**
|
||||
* Get the detected Chrome executable path
|
||||
* @param controller The controller instance
|
||||
* @param request The empty request message
|
||||
* @returns The detected Chrome path and whether it's bundled
|
||||
*/
|
||||
export async function getDetectedChromePath(controller: Controller, request: EmptyRequest): Promise<ChromePath> {
|
||||
try {
|
||||
const { browserSettings } = await getAllExtensionState(controller.context)
|
||||
const browserSession = new BrowserSession(controller.context, browserSettings)
|
||||
const result = await browserSession.getDetectedChromePath()
|
||||
|
||||
return {
|
||||
path: result.path,
|
||||
isBundled: result.isBundled,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error getting detected Chrome path:", error)
|
||||
return {
|
||||
path: "",
|
||||
isBundled: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,16 @@
|
||||
import { registerMethod } from "./index"
|
||||
import { discoverBrowser } from "./discoverBrowser"
|
||||
import { getBrowserConnectionInfo } from "./getBrowserConnectionInfo"
|
||||
import { getDetectedChromePath } from "./getDetectedChromePath"
|
||||
import { testBrowserConnection } from "./testBrowserConnection"
|
||||
import { updateBrowserSettings } from "./updateBrowserSettings"
|
||||
|
||||
// Register all browser service methods
|
||||
export function registerAllMethods(): void {
|
||||
// Register each method with the registry
|
||||
registerMethod("discoverBrowser", discoverBrowser)
|
||||
registerMethod("getBrowserConnectionInfo", getBrowserConnectionInfo)
|
||||
registerMethod("getDetectedChromePath", getDetectedChromePath)
|
||||
registerMethod("testBrowserConnection", testBrowserConnection)
|
||||
registerMethod("updateBrowserSettings", updateBrowserSettings)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { UpdateBrowserSettingsRequest } from "../../../shared/proto/browser"
|
||||
import { Boolean } from "../../../shared/proto/common"
|
||||
import { Controller } from "../index"
|
||||
import { updateGlobalState } from "../../storage/state"
|
||||
import { BrowserSettings as SharedBrowserSettings } from "../../../shared/BrowserSettings"
|
||||
|
||||
/**
|
||||
* Update browser settings
|
||||
* @param controller The controller instance
|
||||
* @param request The browser settings request message
|
||||
* @returns Success response
|
||||
*/
|
||||
export async function updateBrowserSettings(controller: Controller, request: UpdateBrowserSettingsRequest): Promise<Boolean> {
|
||||
try {
|
||||
// Convert from protobuf format to shared format
|
||||
const browserSettings: SharedBrowserSettings = {
|
||||
viewport: {
|
||||
width: request.viewport?.width || 900,
|
||||
height: request.viewport?.height || 600,
|
||||
},
|
||||
remoteBrowserEnabled: request.remoteBrowserEnabled || false,
|
||||
remoteBrowserHost: request.remoteBrowserHost || undefined,
|
||||
}
|
||||
|
||||
// Update global state with new settings
|
||||
await updateGlobalState(controller.context, "browserSettings", browserSettings)
|
||||
|
||||
// Update task browser settings if task exists
|
||||
if (controller.task) {
|
||||
controller.task.browserSettings = browserSettings
|
||||
controller.task.browserSession.browserSettings = browserSettings
|
||||
}
|
||||
|
||||
// Post updated state to webview
|
||||
await controller.postStateToWebview()
|
||||
|
||||
return {
|
||||
value: true,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating browser settings:", error)
|
||||
return {
|
||||
value: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Controller } from ".."
|
||||
import { ClineCheckpointRestore } from "../../../shared/WebviewMessage"
|
||||
import { CheckpointRestoreRequest } from "../../../shared/proto/checkpoints"
|
||||
import { Empty } from "../../../shared/proto/common"
|
||||
import pWaitFor from "p-wait-for"
|
||||
|
||||
export async function checkpointRestore(controller: Controller, request: CheckpointRestoreRequest): Promise<Empty> {
|
||||
await controller.cancelTask() // we cannot alter message history say if the task is active, as it could be in the middle of editing a file or running a command, which expect the ask to be responded to rather than being superseded by a new message eg add deleted_api_reqs
|
||||
|
||||
if (request.number) {
|
||||
// wait for messages to be loaded
|
||||
await pWaitFor(() => controller.task?.isInitialized === true, {
|
||||
timeout: 3_000,
|
||||
}).catch(() => {
|
||||
console.error("Failed to init new cline instance")
|
||||
})
|
||||
|
||||
// NOTE: cancelTask awaits abortTask, which awaits diffViewProvider.revertChanges, which reverts any edited files, allowing us to reset to a checkpoint rather than running into a state where the revertChanges function is called alongside or after the checkpoint reset
|
||||
await controller.task?.restoreCheckpoint(request.number, request.restoreType as ClineCheckpointRestore, request.offset)
|
||||
}
|
||||
return {}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ export type CheckpointsMethodHandler = ServiceMethodHandler
|
||||
export const registerMethod = checkpointsService.registerMethod
|
||||
|
||||
// Export the request handler
|
||||
export const handleCheckpointsDiffServiceRequest = checkpointsService.handleRequest
|
||||
export const handleCheckpointsServiceRequest = checkpointsService.handleRequest
|
||||
|
||||
// Register all checkpoints methods
|
||||
registerAllMethods()
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
// Import all method implementations
|
||||
import { registerMethod } from "./index"
|
||||
import { checkpointDiff } from "./checkpointDiff"
|
||||
import { checkpointRestore } from "./checkpointRestore"
|
||||
|
||||
// Register all checkpoints service methods
|
||||
export function registerAllMethods(): void {
|
||||
// Register each method with the registry
|
||||
registerMethod("checkpointDiff", checkpointDiff)
|
||||
registerMethod("checkpointRestore", checkpointRestore)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Controller } from ".."
|
||||
import { RuleFileRequest, RuleFile } from "@shared/proto/file"
|
||||
import { FileMethodHandler } from "./index"
|
||||
import {
|
||||
createRuleFile as createRuleFileImpl,
|
||||
refreshClineRulesToggles,
|
||||
} from "@core/context/instructions/user-instructions/cline-rules"
|
||||
import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
import { handleFileServiceRequest } from "./index"
|
||||
import { cwd } from "@core/task"
|
||||
|
||||
/**
|
||||
* Creates a rule file in either global or workspace rules directory
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing filename and isGlobal flag
|
||||
* @returns Result with file path and display name
|
||||
* @throws Error if operation fails
|
||||
*/
|
||||
export const createRuleFile: FileMethodHandler = async (controller: Controller, request: RuleFileRequest): Promise<RuleFile> => {
|
||||
if (typeof request.isGlobal !== "boolean" || typeof request.filename !== "string" || !request.filename) {
|
||||
console.error("createRuleFile: Missing or invalid parameters", {
|
||||
isGlobal: typeof request.isGlobal === "boolean" ? request.isGlobal : `Invalid: ${typeof request.isGlobal}`,
|
||||
filename: typeof request.filename === "string" ? request.filename : `Invalid: ${typeof request.filename}`,
|
||||
})
|
||||
throw new Error("Missing or invalid parameters")
|
||||
}
|
||||
|
||||
const { filePath, fileExists } = await createRuleFileImpl(request.isGlobal, request.filename, cwd)
|
||||
|
||||
if (!filePath) {
|
||||
throw new Error("Failed to create rule file.")
|
||||
}
|
||||
|
||||
if (fileExists) {
|
||||
vscode.window.showWarningMessage(`Rule file "${request.filename}" already exists.`)
|
||||
// Still open it for editing
|
||||
await handleFileServiceRequest(controller, "openFile", { value: filePath })
|
||||
} else {
|
||||
await refreshClineRulesToggles(controller.context, cwd)
|
||||
await controller.postStateToWebview()
|
||||
|
||||
await handleFileServiceRequest(controller, "openFile", { value: filePath })
|
||||
|
||||
vscode.window.showInformationMessage(
|
||||
`Created new ${request.isGlobal ? "global" : "workspace"} rule file: ${request.filename}`,
|
||||
)
|
||||
}
|
||||
|
||||
return RuleFile.create({
|
||||
filePath: filePath,
|
||||
displayName: path.basename(filePath),
|
||||
alreadyExists: fileExists,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Controller } from ".."
|
||||
import { RuleFileRequest, RuleFile } from "@shared/proto/file"
|
||||
import { FileMethodHandler } from "./index"
|
||||
import {
|
||||
deleteRuleFile as deleteRuleFileImpl,
|
||||
refreshClineRulesToggles,
|
||||
} from "@core/context/instructions/user-instructions/cline-rules"
|
||||
import { refreshExternalRulesToggles } from "@core/context/instructions/user-instructions/external-rules"
|
||||
import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
import { cwd } from "@core/task"
|
||||
|
||||
/**
|
||||
* Deletes a rule file from either global or workspace rules directory
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing rule path and isGlobal flag
|
||||
* @returns Result with file path and display name
|
||||
* @throws Error if operation fails
|
||||
*/
|
||||
export const deleteRuleFile: FileMethodHandler = async (controller: Controller, request: RuleFileRequest): Promise<RuleFile> => {
|
||||
if (typeof request.isGlobal !== "boolean" || typeof request.rulePath !== "string" || !request.rulePath) {
|
||||
console.error("deleteRuleFile: Missing or invalid parameters", {
|
||||
isGlobal: typeof request.isGlobal === "boolean" ? request.isGlobal : `Invalid: ${typeof request.isGlobal}`,
|
||||
rulePath: typeof request.rulePath === "string" ? request.rulePath : `Invalid: ${typeof request.rulePath}`,
|
||||
})
|
||||
throw new Error("Missing or invalid parameters")
|
||||
}
|
||||
|
||||
const result = await deleteRuleFileImpl(controller.context, request.rulePath, request.isGlobal)
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.message || "Failed to delete rule file")
|
||||
}
|
||||
|
||||
await refreshClineRulesToggles(controller.context, cwd)
|
||||
await refreshExternalRulesToggles(controller.context, cwd)
|
||||
await controller.postStateToWebview()
|
||||
|
||||
const fileName = path.basename(request.rulePath)
|
||||
vscode.window.showInformationMessage(`Rule file "${fileName}" deleted successfully`)
|
||||
|
||||
return RuleFile.create({
|
||||
filePath: request.rulePath,
|
||||
displayName: fileName,
|
||||
alreadyExists: false,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { createServiceRegistry, ServiceMethodHandler } from "../grpc-service"
|
||||
import { registerAllMethods } from "./methods"
|
||||
|
||||
// Create file service registry
|
||||
const fileService = createServiceRegistry("file")
|
||||
|
||||
// Export the method handler type and registration function
|
||||
export type FileMethodHandler = ServiceMethodHandler
|
||||
export const registerMethod = fileService.registerMethod
|
||||
|
||||
// Export the request handler
|
||||
export const handleFileServiceRequest = fileService.handleRequest
|
||||
|
||||
// Register all file methods
|
||||
registerAllMethods()
|
||||
@@ -0,0 +1,20 @@
|
||||
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by proto/build-proto.js
|
||||
|
||||
// Import all method implementations
|
||||
import { registerMethod } from "./index"
|
||||
import { createRuleFile } from "./createRuleFile"
|
||||
import { deleteRuleFile } from "./deleteRuleFile"
|
||||
import { openFile } from "./openFile"
|
||||
import { openImage } from "./openImage"
|
||||
import { searchCommits } from "./searchCommits"
|
||||
|
||||
// Register all file service methods
|
||||
export function registerAllMethods(): void {
|
||||
// Register each method with the registry
|
||||
registerMethod("createRuleFile", createRuleFile)
|
||||
registerMethod("deleteRuleFile", deleteRuleFile)
|
||||
registerMethod("openFile", openFile)
|
||||
registerMethod("openImage", openImage)
|
||||
registerMethod("searchCommits", searchCommits)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user