mirror of
https://github.com/cline/cline.git
synced 2026-09-11 05:47:07 +08:00
Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6226e2c48b | ||
|
|
faeee37d49 | ||
|
|
1700c0e4f8 | ||
|
|
c535a5ec73 | ||
|
|
644280bbb4 | ||
|
|
43357c1100 | ||
|
|
eb19731843 | ||
|
|
95750f8c9c | ||
|
|
12820a4042 | ||
|
|
8e3adb42d6 | ||
|
|
8ab35a5b06 | ||
|
|
ba64d9fafb | ||
|
|
2ba2b5b264 | ||
|
|
0dad8e178a | ||
|
|
1470563142 | ||
|
|
0ca16961ee | ||
|
|
8d8452e668 | ||
|
|
6c18d5154f | ||
|
|
aabe4ae1e1 | ||
|
|
5147e28aaf | ||
|
|
c6e8b04b86 | ||
|
|
c0b3c69a8f | ||
|
|
080ed7c1c6 | ||
|
|
570ece3284 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
taskCompletionViewChanges protobus migration
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
copyToClipboard protobus migration
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Finishing the migration of Vscode Advanced settings to Settings Webview
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
migrate accountLogoutClicked to protobus
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Migrate restartMcpServer to protobus
|
||||
@@ -1,2 +1,4 @@
|
||||
demo.gif filter=lfs diff=lfs merge=lfs -text
|
||||
assets/docs/demo.gif filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
* text=auto eol=lf
|
||||
|
||||
@@ -15,7 +15,15 @@ permissions:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: ${{ matrix.os == 'ubuntu-latest' && 'test' || format('test ({0})', matrix.os) }}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@@ -60,6 +68,11 @@ jobs:
|
||||
if: steps.webview-cache.outputs.cache-hit != 'true'
|
||||
run: cd webview-ui && npm ci
|
||||
|
||||
- name: Set up NPM on Windows
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
npm config set script-shell "C:\\Program Files\\Git\\bin\\bash.exe"
|
||||
|
||||
- name: Type Check
|
||||
run: npm run check-types
|
||||
|
||||
@@ -81,8 +94,9 @@ jobs:
|
||||
id: extension_coverage
|
||||
continue-on-error: true
|
||||
run: |
|
||||
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
|
||||
node ./scripts/test-ci.js > extension_coverage.txt 2>&1
|
||||
# Default the encoding to UTF-8 - It's not the default on Windows
|
||||
PYTHONUTF8=1 PYTHONPATH=.github/scripts python -m coverage_check extract-coverage extension_coverage.txt --type=extension --github-output --verbose
|
||||
|
||||
# Run webview tests with coverage
|
||||
- name: Webview Tests with Coverage
|
||||
@@ -92,13 +106,16 @@ jobs:
|
||||
cd webview-ui
|
||||
# Ensure coverage dependency is installed
|
||||
npm install --no-save @vitest/coverage-v8
|
||||
npm run test:coverage > webview_coverage.txt 2>&1 || true
|
||||
npm run test:coverage > webview_coverage.txt 2>&1
|
||||
cd ..
|
||||
PYTHONPATH=.github/scripts python -m coverage_check extract-coverage webview-ui/webview_coverage.txt --type=webview --github-output --verbose
|
||||
# Default the encoding to UTF-8 - It's not the default on Windows
|
||||
PYTHONUTF8=1 PYTHONPATH=.github/scripts python -m coverage_check extract-coverage webview-ui/webview_coverage.txt --type=webview --github-output --verbose
|
||||
|
||||
# Save coverage reports as artifacts (workflow-scoped)
|
||||
- name: Save Coverage Reports
|
||||
uses: actions/upload-artifact@v4
|
||||
# Only upload artifacts on Linux - We only need coverage from one OS
|
||||
if: runner.os == 'Linux'
|
||||
with:
|
||||
name: pr-coverage-reports
|
||||
path: |
|
||||
@@ -107,14 +124,18 @@ jobs:
|
||||
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
|
||||
- name: Print test results and check for failures
|
||||
run: |
|
||||
echo "Extension Tests Result: ${{ steps.extension_coverage.outcome }}"
|
||||
cat extension_coverage.txt
|
||||
|
||||
echo "Webview Tests Result: ${{ steps.webview_coverage.outcome }}"
|
||||
cat webview-ui/webview_coverage.txt
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
+2
-1
@@ -3,5 +3,6 @@
|
||||
"useTabs": true,
|
||||
"printWidth": 130,
|
||||
"semi": false,
|
||||
"bracketSameLine": true
|
||||
"bracketSameLine": true,
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
|
||||
Vendored
+6
-1
@@ -1,5 +1,10 @@
|
||||
{
|
||||
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": ["dbaeumer.vscode-eslint", "connor4312.esbuild-problem-matchers", "ms-vscode.extension-test-runner"]
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"connor4312.esbuild-problem-matchers",
|
||||
"ms-vscode.extension-test-runner",
|
||||
"bradlc.vscode-tailwindcss"
|
||||
]
|
||||
}
|
||||
|
||||
+20
-1
@@ -1,8 +1,27 @@
|
||||
# Changelog
|
||||
|
||||
## [3.16.1]
|
||||
|
||||
- Add Enable auto approve toggle switch, allowing users to easily turn auto-approve functionality on or off without losing their action settings
|
||||
- Improve Gemini retry handling with better UI feedback, showing retry progress during API request attempts
|
||||
- Fix memory leak issue that could occur during long sessions with multiple tasks
|
||||
- Improve UI for Gemini model retry attempts with clearer status updates
|
||||
- Fix quick actions functionality in auto-approve settings
|
||||
- Update UI styling for auto-approve menu items to conserve space
|
||||
|
||||
## [3.16.0]
|
||||
|
||||
- Add new workflow feature allowing users to create and manage workflow files that can be injected into conversations via slash commands
|
||||
- Add collapsible recent task list, allowing users to hide their task history when sharing their screen (Thanks @cosmix!)
|
||||
- Add global endpoint option for Vertex AI users, providing higher availability and reducing 429 errors (Thanks @soniqua!)
|
||||
- Add detection for new users to display special components and guidance
|
||||
- Add Tailwind CSS IntelliSense to the recommended extensions list
|
||||
- Fix eternal loading states when the last message is a checkpoint (Thanks @BarreiroT!)
|
||||
- Improve settings organization by migrating VSCode Advanced settings to Settings Webview
|
||||
|
||||
## [3.15.5]
|
||||
|
||||
- Fix inefficient memory management in the task timeline
|
||||
- Fix inefficient memory management in the task timeline
|
||||
- Fix Gemini rate limitation response not being handled properly (Thanks @BarreiroT!)
|
||||
|
||||
## [3.15.4]
|
||||
|
||||
@@ -44,16 +44,16 @@ This guide is tailored for organizations with established GCP environments (leve
|
||||
|
||||
#### 2.1 Choose and Confirm a Region
|
||||
|
||||
Vertex AI supports eight regions. Select a region that meets your latency, compliance, and capacity needs. Examples include:
|
||||
Vertex AI supports multiple regions. Select a region that meets your latency, compliance, and capacity needs. Examples include:
|
||||
|
||||
- **us-east5 (Columbus, Ohio)**
|
||||
- **us-east1 (South Carolina)**
|
||||
- **us-east4 (Northern Virginia)**
|
||||
- **us-central1 (Iowa)**
|
||||
- **us-west1 (The Dalles, Oregon)**
|
||||
- **us-west4 (Las Vegas, Nevada)**
|
||||
- **europe-west1 (Belgium)**
|
||||
- **europe-west4 (Netherlands)**
|
||||
- **asia-southeast1 (Singapore)**
|
||||
- **global (Global)**
|
||||
|
||||
The Global endpoint may offer higher availability and reduce resource exhausted errors. Only Gemini models are supported.
|
||||
|
||||
#### 2.2 Enable the Claude 3.5 Sonnet v2 Model
|
||||
|
||||
|
||||
Generated
+18
-18
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-dev",
|
||||
"version": "3.15.5",
|
||||
"version": "3.16.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-dev",
|
||||
"version": "3.15.5",
|
||||
"version": "3.16.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/bedrock-sdk": "^0.12.4",
|
||||
@@ -15,7 +15,7 @@
|
||||
"@aws-sdk/client-bedrock-runtime": "^3.758.0",
|
||||
"@bufbuild/protobuf": "^2.2.5",
|
||||
"@google-cloud/vertexai": "^1.9.3",
|
||||
"@google/genai": "^0.9.0",
|
||||
"@google/genai": "^0.13.0",
|
||||
"@grpc/grpc-js": "^1.9.15",
|
||||
"@grpc/reflection": "^1.0.4",
|
||||
"@mistralai/mistralai": "^1.5.0",
|
||||
@@ -86,8 +86,8 @@
|
||||
"@types/vscode": "^1.84.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.14.1",
|
||||
"@typescript-eslint/parser": "^7.11.0",
|
||||
"@vscode/test-cli": "^0.0.9",
|
||||
"@vscode/test-electron": "^2.4.0",
|
||||
"@vscode/test-cli": "^0.0.10",
|
||||
"@vscode/test-electron": "^2.4.1",
|
||||
"chai": "^4.3.10",
|
||||
"chalk": "^5.3.0",
|
||||
"esbuild": "^0.25.0",
|
||||
@@ -5295,9 +5295,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@google/genai": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-0.9.0.tgz",
|
||||
"integrity": "sha512-FD2RizYGInsvfjeaN6O+wQGpRnGVglS1XWrGQr8K7D04AfMmvPodDSw94U9KyFtsVLzWH9kmlPyFM+G4jbmkqg==",
|
||||
"version": "0.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-0.13.0.tgz",
|
||||
"integrity": "sha512-eaEncWt875H7046T04mOpxpHJUM+jLIljEf+5QctRyOeChylE/nhpwm1bZWTRWoOu/t46R9r+PmgsJFhTpE7tQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"google-auth-library": "^9.14.2",
|
||||
@@ -11473,9 +11473,9 @@
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/@vscode/test-cli": {
|
||||
"version": "0.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/test-cli/-/test-cli-0.0.9.tgz",
|
||||
"integrity": "sha512-vsl5/ueE3Jf0f6XzB0ECHHMsd5A0Yu6StElb8a+XsubZW7kHNAOw4Y3TSSuDzKEpLnJ92nbMy1Zl+KLGCE6NaA==",
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/test-cli/-/test-cli-0.0.10.tgz",
|
||||
"integrity": "sha512-B0mMH4ia+MOOtwNiLi79XhA+MLmUItIC8FckEuKrVAVriIuSWjt7vv4+bF8qVFiNFe4QRfzPaIZk39FZGWEwHA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -29924,9 +29924,9 @@
|
||||
}
|
||||
},
|
||||
"@google/genai": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-0.9.0.tgz",
|
||||
"integrity": "sha512-FD2RizYGInsvfjeaN6O+wQGpRnGVglS1XWrGQr8K7D04AfMmvPodDSw94U9KyFtsVLzWH9kmlPyFM+G4jbmkqg==",
|
||||
"version": "0.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-0.13.0.tgz",
|
||||
"integrity": "sha512-eaEncWt875H7046T04mOpxpHJUM+jLIljEf+5QctRyOeChylE/nhpwm1bZWTRWoOu/t46R9r+PmgsJFhTpE7tQ==",
|
||||
"requires": {
|
||||
"google-auth-library": "^9.14.2",
|
||||
"ws": "^8.18.0",
|
||||
@@ -34535,9 +34535,9 @@
|
||||
"integrity": "sha512-wsNOvNMMJ2BY8rC2N2MNBG7yOowV3ov8KlvUE/AiVUlHKTfWsw3OgAOQduX7h0Un6GssKD3aoTVH+TF3DSQwKQ=="
|
||||
},
|
||||
"@vscode/test-cli": {
|
||||
"version": "0.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/test-cli/-/test-cli-0.0.9.tgz",
|
||||
"integrity": "sha512-vsl5/ueE3Jf0f6XzB0ECHHMsd5A0Yu6StElb8a+XsubZW7kHNAOw4Y3TSSuDzKEpLnJ92nbMy1Zl+KLGCE6NaA==",
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/test-cli/-/test-cli-0.0.10.tgz",
|
||||
"integrity": "sha512-B0mMH4ia+MOOtwNiLi79XhA+MLmUItIC8FckEuKrVAVriIuSWjt7vv4+bF8qVFiNFe4QRfzPaIZk39FZGWEwHA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/mocha": "^10.0.2",
|
||||
@@ -44188,4 +44188,4 @@
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-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.15.5",
|
||||
"version": "3.16.1",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
@@ -292,8 +292,8 @@
|
||||
"@types/vscode": "^1.84.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.14.1",
|
||||
"@typescript-eslint/parser": "^7.11.0",
|
||||
"@vscode/test-cli": "^0.0.9",
|
||||
"@vscode/test-electron": "^2.4.0",
|
||||
"@vscode/test-cli": "^0.0.10",
|
||||
"@vscode/test-electron": "^2.4.1",
|
||||
"chai": "^4.3.10",
|
||||
"chalk": "^5.3.0",
|
||||
"esbuild": "^0.25.0",
|
||||
@@ -319,7 +319,7 @@
|
||||
"@aws-sdk/client-bedrock-runtime": "^3.758.0",
|
||||
"@bufbuild/protobuf": "^2.2.5",
|
||||
"@google-cloud/vertexai": "^1.9.3",
|
||||
"@google/genai": "^0.9.0",
|
||||
"@google/genai": "^0.13.0",
|
||||
"@grpc/grpc-js": "^1.9.15",
|
||||
"@grpc/reflection": "^1.0.4",
|
||||
"@mistralai/mistralai": "^1.5.0",
|
||||
|
||||
@@ -12,4 +12,8 @@ service AccountService {
|
||||
// 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);
|
||||
|
||||
// Handles the user clicking the logout button in the UI.
|
||||
// Clears API keys and user state.
|
||||
rpc accountLogoutClicked(EmptyRequest) returns (Empty);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ import "common.proto";
|
||||
|
||||
// Service for file-related operations
|
||||
service FileService {
|
||||
// Copies text to clipboard
|
||||
rpc copyToClipboard(StringRequest) returns (Empty);
|
||||
|
||||
// Opens a file in the editor
|
||||
rpc openFile(StringRequest) returns (Empty);
|
||||
|
||||
@@ -22,6 +25,9 @@ service FileService {
|
||||
|
||||
// Search git commits in the workspace
|
||||
rpc searchCommits(StringRequest) returns (GitCommits);
|
||||
|
||||
// Select images from the file system and return as data URLs
|
||||
rpc selectImages(EmptyRequest) returns (StringArray);
|
||||
|
||||
// Convert URIs to workspace-relative paths
|
||||
rpc getRelativePaths(RelativePathsRequest) returns (RelativePaths);
|
||||
@@ -82,6 +88,7 @@ message RuleFileRequest {
|
||||
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)
|
||||
optional string type = 5; // Type of the file to create (optional)
|
||||
}
|
||||
|
||||
// Result for rule file operations with meaningful data only
|
||||
|
||||
@@ -11,6 +11,7 @@ service McpService {
|
||||
rpc updateMcpTimeout(UpdateMcpTimeoutRequest) returns (McpServers);
|
||||
rpc addRemoteMcpServer(AddRemoteMcpServerRequest) returns (McpServers);
|
||||
rpc downloadMcp(StringRequest) returns (Empty);
|
||||
rpc restartMcpServer(StringRequest) returns (McpServers);
|
||||
}
|
||||
|
||||
message ToggleMcpServerRequest {
|
||||
|
||||
@@ -7,6 +7,7 @@ service StateService {
|
||||
rpc getLatestState(EmptyRequest) returns (State);
|
||||
rpc subscribeToState(EmptyRequest) returns (stream State);
|
||||
rpc toggleFavoriteModel(StringRequest) returns (Empty);
|
||||
rpc resetState(EmptyRequest) returns (Empty);
|
||||
}
|
||||
|
||||
message State {
|
||||
|
||||
@@ -25,6 +25,12 @@ service TaskService {
|
||||
rpc deleteNonFavoritedTasks(EmptyRequest) returns (DeleteNonFavoritedTasksResults);
|
||||
// Gets filtered task history
|
||||
rpc getTaskHistory(GetTaskHistoryRequest) returns (TaskHistoryArray);
|
||||
// Sends a response to a previous ask operation
|
||||
rpc askResponse(AskResponseRequest) returns (Empty);
|
||||
// Records task feedback (thumbs up/down)
|
||||
rpc taskFeedback(StringRequest) returns (Empty);
|
||||
// Shows task completion changes diff in a view
|
||||
rpc taskCompletionViewChanges(Int64Request) returns (Empty);
|
||||
}
|
||||
|
||||
// Request message for creating a new task
|
||||
@@ -89,3 +95,11 @@ message TaskItem {
|
||||
int32 cache_writes = 9;
|
||||
int32 cache_reads = 10;
|
||||
}
|
||||
|
||||
// Request for ask response operation
|
||||
message AskResponseRequest {
|
||||
Metadata metadata = 1;
|
||||
string response_type = 2;
|
||||
string text = 3;
|
||||
repeated string images = 4;
|
||||
}
|
||||
|
||||
@@ -8,9 +8,19 @@ import "common.proto";
|
||||
|
||||
service WebService {
|
||||
rpc checkIsImageUrl(StringRequest) returns (IsImageUrl);
|
||||
rpc fetchOpenGraphData(StringRequest) returns (OpenGraphData);
|
||||
}
|
||||
|
||||
message IsImageUrl {
|
||||
bool is_image = 1;
|
||||
string url = 2;
|
||||
}
|
||||
|
||||
message OpenGraphData {
|
||||
string title = 1;
|
||||
string description = 2;
|
||||
string image = 3;
|
||||
string url = 4;
|
||||
string site_name = 5;
|
||||
string type = 6;
|
||||
}
|
||||
|
||||
@@ -16,8 +16,11 @@ const cwd = process.cwd()
|
||||
process.chdir(BUILD_DIR)
|
||||
try {
|
||||
execSync("npm install", { stdio: "inherit" })
|
||||
// Move the vscode directory into node_modules.
|
||||
// It can't be installed using npm because it will create a symlink which is not portable.
|
||||
fs.renameSync("vscode", path.join("node_modules", "vscode"))
|
||||
} catch (error) {
|
||||
console.error("Error running npm install:", error)
|
||||
console.error("Error during setup:", error)
|
||||
process.exit(1)
|
||||
} finally {
|
||||
process.chdir(cwd)
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ try {
|
||||
execSync("which xvfb-run", { stdio: "ignore" })
|
||||
|
||||
console.log("xvfb-run is installed. Running tests with xvfb-run...")
|
||||
execSync("xvfb-run -a npm run test:integration", { stdio: "inherit" })
|
||||
execSync("xvfb-run -a npm run test:coverage", { stdio: "inherit" })
|
||||
} else {
|
||||
console.log("Non-Linux environment detected. Running tests normally.")
|
||||
execSync("npm run test:integration", { stdio: "inherit" })
|
||||
|
||||
@@ -73,7 +73,11 @@ export class GeminiHandler implements ApiHandler {
|
||||
* @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()
|
||||
@withRetry({
|
||||
maxRetries: 4,
|
||||
baseDelay: 2000,
|
||||
maxDelay: 15000,
|
||||
})
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const { id: modelId, info } = this.getModel()
|
||||
const contents = messages.map(convertAnthropicMessageToGemini)
|
||||
|
||||
@@ -54,6 +54,15 @@ export function withRetry(options: RetryOptions = {}) {
|
||||
delay = Math.min(maxDelay, baseDelay * Math.pow(2, attempt))
|
||||
}
|
||||
|
||||
const handlerInstance = this as any
|
||||
if (handlerInstance.options?.onRetryAttempt) {
|
||||
try {
|
||||
handlerInstance.options.onRetryAttempt(attempt + 1, maxRetries, delay, error)
|
||||
} catch (e) {
|
||||
console.error("Error in onRetryAttempt callback:", e)
|
||||
}
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, delay))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,45 +8,6 @@ import { getGlobalState, getWorkspaceState, updateGlobalState, updateWorkspaceSt
|
||||
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)) {
|
||||
@@ -80,7 +41,8 @@ export const getLocalClineRules = async (cwd: string, toggles: ClineRulesToggles
|
||||
if (await fileExistsAtPath(clineRulesFilePath)) {
|
||||
if (await isDirectory(clineRulesFilePath)) {
|
||||
try {
|
||||
const rulesFilePaths = await readDirectory(clineRulesFilePath)
|
||||
const rulesFilePaths = await readDirectory(clineRulesFilePath, [[".clinerules", "workflows"]])
|
||||
|
||||
const rulesFilesTotalContent = await getRuleFilesTotalContent(rulesFilePaths, cwd, toggles)
|
||||
if (rulesFilesTotalContent) {
|
||||
clineRulesFileInstructions = formatResponse.clineRulesLocalDirectoryInstructions(cwd, rulesFilesTotalContent)
|
||||
@@ -121,7 +83,9 @@ export async function refreshClineRulesToggles(
|
||||
// Local toggles
|
||||
const localClineRulesToggles = ((await getWorkspaceState(context, "localClineRulesToggles")) as ClineRulesToggles) || {}
|
||||
const localClineRulesFilePath = path.resolve(workingDirectory, GlobalFileNames.clineRules)
|
||||
const updatedLocalToggles = await synchronizeRuleToggles(localClineRulesFilePath, localClineRulesToggles)
|
||||
const updatedLocalToggles = await synchronizeRuleToggles(localClineRulesFilePath, localClineRulesToggles, "", [
|
||||
[".clinerules", "workflows"],
|
||||
])
|
||||
await updateWorkspaceState(context, "localClineRulesToggles", updatedLocalToggles)
|
||||
|
||||
return {
|
||||
@@ -129,82 +93,3 @@ export async function refreshClineRulesToggles(
|
||||
localToggles: updatedLocalToggles,
|
||||
}
|
||||
}
|
||||
|
||||
export const createRuleFile = async (isGlobal: boolean, filename: string, cwd: string) => {
|
||||
try {
|
||||
let filePath: string
|
||||
if (isGlobal) {
|
||||
const globalClineRulesFilePath = await ensureRulesDirectoryExists()
|
||||
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)
|
||||
}
|
||||
|
||||
const fileExists = await fileExistsAtPath(filePath)
|
||||
|
||||
if (fileExists) {
|
||||
return { filePath, fileExists }
|
||||
}
|
||||
|
||||
await fs.writeFile(filePath, "", "utf8")
|
||||
|
||||
return { filePath, fileExists: false }
|
||||
} catch (error) {
|
||||
return { filePath: null, fileExists: false }
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteRuleFile(
|
||||
context: vscode.ExtensionContext,
|
||||
rulePath: string,
|
||||
isGlobal: boolean,
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
// Check if file exists
|
||||
const fileExists = await fileExistsAtPath(rulePath)
|
||||
if (!fileExists) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Rule file does not exist: ${rulePath}`,
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the file from disk
|
||||
await fs.unlink(rulePath)
|
||||
|
||||
// Get the filename for messages
|
||||
const fileName = path.basename(rulePath)
|
||||
|
||||
// Update the appropriate toggles
|
||||
if (isGlobal) {
|
||||
const toggles = ((await getGlobalState(context, "globalClineRulesToggles")) as ClineRulesToggles) || {}
|
||||
delete toggles[rulePath]
|
||||
await updateGlobalState(context, "globalClineRulesToggles", toggles)
|
||||
} else {
|
||||
const toggles = ((await getWorkspaceState(context, "localClineRulesToggles")) as ClineRulesToggles) || {}
|
||||
delete toggles[rulePath]
|
||||
await updateWorkspaceState(context, "localClineRulesToggles", toggles)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Rule file "${fileName}" deleted successfully`,
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
console.error(`Error deleting rule file: ${errorMessage}`, error)
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed to delete rule file.`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import { fileExistsAtPath, isDirectory, readDirectory } from "@utils/fs"
|
||||
import { ensureRulesDirectoryExists, GlobalFileNames } from "@core/storage/disk"
|
||||
import { getGlobalState, getWorkspaceState, updateGlobalState, updateWorkspaceState } from "@core/storage/state"
|
||||
import * as path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { ClineRulesToggles } from "@shared/cline-rules"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
/**
|
||||
* Recursively traverses directory and finds all files, including checking for optional whitelisted file extension
|
||||
*/
|
||||
export async function readDirectoryRecursive(directoryPath: string, allowedFileExtension: string): Promise<string[]> {
|
||||
export async function readDirectoryRecursive(
|
||||
directoryPath: string,
|
||||
allowedFileExtension: string,
|
||||
excludedPaths: string[][] = [],
|
||||
): Promise<string[]> {
|
||||
try {
|
||||
const entries = await readDirectory(directoryPath)
|
||||
const entries = await readDirectory(directoryPath, excludedPaths)
|
||||
let results: string[] = []
|
||||
for (const entry of entries) {
|
||||
if (allowedFileExtension !== "") {
|
||||
@@ -33,6 +40,7 @@ export async function synchronizeRuleToggles(
|
||||
rulesDirectoryPath: string,
|
||||
currentToggles: ClineRulesToggles,
|
||||
allowedFileExtension: string = "",
|
||||
excludedPaths: string[][] = [],
|
||||
): Promise<ClineRulesToggles> {
|
||||
// Create a copy of toggles to modify
|
||||
const updatedToggles = { ...currentToggles }
|
||||
@@ -45,7 +53,7 @@ export async function synchronizeRuleToggles(
|
||||
|
||||
if (isDir) {
|
||||
// DIRECTORY CASE
|
||||
const filePaths = await readDirectoryRecursive(rulesDirectoryPath, allowedFileExtension)
|
||||
const filePaths = await readDirectoryRecursive(rulesDirectoryPath, allowedFileExtension, excludedPaths)
|
||||
const existingRulePaths = new Set<string>()
|
||||
|
||||
for (const filePath of filePaths) {
|
||||
@@ -119,3 +127,155 @@ export const getRuleFilesTotalContent = async (rulesFilePaths: string[], basePat
|
||||
).then((contents) => contents.filter(Boolean).join("\n\n"))
|
||||
return ruleFilesTotalContent
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles converting any directory into a file (specifically used for .clinerules and .clinerules/workflows)
|
||||
* The old .clinerules file or .clinerules/workflows file will be renamed to a default filename
|
||||
* Doesn't do anything if the dir already exists or doesn't exist
|
||||
* Returns whether there are any uncaught errors
|
||||
*/
|
||||
export async function ensureLocalClineDirExists(clinerulePath: string, defaultRuleFilename: string): Promise<boolean> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a rule file or workflow file
|
||||
*/
|
||||
export const createRuleFile = async (isGlobal: boolean, filename: string, cwd: string, type: string) => {
|
||||
try {
|
||||
let filePath: string
|
||||
if (isGlobal) {
|
||||
// global means its implicitly clinerules
|
||||
const globalClineRulesFilePath = await ensureRulesDirectoryExists()
|
||||
filePath = path.join(globalClineRulesFilePath, filename)
|
||||
} else {
|
||||
const localClineRulesFilePath = path.resolve(cwd, GlobalFileNames.clineRules)
|
||||
|
||||
const hasError = await ensureLocalClineDirExists(localClineRulesFilePath, "default-rules.md")
|
||||
if (hasError === true) {
|
||||
return { filePath: null, fileExists: false }
|
||||
}
|
||||
|
||||
await fs.mkdir(localClineRulesFilePath, { recursive: true })
|
||||
|
||||
if (type === "workflow") {
|
||||
const localWorkflowsFilePath = path.resolve(cwd, GlobalFileNames.workflows)
|
||||
|
||||
const hasError = await ensureLocalClineDirExists(localWorkflowsFilePath, "default-workflows.md")
|
||||
if (hasError === true) {
|
||||
return { filePath: null, fileExists: false }
|
||||
}
|
||||
|
||||
await fs.mkdir(localWorkflowsFilePath, { recursive: true })
|
||||
|
||||
filePath = path.join(localWorkflowsFilePath, filename)
|
||||
} else {
|
||||
// clinerules file creation
|
||||
filePath = path.join(localClineRulesFilePath, filename)
|
||||
}
|
||||
}
|
||||
|
||||
const fileExists = await fileExistsAtPath(filePath)
|
||||
|
||||
if (fileExists) {
|
||||
return { filePath, fileExists }
|
||||
}
|
||||
|
||||
await fs.writeFile(filePath, "", "utf8")
|
||||
|
||||
return { filePath, fileExists: false }
|
||||
} catch (error) {
|
||||
return { filePath: null, fileExists: false }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a rule file or workflow file
|
||||
*/
|
||||
export async function deleteRuleFile(
|
||||
context: vscode.ExtensionContext,
|
||||
rulePath: string,
|
||||
isGlobal: boolean,
|
||||
type: string,
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
// Check if file exists
|
||||
const fileExists = await fileExistsAtPath(rulePath)
|
||||
if (!fileExists) {
|
||||
return {
|
||||
success: false,
|
||||
message: `File does not exist: ${rulePath}`,
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the file from disk
|
||||
await fs.unlink(rulePath)
|
||||
|
||||
// Get the filename for messages
|
||||
const fileName = path.basename(rulePath)
|
||||
|
||||
// Update the appropriate toggles
|
||||
if (isGlobal) {
|
||||
const toggles = ((await getGlobalState(context, "globalClineRulesToggles")) as ClineRulesToggles) || {}
|
||||
delete toggles[rulePath]
|
||||
await updateGlobalState(context, "globalClineRulesToggles", toggles)
|
||||
} else {
|
||||
if (type === "workflow") {
|
||||
const toggles = ((await getWorkspaceState(context, "workflowToggles")) as ClineRulesToggles) || {}
|
||||
delete toggles[rulePath]
|
||||
await updateWorkspaceState(context, "workflowToggles", toggles)
|
||||
} else if (type === "cursor") {
|
||||
const toggles = ((await getWorkspaceState(context, "localCursorRulesToggles")) as ClineRulesToggles) || {}
|
||||
delete toggles[rulePath]
|
||||
await updateWorkspaceState(context, "localCursorRulesToggles", toggles)
|
||||
} else if (type === "windsurf") {
|
||||
const toggles = ((await getWorkspaceState(context, "localWindsurfRulesToggles")) as ClineRulesToggles) || {}
|
||||
delete toggles[rulePath]
|
||||
await updateWorkspaceState(context, "localWindsurfRulesToggles", toggles)
|
||||
} else {
|
||||
const toggles = ((await getWorkspaceState(context, "localClineRulesToggles")) as ClineRulesToggles) || {}
|
||||
delete toggles[rulePath]
|
||||
await updateWorkspaceState(context, "localClineRulesToggles", toggles)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `File "${fileName}" deleted successfully`,
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
console.error(`Error deleting file: ${errorMessage}`, error)
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed to delete file.`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import path from "path"
|
||||
import { GlobalFileNames } from "@core/storage/disk"
|
||||
import { ClineRulesToggles } from "@shared/cline-rules"
|
||||
import { getWorkspaceState, updateWorkspaceState } from "@core/storage/state"
|
||||
import * as vscode from "vscode"
|
||||
import { synchronizeRuleToggles } from "@core/context/instructions/user-instructions/rule-helpers"
|
||||
|
||||
/**
|
||||
* Refresh the workflow toggles
|
||||
*/
|
||||
export async function refreshWorkflowToggles(
|
||||
context: vscode.ExtensionContext,
|
||||
workingDirectory: string,
|
||||
): Promise<ClineRulesToggles> {
|
||||
const workflowRulesToggles = ((await getWorkspaceState(context, "workflowToggles")) as ClineRulesToggles) || {}
|
||||
const workflowsDirPath = path.resolve(workingDirectory, GlobalFileNames.workflows)
|
||||
const updatedWorkflowToggles = await synchronizeRuleToggles(workflowsDirPath, workflowRulesToggles)
|
||||
await updateWorkspaceState(context, "workflowToggles", updatedWorkflowToggles)
|
||||
return updatedWorkflowToggles
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { Empty } from "../../../shared/proto/common"
|
||||
import type { EmptyRequest } from "../../../shared/proto/common"
|
||||
import type { Controller } from "../index"
|
||||
|
||||
/**
|
||||
* Handles the account logout action
|
||||
* @param controller The controller instance
|
||||
* @param _request The empty request object
|
||||
* @returns Empty response
|
||||
*/
|
||||
export async function accountLogoutClicked(controller: Controller, _request: EmptyRequest): Promise<Empty> {
|
||||
await controller.handleSignOut()
|
||||
return {}
|
||||
}
|
||||
@@ -4,9 +4,11 @@
|
||||
// Import all method implementations
|
||||
import { registerMethod } from "./index"
|
||||
import { accountLoginClicked } from "./accountLoginClicked"
|
||||
import { accountLogoutClicked } from "./accountLogoutClicked"
|
||||
|
||||
// Register all account service methods
|
||||
export function registerAllMethods(): void {
|
||||
// Register each method with the registry
|
||||
registerMethod("accountLoginClicked", accountLoginClicked)
|
||||
registerMethod("accountLogoutClicked", accountLogoutClicked)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import * as vscode from "vscode"
|
||||
import { Controller } from ".."
|
||||
import { Empty, StringRequest } from "../../../shared/proto/common"
|
||||
|
||||
/**
|
||||
* Copies text to the system clipboard
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing the text to copy
|
||||
* @returns Empty response
|
||||
*/
|
||||
export async function copyToClipboard(controller: Controller, request: StringRequest): Promise<Empty> {
|
||||
try {
|
||||
if (request.value) {
|
||||
await vscode.env.clipboard.writeText(request.value)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error copying to clipboard:", error)
|
||||
}
|
||||
return Empty.create()
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
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 { refreshClineRulesToggles } from "@core/context/instructions/user-instructions/cline-rules"
|
||||
import { createRuleFile as createRuleFileImpl } from "@core/context/instructions/user-instructions/rule-helpers"
|
||||
import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
import { handleFileServiceRequest } from "./index"
|
||||
import { cwd } from "@core/task"
|
||||
import { refreshWorkflowToggles } from "@/core/context/instructions/user-instructions/workflows"
|
||||
|
||||
/**
|
||||
* Creates a rule file in either global or workspace rules directory
|
||||
@@ -18,32 +17,45 @@ import { cwd } from "@core/task"
|
||||
* @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) {
|
||||
if (
|
||||
typeof request.isGlobal !== "boolean" ||
|
||||
!request.filename ||
|
||||
typeof request.filename !== "string" ||
|
||||
!request.type ||
|
||||
typeof request.type !== "string"
|
||||
) {
|
||||
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}`,
|
||||
type: typeof request.type === "string" ? request.type : `Invalid: ${typeof request.type}`,
|
||||
})
|
||||
throw new Error("Missing or invalid parameters")
|
||||
}
|
||||
|
||||
const { filePath, fileExists } = await createRuleFileImpl(request.isGlobal, request.filename, cwd)
|
||||
const { filePath, fileExists } = await createRuleFileImpl(request.isGlobal, request.filename, cwd, request.type)
|
||||
|
||||
if (!filePath) {
|
||||
throw new Error("Failed to create rule file.")
|
||||
throw new Error("Failed to create file.")
|
||||
}
|
||||
|
||||
const fileTypeName = request.type === "workflow" ? "workflow" : "rule"
|
||||
|
||||
if (fileExists) {
|
||||
vscode.window.showWarningMessage(`Rule file "${request.filename}" already exists.`)
|
||||
vscode.window.showWarningMessage(`${fileTypeName} file "${request.filename}" already exists.`)
|
||||
// Still open it for editing
|
||||
await handleFileServiceRequest(controller, "openFile", { value: filePath })
|
||||
} else {
|
||||
await refreshClineRulesToggles(controller.context, cwd)
|
||||
if (request.type === "workflow") {
|
||||
await refreshWorkflowToggles(controller.context, cwd)
|
||||
} 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}`,
|
||||
`Created new ${request.isGlobal ? "global" : "workspace"} ${fileTypeName} file: ${request.filename}`,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
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 { refreshClineRulesToggles } from "@core/context/instructions/user-instructions/cline-rules"
|
||||
import { deleteRuleFile as deleteRuleFileImpl } from "@core/context/instructions/user-instructions/rule-helpers"
|
||||
import { refreshExternalRulesToggles } from "@core/context/instructions/user-instructions/external-rules"
|
||||
import { refreshWorkflowToggles } from "@core/context/instructions/user-instructions/workflows"
|
||||
import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
import { cwd } from "@core/task"
|
||||
@@ -18,26 +17,38 @@ import { cwd } from "@core/task"
|
||||
* @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) {
|
||||
if (
|
||||
typeof request.isGlobal !== "boolean" ||
|
||||
typeof request.rulePath !== "string" ||
|
||||
!request.rulePath ||
|
||||
!request.type ||
|
||||
typeof request.type !== "string"
|
||||
) {
|
||||
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}`,
|
||||
type: typeof request.type === "string" ? request.type : `Invalid: ${typeof request.type}`,
|
||||
})
|
||||
throw new Error("Missing or invalid parameters")
|
||||
}
|
||||
|
||||
const result = await deleteRuleFileImpl(controller.context, request.rulePath, request.isGlobal)
|
||||
const result = await deleteRuleFileImpl(controller.context, request.rulePath, request.isGlobal, request.type)
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.message || "Failed to delete rule file")
|
||||
}
|
||||
|
||||
await refreshClineRulesToggles(controller.context, cwd)
|
||||
await refreshExternalRulesToggles(controller.context, cwd)
|
||||
// we refresh inside of the deleteRuleFileImpl(..) call
|
||||
//await refreshClineRulesToggles(controller.context, cwd)
|
||||
//await refreshExternalRulesToggles(controller.context, cwd)
|
||||
//await refreshWorkflowToggles(controller.context, cwd)
|
||||
await controller.postStateToWebview()
|
||||
|
||||
const fileName = path.basename(request.rulePath)
|
||||
vscode.window.showInformationMessage(`Rule file "${fileName}" deleted successfully`)
|
||||
|
||||
const fileTypeName = request.type === "workflow" ? "workflow" : "rule"
|
||||
|
||||
vscode.window.showInformationMessage(`${fileTypeName} file "${fileName}" deleted successfully`)
|
||||
|
||||
return RuleFile.create({
|
||||
filePath: request.rulePath,
|
||||
|
||||
@@ -10,6 +10,7 @@ import { openFile } from "./openFile"
|
||||
import { openImage } from "./openImage"
|
||||
import { searchCommits } from "./searchCommits"
|
||||
import { searchFiles } from "./searchFiles"
|
||||
import { selectImages } from "./selectImages"
|
||||
|
||||
// Register all file service methods
|
||||
export function registerAllMethods(): void {
|
||||
@@ -21,4 +22,5 @@ export function registerAllMethods(): void {
|
||||
registerMethod("openImage", openImage)
|
||||
registerMethod("searchCommits", searchCommits)
|
||||
registerMethod("searchFiles", searchFiles)
|
||||
registerMethod("selectImages", selectImages)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Controller } from ".."
|
||||
import { EmptyRequest, StringArray } from "@shared/proto/common"
|
||||
import { selectImages as selectImagesIntegration } from "@integrations/misc/process-images"
|
||||
import { FileMethodHandler } from "./index"
|
||||
|
||||
/**
|
||||
* Prompts the user to select images from the file system and returns them as data URLs
|
||||
* @param controller The controller instance
|
||||
* @param request Empty request, no parameters needed
|
||||
* @returns Array of image data URLs
|
||||
*/
|
||||
export const selectImages: FileMethodHandler = async (controller: Controller, request: EmptyRequest): Promise<StringArray> => {
|
||||
try {
|
||||
const images = await selectImagesIntegration()
|
||||
return StringArray.create({ values: images })
|
||||
} catch (error) {
|
||||
console.error("Error selecting images:", error)
|
||||
// Return empty array on error
|
||||
return StringArray.create({ values: [] })
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@ import { cleanupLegacyCheckpoints } from "@integrations/checkpoints/CheckpointMi
|
||||
import { downloadTask } from "@integrations/misc/export-markdown"
|
||||
import { fetchOpenGraphData } from "@integrations/misc/link-preview"
|
||||
import { handleFileServiceRequest } from "./file"
|
||||
import { selectImages } from "@integrations/misc/process-images"
|
||||
import { getTheme } from "@integrations/theme/getTheme"
|
||||
import WorkspaceTracker from "@integrations/workspace/WorkspaceTracker"
|
||||
import { ClineAccountService } from "@services/account/ClineAccountService"
|
||||
@@ -51,6 +50,7 @@ import { ClineRulesToggles } from "@shared/cline-rules"
|
||||
import { sendStateUpdate } from "./state/subscribeToState"
|
||||
import { refreshClineRulesToggles } from "@core/context/instructions/user-instructions/cline-rules"
|
||||
import { refreshExternalRulesToggles } from "@core/context/instructions/user-instructions/external-rules"
|
||||
import { refreshWorkflowToggles } from "@core/context/instructions/user-instructions/workflows"
|
||||
|
||||
/*
|
||||
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
||||
@@ -66,7 +66,7 @@ export class Controller {
|
||||
workspaceTracker: WorkspaceTracker
|
||||
mcpHub: McpHub
|
||||
accountService: ClineAccountService
|
||||
private latestAnnouncementId = "may-09-2025_17:11:00" // update to some unique identifier when we add a new announcement
|
||||
private latestAnnouncementId = "may-16-2025_16:11:00" // update to some unique identifier when we add a new announcement
|
||||
|
||||
constructor(
|
||||
readonly context: vscode.ExtensionContext,
|
||||
@@ -146,8 +146,18 @@ export class Controller {
|
||||
chatSettings,
|
||||
shellIntegrationTimeout,
|
||||
enableCheckpointsSetting,
|
||||
isNewUser,
|
||||
taskHistory,
|
||||
} = await getAllExtensionState(this.context)
|
||||
|
||||
const NEW_USER_TASK_COUNT_THRESHOLD = 10
|
||||
|
||||
// Check if the user has completed enough tasks to no longer be considered a "new user"
|
||||
if (isNewUser && !historyItem && taskHistory && taskHistory.length >= NEW_USER_TASK_COUNT_THRESHOLD) {
|
||||
await updateGlobalState(this.context, "isNewUser", false)
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
|
||||
if (autoApprovalSettings) {
|
||||
const updatedAutoApprovalSettings = {
|
||||
...autoApprovalSettings,
|
||||
@@ -313,26 +323,14 @@ export class Controller {
|
||||
const browserSession = new BrowserSession(this.context, browserSettings)
|
||||
await browserSession.relaunchChromeDebugMode(this)
|
||||
break
|
||||
case "askResponse":
|
||||
this.task?.handleWebviewAskResponse(message.askResponse!, message.text, message.images)
|
||||
break
|
||||
case "didShowAnnouncement":
|
||||
await updateGlobalState(this.context, "lastShownAnnouncementId", this.latestAnnouncementId)
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
case "selectImages":
|
||||
const images = await selectImages()
|
||||
await this.postMessageToWebview({
|
||||
type: "selectedImages",
|
||||
images,
|
||||
})
|
||||
break
|
||||
case "resetState":
|
||||
await this.resetState()
|
||||
break
|
||||
case "refreshClineRules":
|
||||
await refreshClineRulesToggles(this.context, cwd)
|
||||
await refreshExternalRulesToggles(this.context, cwd)
|
||||
await refreshWorkflowToggles(this.context, cwd)
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
case "openInBrowser":
|
||||
@@ -340,22 +338,9 @@ export class Controller {
|
||||
vscode.env.openExternal(vscode.Uri.parse(message.url))
|
||||
}
|
||||
break
|
||||
case "fetchOpenGraphData":
|
||||
this.fetchOpenGraphData(message.text!)
|
||||
break
|
||||
case "openMention":
|
||||
openMention(message.text)
|
||||
break
|
||||
case "taskCompletionViewChanges": {
|
||||
if (message.number) {
|
||||
await this.task?.presentMultifileDiff(message.number, true)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "accountLogoutClicked": {
|
||||
await this.handleSignOut()
|
||||
break
|
||||
}
|
||||
case "showAccountViewClicked": {
|
||||
await this.postMessageToWebview({ type: "action", action: "accountButtonClicked" })
|
||||
break
|
||||
@@ -379,11 +364,6 @@ export class Controller {
|
||||
await this.silentlyRefreshMcpMarketplace()
|
||||
break
|
||||
}
|
||||
case "taskFeedback":
|
||||
if (message.feedbackType && this.task?.taskId) {
|
||||
telemetryService.captureTaskFeedback(this.task.taskId, message.feedbackType)
|
||||
}
|
||||
break
|
||||
// case "openMcpMarketplaceServerDetails": {
|
||||
// if (message.text) {
|
||||
// const response = await fetch(`https://api.cline.bot/v1/mcp/marketplace/item?mcpId=${message.mcpId}`)
|
||||
@@ -484,16 +464,18 @@ export class Controller {
|
||||
}
|
||||
break
|
||||
}
|
||||
case "requestTotalTasksSize": {
|
||||
this.refreshTotalTasksSize()
|
||||
case "toggleWorkflow": {
|
||||
const { workflowPath, enabled } = message
|
||||
if (workflowPath && typeof enabled === "boolean") {
|
||||
const toggles = ((await getWorkspaceState(this.context, "workflowToggles")) as ClineRulesToggles) || {}
|
||||
toggles[workflowPath] = enabled
|
||||
await updateWorkspaceState(this.context, "workflowToggles", toggles)
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
break
|
||||
}
|
||||
case "restartMcpServer": {
|
||||
try {
|
||||
await this.mcpHub?.restartConnection(message.text!)
|
||||
} catch (error) {
|
||||
console.error(`Failed to retry connection for ${message.text}:`, error)
|
||||
}
|
||||
case "requestTotalTasksSize": {
|
||||
this.refreshTotalTasksSize()
|
||||
break
|
||||
}
|
||||
case "deleteMcpServer": {
|
||||
@@ -621,14 +603,6 @@ export class Controller {
|
||||
break
|
||||
}
|
||||
|
||||
case "copyToClipboard": {
|
||||
try {
|
||||
await vscode.env.clipboard.writeText(message.text || "")
|
||||
} catch (error) {
|
||||
console.error("Error copying to clipboard:", error)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "updateTerminalConnectionTimeout": {
|
||||
if (message.shellIntegrationTimeout !== undefined) {
|
||||
const timeout = message.shellIntegrationTimeout
|
||||
@@ -1334,7 +1308,10 @@ export class Controller {
|
||||
|
||||
async postStateToWebview() {
|
||||
const state = await this.getStateToPostToWebview()
|
||||
await sendStateUpdate(state)
|
||||
// For testing: Bypass gRPC stream and send state directly
|
||||
console.log("[Controller Test Revert] Posting full state via direct 'state' message.")
|
||||
await this.postMessageToWebview({ type: "state", state: state })
|
||||
// await sendStateUpdate(state) // Original line for the GrPC stream
|
||||
}
|
||||
|
||||
async getStateToPostToWebview(): Promise<ExtensionState> {
|
||||
@@ -1353,6 +1330,7 @@ export class Controller {
|
||||
enableCheckpointsSetting,
|
||||
globalClineRulesToggles,
|
||||
shellIntegrationTimeout,
|
||||
isNewUser,
|
||||
} = await getAllExtensionState(this.context)
|
||||
|
||||
const localClineRulesToggles =
|
||||
@@ -1364,6 +1342,8 @@ export class Controller {
|
||||
const localCursorRulesToggles =
|
||||
((await getWorkspaceState(this.context, "localCursorRulesToggles")) as ClineRulesToggles) || {}
|
||||
|
||||
const workflowToggles = ((await getWorkspaceState(this.context, "workflowToggles")) as ClineRulesToggles) || {}
|
||||
|
||||
return {
|
||||
version: this.context.extension?.packageJSON?.version ?? "",
|
||||
apiConfiguration,
|
||||
@@ -1391,7 +1371,9 @@ export class Controller {
|
||||
localClineRulesToggles: localClineRulesToggles || {},
|
||||
localWindsurfRulesToggles: localWindsurfRulesToggles || {},
|
||||
localCursorRulesToggles: localCursorRulesToggles || {},
|
||||
workflowToggles: workflowToggles || {},
|
||||
shellIntegrationTimeout,
|
||||
isNewUser,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1467,30 +1449,6 @@ export class Controller {
|
||||
|
||||
// secrets
|
||||
|
||||
// Open Graph Data
|
||||
|
||||
async fetchOpenGraphData(url: string) {
|
||||
try {
|
||||
// Use the fetchOpenGraphData function from link-preview.ts
|
||||
const ogData = await fetchOpenGraphData(url)
|
||||
|
||||
// Send the data back to the webview
|
||||
await this.postMessageToWebview({
|
||||
type: "openGraphData",
|
||||
openGraphData: ogData,
|
||||
url: url,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(`Error fetching Open Graph data for ${url}:`, error)
|
||||
// Send an error response
|
||||
await this.postMessageToWebview({
|
||||
type: "openGraphData",
|
||||
error: `Failed to fetch Open Graph data: ${error}`,
|
||||
url: url,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Git commit message generation
|
||||
|
||||
async generateGitCommitMessage() {
|
||||
@@ -1595,19 +1553,4 @@ Commit message:`
|
||||
}
|
||||
|
||||
// dev
|
||||
|
||||
async resetState() {
|
||||
vscode.window.showInformationMessage("Resetting state...")
|
||||
await resetExtensionState(this.context)
|
||||
if (this.task) {
|
||||
this.task.abortTask()
|
||||
this.task = undefined
|
||||
}
|
||||
vscode.window.showInformationMessage("State reset")
|
||||
await this.postStateToWebview()
|
||||
await this.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "chatButtonClicked",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { registerMethod } from "./index"
|
||||
import { addRemoteMcpServer } from "./addRemoteMcpServer"
|
||||
import { downloadMcp } from "./downloadMcp"
|
||||
import { restartMcpServer } from "./restartMcpServer"
|
||||
import { toggleMcpServer } from "./toggleMcpServer"
|
||||
import { updateMcpTimeout } from "./updateMcpTimeout"
|
||||
|
||||
@@ -13,6 +14,7 @@ export function registerAllMethods(): void {
|
||||
// Register each method with the registry
|
||||
registerMethod("addRemoteMcpServer", addRemoteMcpServer)
|
||||
registerMethod("downloadMcp", downloadMcp)
|
||||
registerMethod("restartMcpServer", restartMcpServer)
|
||||
registerMethod("toggleMcpServer", toggleMcpServer)
|
||||
registerMethod("updateMcpTimeout", updateMcpTimeout)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { McpServers } from "@shared/proto/mcp"
|
||||
import type { Controller } from "../index"
|
||||
import { convertMcpServersToProtoMcpServers } from "@shared/proto-conversions/mcp/mcp-server-conversion"
|
||||
import { StringRequest } from "@/shared/proto/common"
|
||||
|
||||
/**
|
||||
* Restarts an MCP server connection
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing the server name
|
||||
* @returns The updated list of MCP servers
|
||||
*/
|
||||
export async function restartMcpServer(controller: Controller, request: StringRequest): Promise<McpServers> {
|
||||
try {
|
||||
const mcpServers = await controller.mcpHub?.restartConnectionRPC(request.value)
|
||||
|
||||
// Convert from McpServer[] to ProtoMcpServer[] ensuring all required fields are set
|
||||
const protoServers = convertMcpServersToProtoMcpServers(mcpServers)
|
||||
|
||||
return { mcpServers: protoServers }
|
||||
} catch (error) {
|
||||
console.error(`Failed to restart MCP server ${request.value}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
// Import all method implementations
|
||||
import { registerMethod } from "./index"
|
||||
import { getLatestState } from "./getLatestState"
|
||||
import { resetState } from "./resetState"
|
||||
import { subscribeToState } from "./subscribeToState"
|
||||
import { toggleFavoriteModel } from "./toggleFavoriteModel"
|
||||
|
||||
@@ -14,6 +15,7 @@ export const streamingMethods = ["subscribeToState"]
|
||||
export function registerAllMethods(): void {
|
||||
// Register each method with the registry
|
||||
registerMethod("getLatestState", getLatestState)
|
||||
registerMethod("resetState", resetState)
|
||||
registerMethod("subscribeToState", subscribeToState, { isStreaming: true })
|
||||
registerMethod("toggleFavoriteModel", toggleFavoriteModel)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Controller } from ".."
|
||||
import { Empty, EmptyRequest } from "../../../shared/proto/common"
|
||||
import { resetExtensionState } from "../../../core/storage/state"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
/**
|
||||
* Resets the extension state to its defaults
|
||||
* @param controller The controller instance
|
||||
* @param request An empty request (no parameters needed)
|
||||
* @returns An empty response
|
||||
*/
|
||||
export async function resetState(controller: Controller, request: EmptyRequest): Promise<Empty> {
|
||||
try {
|
||||
vscode.window.showInformationMessage("Resetting state...")
|
||||
await resetExtensionState(controller.context)
|
||||
|
||||
if (controller.task) {
|
||||
controller.task.abortTask()
|
||||
controller.task = undefined
|
||||
}
|
||||
|
||||
vscode.window.showInformationMessage("State reset")
|
||||
await controller.postStateToWebview()
|
||||
|
||||
await controller.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "chatButtonClicked",
|
||||
})
|
||||
|
||||
return Empty.create()
|
||||
} catch (error) {
|
||||
console.error("Error resetting state:", error)
|
||||
vscode.window.showErrorMessage(`Failed to reset state: ${error instanceof Error ? error.message : String(error)}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Controller } from ".."
|
||||
import { Empty } from "../../../shared/proto/common"
|
||||
import { AskResponseRequest } from "../../../shared/proto/task"
|
||||
import { ClineAskResponse } from "../../../shared/WebviewMessage"
|
||||
|
||||
/**
|
||||
* Handles a response from the webview for a previous ask operation
|
||||
*
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing response type, optional text and optional images
|
||||
* @returns Empty response
|
||||
*/
|
||||
export async function askResponse(controller: Controller, request: AskResponseRequest): Promise<Empty> {
|
||||
try {
|
||||
if (!controller.task) {
|
||||
console.warn("askResponse: No active task to receive response")
|
||||
return Empty.create()
|
||||
}
|
||||
|
||||
// Map the string responseType to the ClineAskResponse enum
|
||||
let responseType: ClineAskResponse
|
||||
switch (request.responseType) {
|
||||
case "yesButtonClicked":
|
||||
responseType = "yesButtonClicked"
|
||||
break
|
||||
case "noButtonClicked":
|
||||
responseType = "noButtonClicked"
|
||||
break
|
||||
case "messageResponse":
|
||||
responseType = "messageResponse"
|
||||
break
|
||||
default:
|
||||
console.warn(`askResponse: Unknown response type: ${request.responseType}`)
|
||||
return Empty.create()
|
||||
}
|
||||
|
||||
// Call the task's handler for webview responses
|
||||
await controller.task.handleWebviewAskResponse(responseType, request.text, request.images)
|
||||
|
||||
return Empty.create()
|
||||
} catch (error) {
|
||||
console.error("Error in askResponse handler:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
// Import all method implementations
|
||||
import { registerMethod } from "./index"
|
||||
import { askResponse } from "./askResponse"
|
||||
import { cancelTask } from "./cancelTask"
|
||||
import { clearTask } from "./clearTask"
|
||||
import { deleteNonFavoritedTasks } from "./deleteNonFavoritedTasks"
|
||||
@@ -11,11 +12,14 @@ import { exportTaskWithId } from "./exportTaskWithId"
|
||||
import { getTaskHistory } from "./getTaskHistory"
|
||||
import { newTask } from "./newTask"
|
||||
import { showTaskWithId } from "./showTaskWithId"
|
||||
import { taskCompletionViewChanges } from "./taskCompletionViewChanges"
|
||||
import { taskFeedback } from "./taskFeedback"
|
||||
import { toggleTaskFavorite } from "./toggleTaskFavorite"
|
||||
|
||||
// Register all task service methods
|
||||
export function registerAllMethods(): void {
|
||||
// Register each method with the registry
|
||||
registerMethod("askResponse", askResponse)
|
||||
registerMethod("cancelTask", cancelTask)
|
||||
registerMethod("clearTask", clearTask)
|
||||
registerMethod("deleteNonFavoritedTasks", deleteNonFavoritedTasks)
|
||||
@@ -24,5 +28,7 @@ export function registerAllMethods(): void {
|
||||
registerMethod("getTaskHistory", getTaskHistory)
|
||||
registerMethod("newTask", newTask)
|
||||
registerMethod("showTaskWithId", showTaskWithId)
|
||||
registerMethod("taskCompletionViewChanges", taskCompletionViewChanges)
|
||||
registerMethod("taskFeedback", taskFeedback)
|
||||
registerMethod("toggleTaskFavorite", toggleTaskFavorite)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Controller } from ".."
|
||||
import { Empty } from "../../../shared/proto/common"
|
||||
import { Int64Request } from "../../../shared/proto/common"
|
||||
|
||||
/**
|
||||
* Shows task completion changes in a diff view
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing the timestamp of the message
|
||||
* @returns Empty response
|
||||
*/
|
||||
export async function taskCompletionViewChanges(controller: Controller, request: Int64Request): Promise<Empty> {
|
||||
try {
|
||||
if (request.value && controller.task) {
|
||||
await controller.task.presentMultifileDiff(request.value, true)
|
||||
}
|
||||
return Empty.create()
|
||||
} catch (error) {
|
||||
console.error("Error in taskCompletionViewChanges handler:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Controller } from ".."
|
||||
import { Empty, StringRequest } from "../../../shared/proto/common"
|
||||
import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
|
||||
|
||||
/**
|
||||
* Handles task feedback submission (thumbs up/down)
|
||||
* @param controller The controller instance
|
||||
* @param request The StringRequest containing the feedback type ("thumbs_up" or "thumbs_down") in the value field
|
||||
* @returns Empty response
|
||||
*/
|
||||
export async function taskFeedback(controller: Controller, request: StringRequest): Promise<Empty> {
|
||||
if (!request.value) {
|
||||
console.warn("taskFeedback: Missing feedback type value")
|
||||
return Empty.create()
|
||||
}
|
||||
|
||||
try {
|
||||
if (controller.task?.taskId) {
|
||||
telemetryService.captureTaskFeedback(controller.task.taskId, request.value as any)
|
||||
} else {
|
||||
console.warn("taskFeedback: No active task to receive feedback")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in taskFeedback handler:", error)
|
||||
}
|
||||
|
||||
return Empty.create()
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Controller } from ".."
|
||||
import { StringRequest } from "../../../shared/proto/common"
|
||||
import { OpenGraphData } from "../../../shared/proto/web"
|
||||
import { fetchOpenGraphData as fetchOGData } from "../../../integrations/misc/link-preview"
|
||||
import { convertDomainOpenGraphDataToProto } from "../../../shared/proto-conversions/web/open-graph-conversion"
|
||||
|
||||
/**
|
||||
* Fetches Open Graph metadata from a URL
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing the URL to fetch metadata from
|
||||
* @returns Promise resolving to OpenGraphData
|
||||
*/
|
||||
export async function fetchOpenGraphData(controller: Controller, request: StringRequest): Promise<OpenGraphData> {
|
||||
try {
|
||||
const url = request.value || ""
|
||||
// Fetch open graph data using the existing utility
|
||||
const ogData = await fetchOGData(url)
|
||||
|
||||
// Convert domain model to proto model
|
||||
return convertDomainOpenGraphDataToProto(ogData)
|
||||
} catch (error) {
|
||||
console.error(`Error fetching Open Graph data: ${request.value}`, error)
|
||||
// Return empty OpenGraphData object
|
||||
return OpenGraphData.create({})
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,11 @@
|
||||
// Import all method implementations
|
||||
import { registerMethod } from "./index"
|
||||
import { checkIsImageUrl } from "./checkIsImageUrl"
|
||||
import { fetchOpenGraphData } from "./fetchOpenGraphData"
|
||||
|
||||
// Register all web service methods
|
||||
export function registerAllMethods(): void {
|
||||
// Register each method with the registry
|
||||
registerMethod("checkIsImageUrl", checkIsImageUrl)
|
||||
registerMethod("fetchOpenGraphData", fetchOpenGraphData)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { newTaskToolResponse, condenseToolResponse, newRuleToolResponse, reportBugToolResponse } from "../prompts/commands"
|
||||
import { ClineRulesToggles } from "@shared/cline-rules"
|
||||
import fs from "fs/promises"
|
||||
|
||||
/**
|
||||
* Processes text for slash commands and transforms them with appropriate instructions
|
||||
* This is called after parseMentions() to process any slash commands in the user's message
|
||||
*/
|
||||
export function parseSlashCommands(text: string): { processedText: string; needsClinerulesFileCheck: boolean } {
|
||||
const SUPPORTED_COMMANDS = ["newtask", "smol", "compact", "newrule", "reportbug"]
|
||||
export async function parseSlashCommands(
|
||||
text: string,
|
||||
workflowToggles: ClineRulesToggles,
|
||||
): Promise<{ processedText: string; needsClinerulesFileCheck: boolean }> {
|
||||
const SUPPORTED_DEFAULT_COMMANDS = ["newtask", "smol", "compact", "newrule", "reportbug"]
|
||||
|
||||
const commandReplacements: Record<string, string> = {
|
||||
newtask: newTaskToolResponse(),
|
||||
@@ -17,10 +22,10 @@ export function parseSlashCommands(text: string): { processedText: string; needs
|
||||
|
||||
// this currently allows matching prepended whitespace prior to /slash-command
|
||||
const tagPatterns = [
|
||||
{ tag: "task", regex: /<task>(\s*\/([a-zA-Z0-9_-]+))(\s+.+?)?\s*<\/task>/is },
|
||||
{ tag: "feedback", regex: /<feedback>(\s*\/([a-zA-Z0-9_-]+))(\s+.+?)?\s*<\/feedback>/is },
|
||||
{ tag: "answer", regex: /<answer>(\s*\/([a-zA-Z0-9_-]+))(\s+.+?)?\s*<\/answer>/is },
|
||||
{ tag: "user_message", regex: /<user_message>(\s*\/([a-zA-Z0-9_-]+))(\s+.+?)?\s*<\/user_message>/is },
|
||||
{ tag: "task", regex: /<task>(\s*\/([a-zA-Z0-9_\.-]+))(\s+.+?)?\s*<\/task>/is },
|
||||
{ tag: "feedback", regex: /<feedback>(\s*\/([a-zA-Z0-9_\.-]+))(\s+.+?)?\s*<\/feedback>/is },
|
||||
{ tag: "answer", regex: /<answer>(\s*\/([a-zA-Z0-9_\.-]+))(\s+.+?)?\s*<\/answer>/is },
|
||||
{ tag: "user_message", regex: /<user_message>(\s*\/([a-zA-Z0-9_\.-]+))(\s+.+?)?\s*<\/user_message>/is },
|
||||
]
|
||||
|
||||
// if we find a valid match, we will return inside that block
|
||||
@@ -34,7 +39,8 @@ export function parseSlashCommands(text: string): { processedText: string; needs
|
||||
|
||||
const commandName = match[2] // casing matters
|
||||
|
||||
if (SUPPORTED_COMMANDS.includes(commandName)) {
|
||||
// we give preference to the default commands if the user has a file with the same name
|
||||
if (SUPPORTED_DEFAULT_COMMANDS.includes(commandName)) {
|
||||
const fullMatchStartIndex = match.index
|
||||
|
||||
// find position of slash command within the full match
|
||||
@@ -51,6 +57,48 @@ export function parseSlashCommands(text: string): { processedText: string; needs
|
||||
|
||||
return { processedText: processedText, needsClinerulesFileCheck: commandName === "newrule" ? true : false }
|
||||
}
|
||||
|
||||
// in practice we want to minimize this work, so we only do it if theres a possible match
|
||||
const enabledWorkflows = Object.entries(workflowToggles)
|
||||
.filter(([_, enabled]) => enabled)
|
||||
.map(([filePath, _]) => {
|
||||
const fileName = filePath.replace(/^.*[/\\]/, "")
|
||||
|
||||
return {
|
||||
fullPath: filePath,
|
||||
fileName: fileName,
|
||||
}
|
||||
})
|
||||
|
||||
// Then check if the command matches any enabled workflow filename
|
||||
const matchingWorkflow = enabledWorkflows.find((workflow) => workflow.fileName === commandName)
|
||||
|
||||
if (matchingWorkflow) {
|
||||
try {
|
||||
// Read workflow file content from the full path
|
||||
const workflowContent = (await fs.readFile(matchingWorkflow.fullPath, "utf8")).trim()
|
||||
|
||||
// find position of slash command within the full match
|
||||
const fullMatchStartIndex = match.index
|
||||
const fullMatch = match[0]
|
||||
const relativeStartIndex = fullMatch.indexOf(match[1])
|
||||
|
||||
// calculate absolute indices in the original string
|
||||
const slashCommandStartIndex = fullMatchStartIndex + relativeStartIndex
|
||||
const slashCommandEndIndex = slashCommandStartIndex + match[1].length
|
||||
|
||||
// remove the slash command and add custom instructions at the top of this message
|
||||
const textWithoutSlashCommand =
|
||||
text.substring(0, slashCommandStartIndex) + text.substring(slashCommandEndIndex)
|
||||
const processedText =
|
||||
`<explicit_instructions type="${matchingWorkflow.fileName}">\n${workflowContent}\n</explicit_instructions>\n` +
|
||||
textWithoutSlashCommand
|
||||
|
||||
return { processedText, needsClinerulesFileCheck: false }
|
||||
} catch (error) {
|
||||
console.error(`Error reading workflow file ${matchingWorkflow.fullPath}: ${error}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ export const GlobalFileNames = {
|
||||
openRouterModels: "openrouter_models.json",
|
||||
mcpSettings: "cline_mcp_settings.json",
|
||||
clineRules: ".clinerules",
|
||||
workflows: ".clinerules/workflows",
|
||||
cursorRulesDir: ".cursor/rules",
|
||||
cursorRulesFile: ".cursorrules",
|
||||
windsurfRules: ".windsurfrules",
|
||||
|
||||
@@ -88,5 +88,6 @@ export type GlobalStateKey =
|
||||
| "favoritedModelIds"
|
||||
| "requestTimeoutMs"
|
||||
| "shellIntegrationTimeout"
|
||||
| "isNewUser"
|
||||
|
||||
export type LocalStateKey = "localClineRulesToggles"
|
||||
|
||||
@@ -76,6 +76,7 @@ async function migrateEnableCheckpointsSetting(enableCheckpointsSettingRaw: bool
|
||||
|
||||
export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
const [
|
||||
isNewUser,
|
||||
storedApiProvider,
|
||||
apiModelId,
|
||||
apiKey,
|
||||
@@ -162,6 +163,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
enableCheckpointsSettingRaw,
|
||||
mcpMarketplaceEnabledRaw,
|
||||
] = await Promise.all([
|
||||
getGlobalState(context, "isNewUser") as Promise<boolean | undefined>,
|
||||
getGlobalState(context, "apiProvider") as Promise<ApiProvider | undefined>,
|
||||
getGlobalState(context, "apiModelId") as Promise<string | undefined>,
|
||||
getSecret(context, "apiKey") as Promise<string | undefined>,
|
||||
@@ -354,6 +356,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
favoritedModelIds,
|
||||
requestTimeoutMs,
|
||||
},
|
||||
isNewUser: isNewUser ?? true,
|
||||
lastShownAnnouncementId,
|
||||
customInstructions,
|
||||
taskHistory,
|
||||
|
||||
+62
-5
@@ -80,6 +80,7 @@ import {
|
||||
ensureTaskDirectoryExists,
|
||||
getSavedApiConversationHistory,
|
||||
getSavedClineMessages,
|
||||
GlobalFileNames,
|
||||
saveApiConversationHistory,
|
||||
saveClineMessages,
|
||||
} from "@core/storage/disk"
|
||||
@@ -87,13 +88,14 @@ import {
|
||||
getGlobalClineRules,
|
||||
getLocalClineRules,
|
||||
refreshClineRulesToggles,
|
||||
ensureLocalClinerulesDirExists,
|
||||
} from "@core/context/instructions/user-instructions/cline-rules"
|
||||
import { ensureLocalClineDirExists } from "../context/instructions/user-instructions/rule-helpers"
|
||||
import {
|
||||
refreshExternalRulesToggles,
|
||||
getLocalWindsurfRules,
|
||||
getLocalCursorRules,
|
||||
} from "@core/context/instructions/user-instructions/external-rules"
|
||||
import { refreshWorkflowToggles } from "../context/instructions/user-instructions/workflows"
|
||||
import { getGlobalState } from "@core/storage/state"
|
||||
import { parseSlashCommands } from "@core/slash-commands"
|
||||
import WorkspaceTracker from "@integrations/workspace/WorkspaceTracker"
|
||||
@@ -230,6 +232,37 @@ export class Task {
|
||||
let effectiveApiConfiguration: ApiConfiguration = {
|
||||
...apiConfiguration,
|
||||
taskId: this.taskId,
|
||||
onRetryAttempt: (attempt: number, maxRetries: number, delay: number, error: any) => {
|
||||
const lastApiReqStartedIndex = findLastIndex(this.clineMessages, (m) => m.say === "api_req_started")
|
||||
if (lastApiReqStartedIndex !== -1) {
|
||||
try {
|
||||
const currentApiReqInfo: ClineApiReqInfo = JSON.parse(
|
||||
this.clineMessages[lastApiReqStartedIndex].text || "{}",
|
||||
)
|
||||
currentApiReqInfo.retryStatus = {
|
||||
attempt: attempt, // attempt is already 1-indexed from retry.ts
|
||||
maxAttempts: maxRetries, // total attempts
|
||||
delaySec: Math.round(delay / 1000),
|
||||
errorSnippet: error?.message ? `${String(error.message).substring(0, 50)}...` : undefined,
|
||||
}
|
||||
// Clear previous cancelReason and streamingFailedMessage if we are retrying
|
||||
delete currentApiReqInfo.cancelReason
|
||||
delete currentApiReqInfo.streamingFailedMessage
|
||||
this.clineMessages[lastApiReqStartedIndex].text = JSON.stringify(currentApiReqInfo)
|
||||
|
||||
// Post the updated state to the webview so the UI reflects the retry attempt
|
||||
this.postStateToWebview().catch((e) =>
|
||||
console.error("Error posting state to webview in onRetryAttempt:", e),
|
||||
)
|
||||
|
||||
console.log(
|
||||
`[Task ${this.taskId}] API Auto-Retry Status Update: Attempt ${attempt}/${maxRetries}, Delay: ${delay}ms`,
|
||||
)
|
||||
} catch (e) {
|
||||
console.error(`[Task ${this.taskId}] Error updating api_req_started with retryStatus:`, e)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
if (apiConfiguration.apiProvider === "openai" || apiConfiguration.apiProvider === "openai-native") {
|
||||
@@ -598,7 +631,9 @@ export class Task {
|
||||
}
|
||||
|
||||
async doesLatestTaskCompletionHaveNewChanges() {
|
||||
if (!this.enableCheckpoints) return false
|
||||
if (!this.enableCheckpoints) {
|
||||
return false
|
||||
}
|
||||
|
||||
const messageIndex = findLastIndex(this.clineMessages, (m) => m.say === "completion_result")
|
||||
const message = this.clineMessages[messageIndex]
|
||||
@@ -1652,6 +1687,20 @@ export class Task {
|
||||
|
||||
const errorMessage = this.formatErrorWithStatusCode(error)
|
||||
|
||||
// Update the 'api_req_started' message to reflect final failure before asking user to manually retry
|
||||
const lastApiReqStartedIndex = findLastIndex(this.clineMessages, (m) => m.say === "api_req_started")
|
||||
if (lastApiReqStartedIndex !== -1) {
|
||||
const currentApiReqInfo: ClineApiReqInfo = JSON.parse(this.clineMessages[lastApiReqStartedIndex].text || "{}")
|
||||
delete currentApiReqInfo.retryStatus
|
||||
|
||||
this.clineMessages[lastApiReqStartedIndex].text = JSON.stringify({
|
||||
...currentApiReqInfo, // Spread the modified info (with retryStatus removed)
|
||||
cancelReason: "retries_exhausted", // Indicate that automatic retries failed
|
||||
streamingFailedMessage: errorMessage,
|
||||
} satisfies ClineApiReqInfo)
|
||||
// this.ask will trigger postStateToWebview, so this change should be picked up.
|
||||
}
|
||||
|
||||
const { response } = await this.ask("api_req_failed", errorMessage)
|
||||
|
||||
if (response !== "yesButtonClicked") {
|
||||
@@ -3795,8 +3844,11 @@ export class Task {
|
||||
// fortunately api_req_finished was always parsed out for the gui anyways, so it remains solely for legacy purposes to keep track of prices in tasks from history
|
||||
// (it's worth removing a few months from now)
|
||||
const updateApiReqMsg = (cancelReason?: ClineApiReqCancelReason, streamingFailedMessage?: string) => {
|
||||
const currentApiReqInfo: ClineApiReqInfo = JSON.parse(this.clineMessages[lastApiReqIndex].text || "{}")
|
||||
delete currentApiReqInfo.retryStatus // Clear retry status when request is finalized
|
||||
|
||||
this.clineMessages[lastApiReqIndex].text = JSON.stringify({
|
||||
...JSON.parse(this.clineMessages[lastApiReqIndex].text || "{}"),
|
||||
...currentApiReqInfo, // Spread the modified info (with retryStatus removed)
|
||||
tokensIn: inputTokens,
|
||||
tokensOut: outputTokens,
|
||||
cacheWrites: cacheWriteTokens,
|
||||
@@ -4065,6 +4117,8 @@ export class Task {
|
||||
// Track if we need to check clinerulesFile
|
||||
let needsClinerulesFileCheck = false
|
||||
|
||||
const workflowToggles = await refreshWorkflowToggles(this.getContext(), cwd)
|
||||
|
||||
const processUserContent = async () => {
|
||||
// This is a temporary solution to dynamically load context mentions from tool results. It checks for the presence of tags that indicate that the tool was rejected and feedback was provided (see formatToolDeniedFeedback, attemptCompletion, executeCommand, and consecutiveMistakeCount >= 3) or "<answer>" (see askFollowupQuestion), we place all user generated content in these tags so they can effectively be used as markers for when we should parse mentions). However if we allow multiple tools responses in the future, we will need to parse mentions specifically within the user content tags.
|
||||
// (Note: this caused the @/ import alias bug where file contents were being parsed as well, since v2 converted tool results to text blocks)
|
||||
@@ -4087,7 +4141,10 @@ export class Task {
|
||||
)
|
||||
|
||||
// when parsing slash commands, we still want to allow the user to provide their desired context
|
||||
const { processedText, needsClinerulesFileCheck: needsCheck } = parseSlashCommands(parsedText)
|
||||
const { processedText, needsClinerulesFileCheck: needsCheck } = await parseSlashCommands(
|
||||
parsedText,
|
||||
workflowToggles,
|
||||
)
|
||||
|
||||
if (needsCheck) {
|
||||
needsClinerulesFileCheck = true
|
||||
@@ -4113,7 +4170,7 @@ export class Task {
|
||||
// After processing content, check clinerulesData if needed
|
||||
let clinerulesError = false
|
||||
if (needsClinerulesFileCheck) {
|
||||
clinerulesError = await ensureLocalClinerulesDirExists(cwd)
|
||||
clinerulesError = await ensureLocalClineDirExists(cwd, GlobalFileNames.clineRules)
|
||||
}
|
||||
|
||||
// Return all results
|
||||
|
||||
@@ -103,6 +103,7 @@ export class McpHub {
|
||||
|
||||
getServers(): McpServer[] {
|
||||
// Only return enabled servers
|
||||
|
||||
return this.connections.filter((conn) => !conn.server.disabled).map((conn) => conn.server)
|
||||
}
|
||||
|
||||
@@ -180,6 +181,120 @@ export class McpHub {
|
||||
}
|
||||
}
|
||||
|
||||
private async connectToServerRPC(
|
||||
name: string,
|
||||
config: z.infer<typeof StdioConfigSchema> | z.infer<typeof SseConfigSchema>,
|
||||
): Promise<void> {
|
||||
// Remove existing connection if it exists (should never happen, the connection should be deleted beforehand)
|
||||
this.connections = this.connections.filter((conn) => conn.server.name !== name)
|
||||
|
||||
try {
|
||||
// Each MCP server requires its own transport connection and has unique capabilities, configurations, and error handling. Having separate clients also allows proper scoping of resources/tools and independent server management like reconnection.
|
||||
const client = new Client(
|
||||
{
|
||||
name: "Cline",
|
||||
version: this.clientVersion,
|
||||
},
|
||||
{
|
||||
capabilities: {},
|
||||
},
|
||||
)
|
||||
|
||||
let transport: StdioClientTransport | SSEClientTransport
|
||||
|
||||
if (config.transportType === "sse") {
|
||||
transport = new SSEClientTransport(new URL(config.url), {})
|
||||
} else {
|
||||
transport = new StdioClientTransport({
|
||||
command: config.command,
|
||||
args: config.args,
|
||||
env: {
|
||||
...config.env,
|
||||
...(process.env.PATH ? { PATH: process.env.PATH } : {}),
|
||||
// ...(process.env.NODE_PATH ? { NODE_PATH: process.env.NODE_PATH } : {}),
|
||||
},
|
||||
stderr: "pipe", // necessary for stderr to be available
|
||||
})
|
||||
}
|
||||
|
||||
transport.onerror = async (error) => {
|
||||
console.error(`Transport error for "${name}":`, error)
|
||||
const connection = this.connections.find((conn) => conn.server.name === name)
|
||||
if (connection) {
|
||||
connection.server.status = "disconnected"
|
||||
this.appendErrorMessage(connection, error.message)
|
||||
}
|
||||
}
|
||||
|
||||
transport.onclose = async () => {
|
||||
const connection = this.connections.find((conn) => conn.server.name === name)
|
||||
if (connection) {
|
||||
connection.server.status = "disconnected"
|
||||
}
|
||||
}
|
||||
|
||||
const connection: McpConnection = {
|
||||
server: {
|
||||
name,
|
||||
config: JSON.stringify(config),
|
||||
status: "connecting",
|
||||
disabled: config.disabled,
|
||||
},
|
||||
client,
|
||||
transport,
|
||||
}
|
||||
this.connections.push(connection)
|
||||
|
||||
if (config.transportType === "stdio") {
|
||||
// transport.stderr is only available after the process has been started. However we can't start it separately from the .connect() call because it also starts the transport. And we can't place this after the connect call since we need to capture the stderr stream before the connection is established, in order to capture errors during the connection process.
|
||||
// As a workaround, we start the transport ourselves, and then monkey-patch the start method to no-op so that .connect() doesn't try to start it again.
|
||||
await transport.start()
|
||||
const stderrStream = (transport as StdioClientTransport).stderr
|
||||
if (stderrStream) {
|
||||
stderrStream.on("data", async (data: Buffer) => {
|
||||
const output = data.toString()
|
||||
// Check if output contains INFO level log
|
||||
const isInfoLog = !/\berror\b/i.test(output)
|
||||
|
||||
if (isInfoLog) {
|
||||
// Log normal informational messages
|
||||
console.info(`Server "${name}" info:`, output)
|
||||
} else {
|
||||
// Treat as error log
|
||||
console.error(`Server "${name}" stderr:`, output)
|
||||
const connection = this.connections.find((conn) => conn.server.name === name)
|
||||
if (connection) {
|
||||
this.appendErrorMessage(connection, output)
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
console.error(`No stderr stream for ${name}`)
|
||||
}
|
||||
transport.start = async () => {} // No-op now, .connect() won't fail
|
||||
}
|
||||
|
||||
// Connect
|
||||
await client.connect(transport)
|
||||
|
||||
connection.server.status = "connected"
|
||||
connection.server.error = ""
|
||||
|
||||
// Initial fetch of tools and resources
|
||||
connection.server.tools = await this.fetchToolsList(name)
|
||||
connection.server.resources = await this.fetchResourcesList(name)
|
||||
connection.server.resourceTemplates = await this.fetchResourceTemplatesList(name)
|
||||
} catch (error) {
|
||||
// Update status with error
|
||||
const connection = this.connections.find((conn) => conn.server.name === name)
|
||||
if (connection) {
|
||||
connection.server.status = "disconnected"
|
||||
this.appendErrorMessage(connection, error instanceof Error ? error.message : String(error))
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private async connectToServer(
|
||||
name: string,
|
||||
config: z.infer<typeof StdioConfigSchema> | z.infer<typeof SseConfigSchema>,
|
||||
@@ -494,6 +609,36 @@ export class McpHub {
|
||||
this.fileWatchers.clear()
|
||||
}
|
||||
|
||||
async restartConnectionRPC(serverName: string): Promise<McpServer[]> {
|
||||
this.isConnecting = true
|
||||
|
||||
// Get existing connection and update its status
|
||||
const connection = this.connections.find((conn) => conn.server.name === serverName)
|
||||
const inMemoryConfig = connection?.server.config
|
||||
if (inMemoryConfig) {
|
||||
connection.server.status = "connecting"
|
||||
connection.server.error = ""
|
||||
await setTimeoutPromise(500) // artificial delay to show user that server is restarting
|
||||
try {
|
||||
await this.deleteConnection(serverName)
|
||||
// Try to connect again using existing config
|
||||
await this.connectToServerRPC(serverName, JSON.parse(inMemoryConfig))
|
||||
} catch (error) {
|
||||
console.error(`Failed to restart connection for ${serverName}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
this.isConnecting = false
|
||||
|
||||
const config = await this.readAndValidateMcpSettingsFile()
|
||||
if (!config) {
|
||||
throw new Error("Failed to read or validate MCP settings")
|
||||
}
|
||||
|
||||
const serverOrder = Object.keys(config.mcpServers || {})
|
||||
return this.getSortedMcpServers(serverOrder)
|
||||
}
|
||||
|
||||
async restartConnection(serverName: string): Promise<void> {
|
||||
this.isConnecting = true
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { execa } from "execa"
|
||||
import { Logger } from "@services/logging/Logger"
|
||||
import { WebviewProvider } from "@core/webview"
|
||||
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
|
||||
import { TaskServiceClient } from "webview-ui/src/services/grpc-client"
|
||||
import {
|
||||
getWorkspacePath,
|
||||
validateWorkspacePath,
|
||||
@@ -15,7 +16,6 @@ import {
|
||||
import { updateGlobalState, getAllExtensionState, updateApiConfiguration, storeSecret } from "@core/storage/state"
|
||||
import { ClineAsk, ExtensionMessage } from "@shared/ExtensionMessage"
|
||||
import { ApiProvider } from "@shared/api"
|
||||
import { WebviewMessage } from "@shared/WebviewMessage"
|
||||
import { HistoryItem } from "@shared/HistoryItem"
|
||||
import { getSavedClineMessages, getSavedApiConversationHistory } from "@core/storage/disk"
|
||||
|
||||
@@ -515,8 +515,8 @@ export function createMessageCatcher(webviewProvider: WebviewProvider): vscode.D
|
||||
const askText = message.partialMessage.text
|
||||
|
||||
// Automatically respond to different types of asks
|
||||
setTimeout(() => {
|
||||
autoRespondToAsk(webviewProvider, askType, askText)
|
||||
setTimeout(async () => {
|
||||
await autoRespondToAsk(webviewProvider, askType, askText)
|
||||
}, 100) // Small delay to ensure the message is processed first
|
||||
}
|
||||
|
||||
@@ -538,65 +538,64 @@ export function createMessageCatcher(webviewProvider: WebviewProvider): vscode.D
|
||||
* @param askType The type of ask message
|
||||
* @param askText The text content of the ask message
|
||||
*/
|
||||
function autoRespondToAsk(webviewProvider: WebviewProvider, askType: ClineAsk, askText?: string): void {
|
||||
async function autoRespondToAsk(webviewProvider: WebviewProvider, askType: ClineAsk, askText?: string): Promise<void> {
|
||||
if (!webviewProvider.controller) {
|
||||
return
|
||||
}
|
||||
|
||||
Logger.log(`Auto-responding to ask type: ${askType}`)
|
||||
|
||||
// Create a response message based on the ask type
|
||||
const response: WebviewMessage = {
|
||||
type: "askResponse",
|
||||
askResponse: "yesButtonClicked", // Default to approving most actions
|
||||
}
|
||||
// Default to approving most actions
|
||||
let responseType = "yesButtonClicked"
|
||||
let responseText: string | undefined
|
||||
let responseImages: string[] | undefined
|
||||
|
||||
// Handle specific ask types differently if needed
|
||||
switch (askType) {
|
||||
case "followup":
|
||||
// For follow-up questions, provide a generic response
|
||||
response.askResponse = "messageResponse"
|
||||
response.text = "I can't answer any questions right now, use your best judgment."
|
||||
responseType = "messageResponse"
|
||||
responseText = "I can't answer any questions right now, use your best judgment."
|
||||
break
|
||||
|
||||
case "api_req_failed":
|
||||
// Always retry API requests
|
||||
response.askResponse = "yesButtonClicked" // "Retry" button
|
||||
responseType = "yesButtonClicked" // "Retry" button
|
||||
break
|
||||
|
||||
case "completion_result":
|
||||
// Accept the completion
|
||||
response.askResponse = "messageResponse"
|
||||
response.text = "Task completed successfully."
|
||||
responseType = "messageResponse"
|
||||
responseText = "Task completed successfully."
|
||||
break
|
||||
|
||||
case "mistake_limit_reached":
|
||||
// Provide guidance to continue
|
||||
response.askResponse = "messageResponse"
|
||||
response.text = "Try breaking down the task into smaller steps."
|
||||
responseType = "messageResponse"
|
||||
responseText = "Try breaking down the task into smaller steps."
|
||||
break
|
||||
|
||||
case "auto_approval_max_req_reached":
|
||||
// Reset the count to continue
|
||||
response.askResponse = "yesButtonClicked" // "Reset and continue" button
|
||||
responseType = "yesButtonClicked" // "Reset and continue" button
|
||||
break
|
||||
|
||||
case "resume_task":
|
||||
case "resume_completed_task":
|
||||
// Resume the task
|
||||
response.askResponse = "messageResponse"
|
||||
responseType = "messageResponse"
|
||||
break
|
||||
|
||||
case "new_task":
|
||||
// Decline creating a new task to keep the current task running
|
||||
response.askResponse = "messageResponse"
|
||||
response.text = "Continue with the current task."
|
||||
responseType = "messageResponse"
|
||||
responseText = "Continue with the current task."
|
||||
break
|
||||
|
||||
case "plan_mode_respond":
|
||||
// Respond to plan mode with a message to toggle to Act mode
|
||||
response.askResponse = "messageResponse"
|
||||
response.text = "PLAN_MODE_TOGGLE_RESPONSE" // Special marker to toggle to Act mode
|
||||
responseType = "messageResponse"
|
||||
responseText = "PLAN_MODE_TOGGLE_RESPONSE" // Special marker to toggle to Act mode
|
||||
|
||||
// Automatically toggle to Act mode after responding
|
||||
setTimeout(async () => {
|
||||
@@ -616,8 +615,16 @@ function autoRespondToAsk(webviewProvider: WebviewProvider, askType: ClineAsk, a
|
||||
}
|
||||
|
||||
// Send the response message
|
||||
webviewProvider.controller.handleWebviewMessage(response)
|
||||
Logger.log(`Auto-responded to ${askType} with ${response.askResponse}`)
|
||||
try {
|
||||
await TaskServiceClient.askResponse({
|
||||
responseType,
|
||||
text: responseText,
|
||||
images: responseImages,
|
||||
})
|
||||
Logger.log(`Auto-responded to ${askType} with ${responseType}`)
|
||||
} catch (error) {
|
||||
Logger.log(`Error sending askResponse: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -35,5 +35,5 @@ export const DEFAULT_AUTO_APPROVAL_SETTINGS: AutoApprovalSettings = {
|
||||
},
|
||||
maxRequests: 20,
|
||||
enableNotifications: false,
|
||||
favorites: ["enableAll", "readFiles", "editFiles"],
|
||||
favorites: ["enableAutoApprove", "readFiles", "editFiles"],
|
||||
}
|
||||
|
||||
@@ -115,6 +115,7 @@ export type Platform = "aix" | "darwin" | "freebsd" | "linux" | "openbsd" | "sun
|
||||
export const DEFAULT_PLATFORM = "unknown"
|
||||
|
||||
export interface ExtensionState {
|
||||
isNewUser: boolean
|
||||
apiConfiguration?: ApiConfiguration
|
||||
autoApprovalSettings: AutoApprovalSettings
|
||||
browserSettings: BrowserSettings
|
||||
@@ -142,6 +143,7 @@ export interface ExtensionState {
|
||||
vscMachineId: string
|
||||
globalClineRulesToggles: ClineRulesToggles
|
||||
localClineRulesToggles: ClineRulesToggles
|
||||
workflowToggles: ClineRulesToggles
|
||||
localCursorRulesToggles: ClineRulesToggles
|
||||
localWindsurfRulesToggles: ClineRulesToggles
|
||||
}
|
||||
@@ -206,6 +208,7 @@ export type ClineSay =
|
||||
| "clineignore_error"
|
||||
| "checkpoint_created"
|
||||
| "load_mcp_documentation"
|
||||
| "info" // Added for general informational messages like retry status
|
||||
|
||||
export interface ClineSayTool {
|
||||
tool:
|
||||
@@ -274,8 +277,14 @@ export interface ClineApiReqInfo {
|
||||
cost?: number
|
||||
cancelReason?: ClineApiReqCancelReason
|
||||
streamingFailedMessage?: string
|
||||
retryStatus?: {
|
||||
attempt: number
|
||||
maxAttempts: number
|
||||
delaySec: number
|
||||
errorSnippet?: string
|
||||
}
|
||||
}
|
||||
|
||||
export type ClineApiReqCancelReason = "streaming_failed" | "user_cancelled"
|
||||
export type ClineApiReqCancelReason = "streaming_failed" | "user_cancelled" | "retries_exhausted"
|
||||
|
||||
export const COMPLETION_RESULT_CHANGES_FLAG = "HAS_CHANGES"
|
||||
|
||||
@@ -14,25 +14,19 @@ export interface WebviewMessage {
|
||||
| "newTask"
|
||||
| "condense"
|
||||
| "reportBug"
|
||||
| "askResponse"
|
||||
| "didShowAnnouncement"
|
||||
| "selectImages"
|
||||
| "resetState"
|
||||
| "openInBrowser"
|
||||
| "openMention"
|
||||
| "showChatView"
|
||||
| "refreshClineRules"
|
||||
| "openMcpSettings"
|
||||
| "restartMcpServer"
|
||||
| "deleteMcpServer"
|
||||
| "autoApprovalSettings"
|
||||
| "browserRelaunchResult"
|
||||
| "togglePlanActMode"
|
||||
| "taskCompletionViewChanges"
|
||||
| "openExtensionSettings"
|
||||
| "requestVsCodeLmModels"
|
||||
| "toggleToolAutoApprove"
|
||||
| "accountLogoutClicked"
|
||||
| "showAccountViewClicked"
|
||||
| "authStateChanged"
|
||||
| "authCallback"
|
||||
@@ -42,7 +36,6 @@ export interface WebviewMessage {
|
||||
| "fetchLatestMcpServersFromHub"
|
||||
| "telemetrySetting"
|
||||
| "openSettings"
|
||||
| "fetchOpenGraphData"
|
||||
| "invoke"
|
||||
| "updateSettings"
|
||||
| "clearAllTaskHistory"
|
||||
@@ -50,7 +43,6 @@ export interface WebviewMessage {
|
||||
| "optionsResponse"
|
||||
| "requestTotalTasksSize"
|
||||
| "relaunchChromeDebugMode"
|
||||
| "taskFeedback"
|
||||
| "scrollToSettings"
|
||||
| "searchFiles"
|
||||
| "grpc_request"
|
||||
@@ -58,15 +50,14 @@ export interface WebviewMessage {
|
||||
| "toggleClineRule"
|
||||
| "toggleCursorRule"
|
||||
| "toggleWindsurfRule"
|
||||
| "toggleWorkflow"
|
||||
| "deleteClineRule"
|
||||
| "copyToClipboard"
|
||||
| "updateTerminalConnectionTimeout"
|
||||
| "setActiveQuote"
|
||||
|
||||
// | "relaunchChromeDebugMode"
|
||||
text?: string
|
||||
disabled?: boolean
|
||||
askResponse?: ClineAskResponse
|
||||
apiConfiguration?: ApiConfiguration
|
||||
images?: string[]
|
||||
bool?: boolean
|
||||
@@ -94,8 +85,6 @@ export interface WebviewMessage {
|
||||
mcpMarketplaceEnabled?: boolean
|
||||
telemetrySetting?: TelemetrySetting
|
||||
customInstructionsSetting?: string
|
||||
// For task feedback
|
||||
feedbackType?: TaskFeedbackType
|
||||
mentionsRequestId?: string
|
||||
query?: string
|
||||
// For toggleFavoriteModel
|
||||
@@ -110,9 +99,10 @@ export interface WebviewMessage {
|
||||
grpc_request_cancel?: {
|
||||
request_id: string // ID of the request to cancel
|
||||
}
|
||||
// For cline rules
|
||||
// For cline rules and workflows
|
||||
isGlobal?: boolean
|
||||
rulePath?: string
|
||||
workflowPath?: string
|
||||
enabled?: boolean
|
||||
filename?: string
|
||||
|
||||
|
||||
@@ -88,6 +88,7 @@ export interface ApiHandlerOptions {
|
||||
reasoningEffort?: string
|
||||
sambanovaApiKey?: string
|
||||
requestTimeoutMs?: number
|
||||
onRetryAttempt?: (attempt: number, maxRetries: number, delay: number, error: any) => void
|
||||
}
|
||||
|
||||
export type ApiConfiguration = ApiHandlerOptions & {
|
||||
@@ -114,6 +115,7 @@ export interface ModelInfo {
|
||||
outputPrice?: number // Output price per million tokens when budget > 0
|
||||
outputPriceTiers?: PriceTier[] // Optional: Tiered output price when budget > 0
|
||||
}
|
||||
supportsGlobalEndpoint?: boolean // Whether the model supports a global endpoint with Vertex AI
|
||||
cacheWritesPrice?: number
|
||||
cacheReadsPrice?: number
|
||||
description?: string
|
||||
@@ -403,6 +405,7 @@ export const vertexModels = {
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsGlobalEndpoint: true,
|
||||
inputPrice: 0.15,
|
||||
outputPrice: 0.6,
|
||||
cacheWritesPrice: 1.0,
|
||||
@@ -413,6 +416,7 @@ export const vertexModels = {
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
supportsGlobalEndpoint: true,
|
||||
inputPrice: 0.075,
|
||||
outputPrice: 0.3,
|
||||
},
|
||||
@@ -421,6 +425,7 @@ export const vertexModels = {
|
||||
contextWindow: 32_767,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
supportsGlobalEndpoint: true,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
@@ -429,6 +434,7 @@ export const vertexModels = {
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
supportsGlobalEndpoint: true,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
@@ -445,6 +451,7 @@ export const vertexModels = {
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsGlobalEndpoint: true,
|
||||
inputPrice: 2.5,
|
||||
outputPrice: 15,
|
||||
cacheReadsPrice: 0.31,
|
||||
@@ -468,6 +475,7 @@ export const vertexModels = {
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
supportsGlobalEndpoint: true,
|
||||
inputPrice: 0.15,
|
||||
outputPrice: 0.6,
|
||||
thinkingConfig: {
|
||||
@@ -480,6 +488,7 @@ export const vertexModels = {
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
supportsGlobalEndpoint: true,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
@@ -549,6 +558,10 @@ export const vertexModels = {
|
||||
},
|
||||
} as const satisfies Record<string, ModelInfo>
|
||||
|
||||
export const vertexGlobalModels: Record<string, ModelInfo> = Object.fromEntries(
|
||||
Object.entries(vertexModels).filter(([_k, v]) => v.hasOwnProperty("supportsGlobalEndpoint")),
|
||||
) as Record<string, ModelInfo>
|
||||
|
||||
export const openAiModelInfoSaneDefaults: OpenAiCompatibleModelInfo = {
|
||||
maxTokens: -1,
|
||||
contextWindow: 128_000,
|
||||
|
||||
@@ -2,22 +2,24 @@ import { RuleFileRequest } from "../../proto/file"
|
||||
|
||||
// Helper for creating delete requests
|
||||
export const DeleteRuleFileRequest = {
|
||||
create: (params: { rulePath: string; isGlobal: boolean; metadata?: any }): RuleFileRequest => {
|
||||
create: (params: { rulePath: string; isGlobal: boolean; metadata?: any; type?: string }): RuleFileRequest => {
|
||||
return RuleFileRequest.create({
|
||||
rulePath: params.rulePath,
|
||||
isGlobal: params.isGlobal,
|
||||
metadata: params.metadata,
|
||||
type: params.type,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
// Helper for creating create requests
|
||||
export const CreateRuleFileRequest = {
|
||||
create: (params: { filename: string; isGlobal: boolean; metadata?: any }): RuleFileRequest => {
|
||||
create: (params: { filename: string; isGlobal: boolean; metadata?: any; type?: string }): RuleFileRequest => {
|
||||
return RuleFileRequest.create({
|
||||
filename: params.filename,
|
||||
isGlobal: params.isGlobal,
|
||||
metadata: params.metadata,
|
||||
type: params.type,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { OpenGraphData as DomainOpenGraphData } from "@integrations/misc/link-preview"
|
||||
import { OpenGraphData as ProtoOpenGraphData } from "@shared/proto/web"
|
||||
|
||||
/**
|
||||
* Converts domain OpenGraphData objects to proto OpenGraphData objects
|
||||
* @param ogData Domain OpenGraphData object
|
||||
* @returns Proto OpenGraphData object
|
||||
*/
|
||||
export function convertDomainOpenGraphDataToProto(ogData: DomainOpenGraphData): ProtoOpenGraphData {
|
||||
return ProtoOpenGraphData.create({
|
||||
title: ogData.title || "",
|
||||
description: ogData.description || "",
|
||||
image: ogData.image || "",
|
||||
url: ogData.url || "",
|
||||
siteName: ogData.siteName || "",
|
||||
type: ogData.type || "",
|
||||
})
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
// source: account.proto
|
||||
|
||||
/* eslint-disable */
|
||||
import { EmptyRequest, String } from "./common"
|
||||
import { Empty, EmptyRequest, String } from "./common"
|
||||
|
||||
export const protobufPackage = "cline"
|
||||
|
||||
@@ -28,5 +28,17 @@ export const AccountServiceDefinition = {
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
/**
|
||||
* Handles the user clicking the logout button in the UI.
|
||||
* Clears API keys and user state.
|
||||
*/
|
||||
accountLogoutClicked: {
|
||||
name: "accountLogoutClicked",
|
||||
requestType: EmptyRequest,
|
||||
requestStream: false,
|
||||
responseType: Empty,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
} as const
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
/* eslint-disable */
|
||||
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"
|
||||
import { Empty, Metadata, StringRequest } from "./common"
|
||||
import { Empty, EmptyRequest, Metadata, StringArray, StringRequest } from "./common"
|
||||
|
||||
export const protobufPackage = "cline"
|
||||
|
||||
@@ -73,6 +73,8 @@ export interface RuleFileRequest {
|
||||
rulePath?: string | undefined
|
||||
/** Filename field for createRuleFile (optional) */
|
||||
filename?: string | undefined
|
||||
/** Type of the file to create (optional) */
|
||||
type?: string | undefined
|
||||
}
|
||||
|
||||
/** Result for rule file operations with meaningful data only */
|
||||
@@ -682,7 +684,7 @@ export const GitCommit: MessageFns<GitCommit> = {
|
||||
}
|
||||
|
||||
function createBaseRuleFileRequest(): RuleFileRequest {
|
||||
return { metadata: undefined, isGlobal: false, rulePath: undefined, filename: undefined }
|
||||
return { metadata: undefined, isGlobal: false, rulePath: undefined, filename: undefined, type: undefined }
|
||||
}
|
||||
|
||||
export const RuleFileRequest: MessageFns<RuleFileRequest> = {
|
||||
@@ -699,6 +701,9 @@ export const RuleFileRequest: MessageFns<RuleFileRequest> = {
|
||||
if (message.filename !== undefined) {
|
||||
writer.uint32(34).string(message.filename)
|
||||
}
|
||||
if (message.type !== undefined) {
|
||||
writer.uint32(42).string(message.type)
|
||||
}
|
||||
return writer
|
||||
},
|
||||
|
||||
@@ -741,6 +746,14 @@ export const RuleFileRequest: MessageFns<RuleFileRequest> = {
|
||||
message.filename = reader.string()
|
||||
continue
|
||||
}
|
||||
case 5: {
|
||||
if (tag !== 42) {
|
||||
break
|
||||
}
|
||||
|
||||
message.type = reader.string()
|
||||
continue
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break
|
||||
@@ -756,6 +769,7 @@ export const RuleFileRequest: MessageFns<RuleFileRequest> = {
|
||||
isGlobal: isSet(object.isGlobal) ? globalThis.Boolean(object.isGlobal) : false,
|
||||
rulePath: isSet(object.rulePath) ? globalThis.String(object.rulePath) : undefined,
|
||||
filename: isSet(object.filename) ? globalThis.String(object.filename) : undefined,
|
||||
type: isSet(object.type) ? globalThis.String(object.type) : undefined,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -773,6 +787,9 @@ export const RuleFileRequest: MessageFns<RuleFileRequest> = {
|
||||
if (message.filename !== undefined) {
|
||||
obj.filename = message.filename
|
||||
}
|
||||
if (message.type !== undefined) {
|
||||
obj.type = message.type
|
||||
}
|
||||
return obj
|
||||
},
|
||||
|
||||
@@ -786,6 +803,7 @@ export const RuleFileRequest: MessageFns<RuleFileRequest> = {
|
||||
message.isGlobal = object.isGlobal ?? false
|
||||
message.rulePath = object.rulePath ?? undefined
|
||||
message.filename = object.filename ?? undefined
|
||||
message.type = object.type ?? undefined
|
||||
return message
|
||||
},
|
||||
}
|
||||
@@ -888,6 +906,15 @@ export const FileServiceDefinition = {
|
||||
name: "FileService",
|
||||
fullName: "cline.FileService",
|
||||
methods: {
|
||||
/** Copies text to clipboard */
|
||||
copyToClipboard: {
|
||||
name: "copyToClipboard",
|
||||
requestType: StringRequest,
|
||||
requestStream: false,
|
||||
responseType: Empty,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
/** Opens a file in the editor */
|
||||
openFile: {
|
||||
name: "openFile",
|
||||
@@ -933,6 +960,15 @@ export const FileServiceDefinition = {
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
/** Select images from the file system and return as data URLs */
|
||||
selectImages: {
|
||||
name: "selectImages",
|
||||
requestType: EmptyRequest,
|
||||
requestStream: false,
|
||||
responseType: StringArray,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
/** Convert URIs to workspace-relative paths */
|
||||
getRelativePaths: {
|
||||
name: "getRelativePaths",
|
||||
|
||||
@@ -1012,6 +1012,14 @@ export const McpServiceDefinition = {
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
restartMcpServer: {
|
||||
name: "restartMcpServer",
|
||||
requestType: StringRequest,
|
||||
requestStream: false,
|
||||
responseType: McpServers,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
} as const
|
||||
|
||||
|
||||
@@ -101,6 +101,14 @@ export const StateServiceDefinition = {
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
resetState: {
|
||||
name: "resetState",
|
||||
requestType: EmptyRequest,
|
||||
requestStream: false,
|
||||
responseType: Empty,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
} as const
|
||||
|
||||
|
||||
+145
-1
@@ -6,7 +6,7 @@
|
||||
|
||||
/* eslint-disable */
|
||||
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"
|
||||
import { Empty, EmptyRequest, Metadata, StringArrayRequest, StringRequest } from "./common"
|
||||
import { Empty, EmptyRequest, Int64Request, Metadata, StringArrayRequest, StringRequest } from "./common"
|
||||
|
||||
export const protobufPackage = "cline"
|
||||
|
||||
@@ -73,6 +73,14 @@ export interface TaskItem {
|
||||
cacheReads: number
|
||||
}
|
||||
|
||||
/** Request for ask response operation */
|
||||
export interface AskResponseRequest {
|
||||
metadata?: Metadata | undefined
|
||||
responseType: string
|
||||
text: string
|
||||
images: string[]
|
||||
}
|
||||
|
||||
function createBaseNewTaskRequest(): NewTaskRequest {
|
||||
return { metadata: undefined, text: "", images: [] }
|
||||
}
|
||||
@@ -966,6 +974,115 @@ export const TaskItem: MessageFns<TaskItem> = {
|
||||
},
|
||||
}
|
||||
|
||||
function createBaseAskResponseRequest(): AskResponseRequest {
|
||||
return { metadata: undefined, responseType: "", text: "", images: [] }
|
||||
}
|
||||
|
||||
export const AskResponseRequest: MessageFns<AskResponseRequest> = {
|
||||
encode(message: AskResponseRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.metadata !== undefined) {
|
||||
Metadata.encode(message.metadata, writer.uint32(10).fork()).join()
|
||||
}
|
||||
if (message.responseType !== "") {
|
||||
writer.uint32(18).string(message.responseType)
|
||||
}
|
||||
if (message.text !== "") {
|
||||
writer.uint32(26).string(message.text)
|
||||
}
|
||||
for (const v of message.images) {
|
||||
writer.uint32(34).string(v!)
|
||||
}
|
||||
return writer
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): AskResponseRequest {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
|
||||
let end = length === undefined ? reader.len : reader.pos + length
|
||||
const message = createBaseAskResponseRequest()
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32()
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break
|
||||
}
|
||||
|
||||
message.metadata = Metadata.decode(reader, reader.uint32())
|
||||
continue
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 18) {
|
||||
break
|
||||
}
|
||||
|
||||
message.responseType = reader.string()
|
||||
continue
|
||||
}
|
||||
case 3: {
|
||||
if (tag !== 26) {
|
||||
break
|
||||
}
|
||||
|
||||
message.text = reader.string()
|
||||
continue
|
||||
}
|
||||
case 4: {
|
||||
if (tag !== 34) {
|
||||
break
|
||||
}
|
||||
|
||||
message.images.push(reader.string())
|
||||
continue
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break
|
||||
}
|
||||
reader.skip(tag & 7)
|
||||
}
|
||||
return message
|
||||
},
|
||||
|
||||
fromJSON(object: any): AskResponseRequest {
|
||||
return {
|
||||
metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined,
|
||||
responseType: isSet(object.responseType) ? globalThis.String(object.responseType) : "",
|
||||
text: isSet(object.text) ? globalThis.String(object.text) : "",
|
||||
images: globalThis.Array.isArray(object?.images) ? object.images.map((e: any) => globalThis.String(e)) : [],
|
||||
}
|
||||
},
|
||||
|
||||
toJSON(message: AskResponseRequest): unknown {
|
||||
const obj: any = {}
|
||||
if (message.metadata !== undefined) {
|
||||
obj.metadata = Metadata.toJSON(message.metadata)
|
||||
}
|
||||
if (message.responseType !== "") {
|
||||
obj.responseType = message.responseType
|
||||
}
|
||||
if (message.text !== "") {
|
||||
obj.text = message.text
|
||||
}
|
||||
if (message.images?.length) {
|
||||
obj.images = message.images
|
||||
}
|
||||
return obj
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<AskResponseRequest>, I>>(base?: I): AskResponseRequest {
|
||||
return AskResponseRequest.fromPartial(base ?? ({} as any))
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<AskResponseRequest>, I>>(object: I): AskResponseRequest {
|
||||
const message = createBaseAskResponseRequest()
|
||||
message.metadata =
|
||||
object.metadata !== undefined && object.metadata !== null ? Metadata.fromPartial(object.metadata) : undefined
|
||||
message.responseType = object.responseType ?? ""
|
||||
message.text = object.text ?? ""
|
||||
message.images = object.images?.map((e) => e) || []
|
||||
return message
|
||||
},
|
||||
}
|
||||
|
||||
export type TaskServiceDefinition = typeof TaskServiceDefinition
|
||||
export const TaskServiceDefinition = {
|
||||
name: "TaskService",
|
||||
@@ -1052,6 +1169,33 @@ export const TaskServiceDefinition = {
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
/** Sends a response to a previous ask operation */
|
||||
askResponse: {
|
||||
name: "askResponse",
|
||||
requestType: AskResponseRequest,
|
||||
requestStream: false,
|
||||
responseType: Empty,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
/** Records task feedback (thumbs up/down) */
|
||||
taskFeedback: {
|
||||
name: "taskFeedback",
|
||||
requestType: StringRequest,
|
||||
requestStream: false,
|
||||
responseType: Empty,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
/** Shows task completion changes diff in a view */
|
||||
taskCompletionViewChanges: {
|
||||
name: "taskCompletionViewChanges",
|
||||
requestType: Int64Request,
|
||||
requestStream: false,
|
||||
responseType: Empty,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
} as const
|
||||
|
||||
|
||||
@@ -15,6 +15,15 @@ export interface IsImageUrl {
|
||||
url: string
|
||||
}
|
||||
|
||||
export interface OpenGraphData {
|
||||
title: string
|
||||
description: string
|
||||
image: string
|
||||
url: string
|
||||
siteName: string
|
||||
type: string
|
||||
}
|
||||
|
||||
function createBaseIsImageUrl(): IsImageUrl {
|
||||
return { isImage: false, url: "" }
|
||||
}
|
||||
@@ -91,6 +100,146 @@ export const IsImageUrl: MessageFns<IsImageUrl> = {
|
||||
},
|
||||
}
|
||||
|
||||
function createBaseOpenGraphData(): OpenGraphData {
|
||||
return { title: "", description: "", image: "", url: "", siteName: "", type: "" }
|
||||
}
|
||||
|
||||
export const OpenGraphData: MessageFns<OpenGraphData> = {
|
||||
encode(message: OpenGraphData, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.title !== "") {
|
||||
writer.uint32(10).string(message.title)
|
||||
}
|
||||
if (message.description !== "") {
|
||||
writer.uint32(18).string(message.description)
|
||||
}
|
||||
if (message.image !== "") {
|
||||
writer.uint32(26).string(message.image)
|
||||
}
|
||||
if (message.url !== "") {
|
||||
writer.uint32(34).string(message.url)
|
||||
}
|
||||
if (message.siteName !== "") {
|
||||
writer.uint32(42).string(message.siteName)
|
||||
}
|
||||
if (message.type !== "") {
|
||||
writer.uint32(50).string(message.type)
|
||||
}
|
||||
return writer
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): OpenGraphData {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
|
||||
let end = length === undefined ? reader.len : reader.pos + length
|
||||
const message = createBaseOpenGraphData()
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32()
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break
|
||||
}
|
||||
|
||||
message.title = reader.string()
|
||||
continue
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 18) {
|
||||
break
|
||||
}
|
||||
|
||||
message.description = reader.string()
|
||||
continue
|
||||
}
|
||||
case 3: {
|
||||
if (tag !== 26) {
|
||||
break
|
||||
}
|
||||
|
||||
message.image = reader.string()
|
||||
continue
|
||||
}
|
||||
case 4: {
|
||||
if (tag !== 34) {
|
||||
break
|
||||
}
|
||||
|
||||
message.url = reader.string()
|
||||
continue
|
||||
}
|
||||
case 5: {
|
||||
if (tag !== 42) {
|
||||
break
|
||||
}
|
||||
|
||||
message.siteName = reader.string()
|
||||
continue
|
||||
}
|
||||
case 6: {
|
||||
if (tag !== 50) {
|
||||
break
|
||||
}
|
||||
|
||||
message.type = reader.string()
|
||||
continue
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break
|
||||
}
|
||||
reader.skip(tag & 7)
|
||||
}
|
||||
return message
|
||||
},
|
||||
|
||||
fromJSON(object: any): OpenGraphData {
|
||||
return {
|
||||
title: isSet(object.title) ? globalThis.String(object.title) : "",
|
||||
description: isSet(object.description) ? globalThis.String(object.description) : "",
|
||||
image: isSet(object.image) ? globalThis.String(object.image) : "",
|
||||
url: isSet(object.url) ? globalThis.String(object.url) : "",
|
||||
siteName: isSet(object.siteName) ? globalThis.String(object.siteName) : "",
|
||||
type: isSet(object.type) ? globalThis.String(object.type) : "",
|
||||
}
|
||||
},
|
||||
|
||||
toJSON(message: OpenGraphData): unknown {
|
||||
const obj: any = {}
|
||||
if (message.title !== "") {
|
||||
obj.title = message.title
|
||||
}
|
||||
if (message.description !== "") {
|
||||
obj.description = message.description
|
||||
}
|
||||
if (message.image !== "") {
|
||||
obj.image = message.image
|
||||
}
|
||||
if (message.url !== "") {
|
||||
obj.url = message.url
|
||||
}
|
||||
if (message.siteName !== "") {
|
||||
obj.siteName = message.siteName
|
||||
}
|
||||
if (message.type !== "") {
|
||||
obj.type = message.type
|
||||
}
|
||||
return obj
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<OpenGraphData>, I>>(base?: I): OpenGraphData {
|
||||
return OpenGraphData.fromPartial(base ?? ({} as any))
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<OpenGraphData>, I>>(object: I): OpenGraphData {
|
||||
const message = createBaseOpenGraphData()
|
||||
message.title = object.title ?? ""
|
||||
message.description = object.description ?? ""
|
||||
message.image = object.image ?? ""
|
||||
message.url = object.url ?? ""
|
||||
message.siteName = object.siteName ?? ""
|
||||
message.type = object.type ?? ""
|
||||
return message
|
||||
},
|
||||
}
|
||||
|
||||
export type WebServiceDefinition = typeof WebServiceDefinition
|
||||
export const WebServiceDefinition = {
|
||||
name: "WebService",
|
||||
@@ -104,6 +253,14 @@ export const WebServiceDefinition = {
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
fetchOpenGraphData: {
|
||||
name: "fetchOpenGraphData",
|
||||
requestType: StringRequest,
|
||||
requestStream: false,
|
||||
responseType: OpenGraphData,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
} as const
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { GrpcHandlerWrapper, GrpcStreamingResponseHandlerWrapper } from "./grpc-
|
||||
|
||||
// Account Service
|
||||
import { accountLoginClicked } from "../core/controller/account/accountLoginClicked"
|
||||
import { accountLogoutClicked } from "../core/controller/account/accountLogoutClicked"
|
||||
|
||||
// Browser Service
|
||||
import { getBrowserConnectionInfo } from "../core/controller/browser/getBrowserConnectionInfo"
|
||||
@@ -19,11 +20,13 @@ import { checkpointDiff } from "../core/controller/checkpoints/checkpointDiff"
|
||||
import { checkpointRestore } from "../core/controller/checkpoints/checkpointRestore"
|
||||
|
||||
// File Service
|
||||
import { copyToClipboard } from "../core/controller/file/copyToClipboard"
|
||||
import { openFile } from "../core/controller/file/openFile"
|
||||
import { openImage } from "../core/controller/file/openImage"
|
||||
import { deleteRuleFile } from "../core/controller/file/deleteRuleFile"
|
||||
import { createRuleFile } from "../core/controller/file/createRuleFile"
|
||||
import { searchCommits } from "../core/controller/file/searchCommits"
|
||||
import { selectImages } from "../core/controller/file/selectImages"
|
||||
import { getRelativePaths } from "../core/controller/file/getRelativePaths"
|
||||
import { searchFiles } from "../core/controller/file/searchFiles"
|
||||
|
||||
@@ -32,6 +35,7 @@ import { toggleMcpServer } from "../core/controller/mcp/toggleMcpServer"
|
||||
import { updateMcpTimeout } from "../core/controller/mcp/updateMcpTimeout"
|
||||
import { addRemoteMcpServer } from "../core/controller/mcp/addRemoteMcpServer"
|
||||
import { downloadMcp } from "../core/controller/mcp/downloadMcp"
|
||||
import { restartMcpServer } from "../core/controller/mcp/restartMcpServer"
|
||||
|
||||
// Models Service
|
||||
import { getOllamaModels } from "../core/controller/models/getOllamaModels"
|
||||
@@ -49,6 +53,7 @@ import { condense } from "../core/controller/slash/condense"
|
||||
import { getLatestState } from "../core/controller/state/getLatestState"
|
||||
import { subscribeToState } from "../core/controller/state/subscribeToState"
|
||||
import { toggleFavoriteModel } from "../core/controller/state/toggleFavoriteModel"
|
||||
import { resetState } from "../core/controller/state/resetState"
|
||||
|
||||
// Task Service
|
||||
import { cancelTask } from "../core/controller/task/cancelTask"
|
||||
@@ -60,9 +65,13 @@ import { exportTaskWithId } from "../core/controller/task/exportTaskWithId"
|
||||
import { toggleTaskFavorite } from "../core/controller/task/toggleTaskFavorite"
|
||||
import { deleteNonFavoritedTasks } from "../core/controller/task/deleteNonFavoritedTasks"
|
||||
import { getTaskHistory } from "../core/controller/task/getTaskHistory"
|
||||
import { askResponse } from "../core/controller/task/askResponse"
|
||||
import { taskFeedback } from "../core/controller/task/taskFeedback"
|
||||
import { taskCompletionViewChanges } from "../core/controller/task/taskCompletionViewChanges"
|
||||
|
||||
// Web Service
|
||||
import { checkIsImageUrl } from "../core/controller/web/checkIsImageUrl"
|
||||
import { fetchOpenGraphData } from "../core/controller/web/fetchOpenGraphData"
|
||||
|
||||
export function addServices(
|
||||
server: grpc.Server,
|
||||
@@ -74,6 +83,7 @@ export function addServices(
|
||||
// Account Service
|
||||
server.addService(proto.cline.AccountService.service, {
|
||||
accountLoginClicked: wrapper(accountLoginClicked, controller),
|
||||
accountLogoutClicked: wrapper(accountLogoutClicked, controller),
|
||||
})
|
||||
|
||||
// Browser Service
|
||||
@@ -93,11 +103,13 @@ export function addServices(
|
||||
|
||||
// File Service
|
||||
server.addService(proto.cline.FileService.service, {
|
||||
copyToClipboard: wrapper(copyToClipboard, controller),
|
||||
openFile: wrapper(openFile, controller),
|
||||
openImage: wrapper(openImage, controller),
|
||||
deleteRuleFile: wrapper(deleteRuleFile, controller),
|
||||
createRuleFile: wrapper(createRuleFile, controller),
|
||||
searchCommits: wrapper(searchCommits, controller),
|
||||
selectImages: wrapper(selectImages, controller),
|
||||
getRelativePaths: wrapper(getRelativePaths, controller),
|
||||
searchFiles: wrapper(searchFiles, controller),
|
||||
})
|
||||
@@ -108,6 +120,7 @@ export function addServices(
|
||||
updateMcpTimeout: wrapper(updateMcpTimeout, controller),
|
||||
addRemoteMcpServer: wrapper(addRemoteMcpServer, controller),
|
||||
downloadMcp: wrapper(downloadMcp, controller),
|
||||
restartMcpServer: wrapper(restartMcpServer, controller),
|
||||
})
|
||||
|
||||
// Models Service
|
||||
@@ -131,6 +144,7 @@ export function addServices(
|
||||
getLatestState: wrapper(getLatestState, controller),
|
||||
subscribeToState: wrapStreamingResponse(subscribeToState, controller),
|
||||
toggleFavoriteModel: wrapper(toggleFavoriteModel, controller),
|
||||
resetState: wrapper(resetState, controller),
|
||||
})
|
||||
|
||||
// Task Service
|
||||
@@ -144,10 +158,14 @@ export function addServices(
|
||||
toggleTaskFavorite: wrapper(toggleTaskFavorite, controller),
|
||||
deleteNonFavoritedTasks: wrapper(deleteNonFavoritedTasks, controller),
|
||||
getTaskHistory: wrapper(getTaskHistory, controller),
|
||||
askResponse: wrapper(askResponse, controller),
|
||||
taskFeedback: wrapper(taskFeedback, controller),
|
||||
taskCompletionViewChanges: wrapper(taskCompletionViewChanges, controller),
|
||||
})
|
||||
|
||||
// Web Service
|
||||
server.addService(proto.cline.WebService.service, {
|
||||
checkIsImageUrl: wrapper(checkIsImageUrl, controller),
|
||||
fetchOpenGraphData: wrapper(fetchOpenGraphData, controller),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -37,7 +37,18 @@ describe("Chat Integration Tests", () => {
|
||||
break;
|
||||
case 'invoke':
|
||||
if (message.invoke === 'primaryButtonClick') {
|
||||
vscode.postMessage({ type: 'askResponse', askResponse: 'yesButtonClicked' });
|
||||
vscode.postMessage({
|
||||
type: 'grpc_request',
|
||||
grpc_request: {
|
||||
service: 'cline.TaskService',
|
||||
method: 'askResponse',
|
||||
message: {
|
||||
responseType: 'yesButtonClicked'
|
||||
},
|
||||
request_id: 'test-request-id',
|
||||
is_streaming: false
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -117,10 +128,14 @@ describe("Chat Integration Tests", () => {
|
||||
})
|
||||
|
||||
it("should handle tool approval flow", async () => {
|
||||
// Set up approval listener
|
||||
// Set up approval listener for gRPC request
|
||||
const approvalPromise = new Promise<any>((resolve) => {
|
||||
panel.webview.onDidReceiveMessage((message) => {
|
||||
if (message.type === "askResponse") {
|
||||
if (
|
||||
message.type === "grpc_request" &&
|
||||
message.grpc_request?.service === "cline.TaskService" &&
|
||||
message.grpc_request?.method === "askResponse"
|
||||
) {
|
||||
resolve(message)
|
||||
}
|
||||
})
|
||||
@@ -132,9 +147,11 @@ describe("Chat Integration Tests", () => {
|
||||
invoke: "primaryButtonClick",
|
||||
})
|
||||
|
||||
// Verify approval was sent
|
||||
// Verify gRPC request was sent with correct parameters
|
||||
const response = await approvalPromise
|
||||
assert.equal(response.type, "askResponse")
|
||||
assert.equal(response.askResponse, "yesButtonClicked")
|
||||
assert.equal(response.type, "grpc_request")
|
||||
assert.equal(response.grpc_request.service, "cline.TaskService")
|
||||
assert.equal(response.grpc_request.method, "askResponse")
|
||||
assert.equal(response.grpc_request.message.responseType, "yesButtonClicked")
|
||||
})
|
||||
})
|
||||
|
||||
+169
-1
@@ -3,7 +3,7 @@ import { after, describe, it } from "mocha"
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
import "should"
|
||||
import { createDirectoriesForFile, fileExistsAtPath, isDirectory } from "./fs"
|
||||
import { createDirectoriesForFile, fileExistsAtPath, isDirectory, readDirectory } from "./fs"
|
||||
|
||||
describe("Filesystem Utilities", () => {
|
||||
const tmpDir = path.join(os.tmpdir(), "cline-test-" + Math.random().toString(36).slice(2))
|
||||
@@ -88,4 +88,172 @@ describe("Filesystem Utilities", () => {
|
||||
isDir.should.be.false()
|
||||
})
|
||||
})
|
||||
|
||||
describe("readDirectory", () => {
|
||||
it("should list files in a directory", async () => {
|
||||
// Create test directory with files
|
||||
const testDir = path.join(tmpDir, "read-test")
|
||||
await fs.mkdir(testDir, { recursive: true })
|
||||
await fs.writeFile(path.join(testDir, "file1.txt"), "content")
|
||||
await fs.writeFile(path.join(testDir, "file2.txt"), "content")
|
||||
|
||||
// Get files
|
||||
const files = await readDirectory(testDir)
|
||||
files.length.should.equal(2)
|
||||
files.should.containDeep([path.resolve(testDir, "file1.txt"), path.resolve(testDir, "file2.txt")])
|
||||
})
|
||||
|
||||
it("should exclude specified directories", async () => {
|
||||
// Create test directory with files and an excluded directory
|
||||
const testDir = path.join(tmpDir, "exclude-test")
|
||||
const excludeDir = path.join(testDir, "exclude-me")
|
||||
await fs.mkdir(excludeDir, { recursive: true })
|
||||
await fs.writeFile(path.join(testDir, "include.txt"), "content")
|
||||
await fs.writeFile(path.join(excludeDir, "excluded.txt"), "content")
|
||||
|
||||
// Get files, excluding the "exclude-me" directory
|
||||
const files = await readDirectory(testDir, [["exclude-me"]])
|
||||
files.length.should.equal(1)
|
||||
files.should.containDeep([path.resolve(testDir, "include.txt")])
|
||||
files.should.not.containDeep([path.resolve(excludeDir, "excluded.txt")])
|
||||
})
|
||||
})
|
||||
|
||||
it("should correctly handle complex nested directory structures", async () => {
|
||||
// Create a complex directory structure
|
||||
const complexDir = path.join(tmpDir, "complex-test")
|
||||
|
||||
// Create main dir
|
||||
await fs.mkdir(complexDir, { recursive: true })
|
||||
await fs.writeFile(path.join(complexDir, "root.txt"), "content")
|
||||
|
||||
// Create first branch
|
||||
await fs.mkdir(path.join(complexDir, "dir1"), { recursive: true })
|
||||
await fs.writeFile(path.join(complexDir, "dir1", "file1.txt"), "content")
|
||||
|
||||
// Create second branch with nested structure
|
||||
await fs.mkdir(path.join(complexDir, "dir2", "subdir1"), { recursive: true })
|
||||
await fs.writeFile(path.join(complexDir, "dir2", "file2.txt"), "content")
|
||||
await fs.writeFile(path.join(complexDir, "dir2", "subdir1", "file3.txt"), "content")
|
||||
|
||||
// Create third branch with deep nesting
|
||||
await fs.mkdir(path.join(complexDir, "dir3", "subdir2", "deepdir"), { recursive: true })
|
||||
await fs.writeFile(path.join(complexDir, "dir3", "file4.txt"), "content")
|
||||
await fs.writeFile(path.join(complexDir, "dir3", "subdir2", "file5.txt"), "content")
|
||||
await fs.writeFile(path.join(complexDir, "dir3", "subdir2", "deepdir", "file6.txt"), "content")
|
||||
|
||||
// Get all files
|
||||
const files = await readDirectory(complexDir)
|
||||
|
||||
const expectedFiles = [
|
||||
path.resolve(complexDir, "root.txt"),
|
||||
path.resolve(complexDir, "dir1", "file1.txt"),
|
||||
path.resolve(complexDir, "dir2", "file2.txt"),
|
||||
path.resolve(complexDir, "dir2", "subdir1", "file3.txt"),
|
||||
path.resolve(complexDir, "dir3", "file4.txt"),
|
||||
path.resolve(complexDir, "dir3", "subdir2", "file5.txt"),
|
||||
path.resolve(complexDir, "dir3", "subdir2", "deepdir", "file6.txt"),
|
||||
]
|
||||
|
||||
files.length.should.equal(expectedFiles.length)
|
||||
|
||||
files.sort().should.deepEqual(expectedFiles.sort())
|
||||
})
|
||||
|
||||
it("should correctly exclude multiple directories in complex structures", async () => {
|
||||
// Use the same complex directory structure
|
||||
const complexDir = path.join(tmpDir, "complex-exclude-test")
|
||||
|
||||
// Create main dir
|
||||
await fs.mkdir(complexDir, { recursive: true })
|
||||
await fs.writeFile(path.join(complexDir, "root.txt"), "content")
|
||||
|
||||
// Create first branch
|
||||
await fs.mkdir(path.join(complexDir, "dir1"), { recursive: true })
|
||||
await fs.writeFile(path.join(complexDir, "dir1", "file1.txt"), "content")
|
||||
|
||||
// Create second branch with nested structure
|
||||
await fs.mkdir(path.join(complexDir, "dir2", "subdir1"), { recursive: true })
|
||||
await fs.writeFile(path.join(complexDir, "dir2", "file2.txt"), "content")
|
||||
await fs.writeFile(path.join(complexDir, "dir2", "subdir1", "file3.txt"), "content")
|
||||
|
||||
// Create third branch with deep nesting
|
||||
await fs.mkdir(path.join(complexDir, "dir3", "subdir2", "deepdir"), { recursive: true })
|
||||
await fs.writeFile(path.join(complexDir, "dir3", "file4.txt"), "content")
|
||||
await fs.writeFile(path.join(complexDir, "dir3", "subdir2", "file5.txt"), "content")
|
||||
await fs.writeFile(path.join(complexDir, "dir3", "subdir2", "deepdir", "file6.txt"), "content")
|
||||
|
||||
// Get files excluding multiple directories
|
||||
const files = await readDirectory(complexDir, [["dir1"], ["subdir2"]])
|
||||
|
||||
const expectedFiles = [
|
||||
path.resolve(complexDir, "root.txt"),
|
||||
path.resolve(complexDir, "dir2", "file2.txt"),
|
||||
path.resolve(complexDir, "dir2", "subdir1", "file3.txt"),
|
||||
path.resolve(complexDir, "dir3", "file4.txt"),
|
||||
]
|
||||
|
||||
files.length.should.equal(expectedFiles.length)
|
||||
|
||||
files.sort().should.deepEqual(expectedFiles.sort())
|
||||
})
|
||||
|
||||
it("should exclude .clinerules/workflows directory specifically", async () => {
|
||||
// Create a test directory structure
|
||||
const clinerulesDirTest = path.join(tmpDir, "clinerules-test")
|
||||
const clinerulesDirPath = path.join(clinerulesDirTest, ".clinerules")
|
||||
|
||||
// Create .clinerules directory and root files
|
||||
await fs.mkdir(clinerulesDirPath, { recursive: true })
|
||||
await fs.writeFile(path.join(clinerulesDirPath, "config.json"), "{}")
|
||||
await fs.writeFile(path.join(clinerulesDirPath, "settings.js"), "// settings")
|
||||
|
||||
// Create .clinerules/other directory and files
|
||||
const otherDirPath = path.join(clinerulesDirPath, "other")
|
||||
await fs.mkdir(otherDirPath, { recursive: true })
|
||||
await fs.writeFile(path.join(otherDirPath, "helper.js"), "// helper code")
|
||||
await fs.writeFile(path.join(otherDirPath, "util.js"), "// util functions")
|
||||
|
||||
// Create .clinerules/workflows directory and files
|
||||
const workflowsDirPath = path.join(clinerulesDirPath, "workflows")
|
||||
await fs.mkdir(workflowsDirPath, { recursive: true })
|
||||
await fs.writeFile(path.join(workflowsDirPath, "workflow1.js"), "// workflow1")
|
||||
await fs.writeFile(path.join(workflowsDirPath, "workflow2.js"), "// workflow2")
|
||||
|
||||
// Get all files WITHOUT exclusion
|
||||
const allFiles = await readDirectory(clinerulesDirPath)
|
||||
|
||||
// Verify all files are included
|
||||
allFiles.length.should.equal(6) // 2 in root + 2 in other + 2 in workflows
|
||||
allFiles.some((file) => file.includes("workflow1.js")).should.be.true()
|
||||
allFiles.some((file) => file.includes("workflow2.js")).should.be.true()
|
||||
|
||||
// Get files WITH workflows directory excluded
|
||||
const filteredFiles = await readDirectory(clinerulesDirPath, [[".clinerules", "workflows"]])
|
||||
|
||||
// Verify workflows files are excluded but others remain
|
||||
filteredFiles.length.should.equal(4) // 2 in root + 2 in other
|
||||
|
||||
const expectedFiles = [
|
||||
path.resolve(clinerulesDirPath, "config.json"),
|
||||
path.resolve(clinerulesDirPath, "settings.js"),
|
||||
path.resolve(otherDirPath, "helper.js"),
|
||||
path.resolve(otherDirPath, "util.js"),
|
||||
]
|
||||
|
||||
filteredFiles.sort().should.deepEqual(expectedFiles.sort())
|
||||
|
||||
// Test with multiple exclusions
|
||||
const multiExcludeFiles = await readDirectory(clinerulesDirPath, [
|
||||
[".clinerules", "workflows"],
|
||||
[".clinerules", "other"],
|
||||
])
|
||||
|
||||
// Verify both workflows and other directories are excluded
|
||||
multiExcludeFiles.length.should.equal(2) // only the 2 files in root
|
||||
|
||||
const rootOnlyFiles = [path.resolve(clinerulesDirPath, "config.json"), path.resolve(clinerulesDirPath, "settings.js")]
|
||||
|
||||
multiExcludeFiles.sort().should.deepEqual(rootOnlyFiles.sort())
|
||||
})
|
||||
})
|
||||
|
||||
+19
-1
@@ -86,16 +86,34 @@ const OS_GENERATED_FILES = [
|
||||
* Recursively reads a directory and returns an array of absolute file paths.
|
||||
*
|
||||
* @param directoryPath - The path to the directory to read.
|
||||
* @param excludedPaths - Nested array of paths to ignore.
|
||||
* @returns A promise that resolves to an array of absolute file paths.
|
||||
* @throws Error if the directory cannot be read.
|
||||
*/
|
||||
export const readDirectory = async (directoryPath: string) => {
|
||||
export const readDirectory = async (directoryPath: string, excludedPaths: string[][] = []) => {
|
||||
try {
|
||||
const filePaths = await fs
|
||||
.readdir(directoryPath, { withFileTypes: true, recursive: true })
|
||||
.then((entries) => entries.filter((entry) => !OS_GENERATED_FILES.includes(entry.name)))
|
||||
.then((entries) => entries.filter((entry) => entry.isFile()))
|
||||
.then((files) => files.map((file) => path.resolve(file.parentPath, file.name)))
|
||||
.then((filePaths) =>
|
||||
filePaths.filter((filePath) => {
|
||||
if (excludedPaths.length === 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
for (const excludedPathList of excludedPaths) {
|
||||
const pathToSearchFor = path.sep + excludedPathList.join(path.sep) + path.sep
|
||||
if (filePath.includes(pathToSearchFor)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}),
|
||||
)
|
||||
|
||||
return filePaths
|
||||
} catch {
|
||||
throw new Error(`Error reading directory at ${directoryPath}`)
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
"@grpc/grpc-js": "^1.13.3",
|
||||
"@grpc/reflection": "^1.0.4",
|
||||
"grpc-health-check": "^2.0.2",
|
||||
"open": "^10.1.2",
|
||||
"vscode": "file:./vscode"
|
||||
"open": "^10.1.2"
|
||||
}
|
||||
}
|
||||
|
||||
+5
-2
@@ -1,7 +1,10 @@
|
||||
const tsConfigPaths = require("tsconfig-paths")
|
||||
const fs = require("fs")
|
||||
const path = require("path")
|
||||
|
||||
const tsConfig = JSON.parse(fs.readFileSync("./tsconfig.json", "utf-8"))
|
||||
const baseUrl = path.resolve(__dirname)
|
||||
|
||||
const tsConfig = JSON.parse(fs.readFileSync(path.join(baseUrl, "tsconfig.json"), "utf-8"))
|
||||
|
||||
/**
|
||||
* The aliases point towards the `src` directory.
|
||||
@@ -17,6 +20,6 @@ Object.keys(tsConfig.compilerOptions.paths).forEach((key) => {
|
||||
})
|
||||
|
||||
tsConfigPaths.register({
|
||||
baseUrl: ".",
|
||||
baseUrl: baseUrl,
|
||||
paths: outPaths,
|
||||
})
|
||||
|
||||
Generated
+4
-46
@@ -71,9 +71,11 @@
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-linux-arm64-gnu": "^4.40.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "^4.40.0",
|
||||
"@rollup/rollup-win32-x64-msvc": "^4.40.0",
|
||||
"@swc/core-linux-x64-gnu": "^1.11.0",
|
||||
"@tailwindcss/oxide-linux-x64-gnu": "^4.0.1",
|
||||
"lightningcss-linux-x64-gnu": "^1.29.1"
|
||||
"lightningcss-linux-x64-gnu": "^1.29.1",
|
||||
"lightningcss-win32-x64-msvc": "1.29.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@adobe/css-tools": {
|
||||
@@ -5731,48 +5733,6 @@
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.40.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.1.tgz",
|
||||
"integrity": "sha512-2BRORitq5rQ4Da9blVovzNCMaUlyKrzMSvkVR0D4qPuOy/+pMCrh1d7o01RATwVy+6Fa1WBw+da7QPeLWU/1mQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.40.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.1.tgz",
|
||||
"integrity": "sha512-b2bcNm9Kbde03H+q+Jjw9tSfhYkzrDUf2d5MAd1bOJuVplXvFhWz7tRtWvD8/ORZi7qSCy0idW6tf2HgxSXQSg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.40.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.1.tgz",
|
||||
"integrity": "sha512-DfcogW8N7Zg7llVEfpqWMZcaErKfsj9VvmfSyRjCyo4BI3wPEfrzTtJkZG6gKP/Z92wFm6rz2aDO7/JfiR/whA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.40.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.1.tgz",
|
||||
@@ -5780,7 +5740,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -12208,7 +12167,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -17616,4 +17574,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,8 +78,10 @@
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-linux-arm64-gnu": "^4.40.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "^4.40.0",
|
||||
"@rollup/rollup-win32-x64-msvc": "^4.40.0",
|
||||
"@swc/core-linux-x64-gnu": "^1.11.0",
|
||||
"@tailwindcss/oxide-linux-x64-gnu": "^4.0.1",
|
||||
"lightningcss-linux-x64-gnu": "^1.29.1"
|
||||
"lightningcss-linux-x64-gnu": "^1.29.1",
|
||||
"lightningcss-win32-x64-msvc": "1.29.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,8 +76,8 @@ export const ClineAccountView = () => {
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
// First notify extension to clear API keys and state
|
||||
vscode.postMessage({ type: "accountLogoutClicked" })
|
||||
// Use gRPC client to notify extension to clear API keys and state
|
||||
AccountServiceClient.accountLogoutClicked(EmptyRequest.create()).catch((err) => console.error("Failed to logout:", err))
|
||||
// Then sign out of Firebase
|
||||
handleSignOut()
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
|
||||
const minorVersion = version.split(".").slice(0, 2).join(".") // 2.0.0 -> 2.0
|
||||
return (
|
||||
<div style={containerStyle}>
|
||||
<VSCodeButton appearance="icon" onClick={hideAnnouncement} style={closeIconStyle}>
|
||||
<VSCodeButton data-testid="close-button" appearance="icon" onClick={hideAnnouncement} style={closeIconStyle}>
|
||||
<span className="codicon codicon-close"></span>
|
||||
</VSCodeButton>
|
||||
<h3 style={h3TitleStyle}>
|
||||
@@ -44,22 +44,21 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
|
||||
</h3>
|
||||
<ul style={ulStyle}>
|
||||
<li>
|
||||
<b>Task Timeline:</b> See the history of your coding journey with a visual timeline of checkpoints, letting
|
||||
you understand what Cline did at a glance.
|
||||
<b>Workflows:</b> Create and manage workflow files that can be injected into conversations via slash commands,
|
||||
making it easy to automate repetitive tasks.
|
||||
</li>
|
||||
<li>
|
||||
<b>UX Improvements:</b> Type while Cline works, smarter auto-scrolling, new copy buttons for task headers and
|
||||
messages, and a simplified home interface for a smoother experience.
|
||||
<b>Collapsible Task List:</b> Hide your recent tasks when sharing your screen to keep your prompts private.
|
||||
</li>
|
||||
<li>
|
||||
<b>Commit Message Generation:</b> Let Cline help craft meaningful commit messages based on your changes.
|
||||
<b>Global Endpoint for Vertex AI:</b> Improved availability and reduced rate limiting errors for Vertex AI
|
||||
users.
|
||||
</li>
|
||||
<li>
|
||||
<b>Quote Replies:</b> Easily reference previous messages with new quote reply support for clearer
|
||||
conversations.
|
||||
<b>New User Experience:</b> Special components and guidance for new users to help them get started with Cline.
|
||||
</li>
|
||||
<li>
|
||||
<b>Auto Caching for Gemini:</b> Native support for Gemini's recently released Implicit Caching.
|
||||
<b>UI Improvements:</b> Fixed loading states and improved settings organization for a smoother experience.
|
||||
</li>
|
||||
</ul>
|
||||
<Accordion isCompact className="pl-0">
|
||||
@@ -75,21 +74,20 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
|
||||
}}>
|
||||
<ul style={ulStyle}>
|
||||
<li>
|
||||
<b>Gemini prompt caching:</b> Gemini and Vertex providers now support prompt caching and price
|
||||
tracking for Gemini models.
|
||||
<b>Task Timeline:</b> See the history of your coding journey with a visual timeline of checkpoints.
|
||||
</li>
|
||||
<li>
|
||||
<b>Copy Buttons:</b> Buttons were added to Markdown and Code blocks that allow you to copy their
|
||||
contents easily.
|
||||
<b>UX Improvements:</b> Type while Cline works, smarter auto-scrolling, and copy buttons for task
|
||||
headers and messages.
|
||||
</li>
|
||||
<li>
|
||||
<b>Gemini prompt caching:</b> Gemini and Vertex providers now support prompt caching and price
|
||||
tracking.
|
||||
</li>
|
||||
<li>
|
||||
<b>Global Cline Rules:</b> Store multiple rules files in Documents/Cline/Rules to share between
|
||||
projects.
|
||||
</li>
|
||||
<li>
|
||||
<b>Slash Commands:</b> Type <code>/</code> in chat to see the list of quick actions, like starting a
|
||||
new task.
|
||||
</li>
|
||||
</ul>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
@@ -18,7 +18,7 @@ import { COMMAND_OUTPUT_STRING, COMMAND_REQ_APP_STRING } from "@shared/combineCo
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { findMatchingResourceOrTemplate, getMcpServerDisplayName } from "@/utils/mcp"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { FileServiceClient } from "@/services/grpc-client"
|
||||
import { FileServiceClient, TaskServiceClient } from "@/services/grpc-client"
|
||||
import { CheckmarkControl } from "@/components/common/CheckmarkControl"
|
||||
|
||||
interface CopyButtonProps {
|
||||
@@ -208,12 +208,12 @@ export const ChatRowContent = ({
|
||||
selectedText: "",
|
||||
})
|
||||
const contentRef = useRef<HTMLDivElement>(null)
|
||||
const [cost, apiReqCancelReason, apiReqStreamingFailedMessage] = useMemo(() => {
|
||||
const [cost, apiReqCancelReason, apiReqStreamingFailedMessage, retryStatus] = useMemo(() => {
|
||||
if (message.text != null && message.say === "api_req_started") {
|
||||
const info: ClineApiReqInfo = JSON.parse(message.text)
|
||||
return [info.cost, info.cancelReason, info.streamingFailedMessage]
|
||||
return [info.cost, info.cancelReason, info.streamingFailedMessage, info.retryStatus]
|
||||
}
|
||||
return [undefined, undefined, undefined]
|
||||
return [undefined, undefined, undefined, undefined]
|
||||
}, [message.text, message.say])
|
||||
|
||||
// when resuming task last won't be api_req_failed but a resume_task message so api_req_started will show loading spinner. that's why we just remove the last api_req_started that failed without streaming anything
|
||||
@@ -445,6 +445,17 @@ export const ChatRowContent = ({
|
||||
if (apiRequestFailedMessage) {
|
||||
return <span style={{ color: errorColor, fontWeight: "bold" }}>API Request Failed</span>
|
||||
}
|
||||
// New: Check for retryStatus to modify the title
|
||||
if (retryStatus && cost == null && !apiReqCancelReason) {
|
||||
const retryOperations = retryStatus.maxAttempts > 0 ? retryStatus.maxAttempts - 1 : 0
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
color: normalColor,
|
||||
fontWeight: "bold",
|
||||
}}>{`API Request (Retrying failed attempt ${retryStatus.attempt}/${retryOperations})...`}</span>
|
||||
)
|
||||
}
|
||||
|
||||
return <span style={{ color: normalColor, fontWeight: "bold" }}>API Request...</span>
|
||||
})(),
|
||||
@@ -1215,10 +1226,9 @@ export const ChatRowContent = ({
|
||||
disabled={seeNewChangesDisabled}
|
||||
onClick={() => {
|
||||
setSeeNewChangesDisabled(true)
|
||||
vscode.postMessage({
|
||||
type: "taskCompletionViewChanges",
|
||||
number: message.ts,
|
||||
})
|
||||
TaskServiceClient.taskCompletionViewChanges({
|
||||
value: message.ts,
|
||||
}).catch((err) => console.error("Failed to show task completion view changes:", err))
|
||||
}}
|
||||
style={{
|
||||
cursor: seeNewChangesDisabled ? "wait" : "pointer",
|
||||
@@ -1379,10 +1389,11 @@ export const ChatRowContent = ({
|
||||
disabled={seeNewChangesDisabled}
|
||||
onClick={() => {
|
||||
setSeeNewChangesDisabled(true)
|
||||
vscode.postMessage({
|
||||
type: "taskCompletionViewChanges",
|
||||
number: message.ts,
|
||||
})
|
||||
TaskServiceClient.taskCompletionViewChanges({
|
||||
value: message.ts,
|
||||
}).catch((err) =>
|
||||
console.error("Failed to show task completion view changes:", err),
|
||||
)
|
||||
}}>
|
||||
<i
|
||||
className="codicon codicon-new-file"
|
||||
|
||||
@@ -259,7 +259,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const { filePaths, chatSettings, apiConfiguration, openRouterModels, platform } = useExtensionState()
|
||||
const { filePaths, chatSettings, apiConfiguration, openRouterModels, platform, workflowToggles } = useExtensionState()
|
||||
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false)
|
||||
const [isDraggingOver, setIsDraggingOver] = useState(false)
|
||||
const [gitCommits, setGitCommits] = useState<GitCommit[]>([])
|
||||
@@ -373,6 +373,25 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
}
|
||||
}, [showContextMenu, setShowContextMenu])
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutsideSlashMenu = (event: MouseEvent) => {
|
||||
if (
|
||||
slashCommandsMenuContainerRef.current &&
|
||||
!slashCommandsMenuContainerRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setShowSlashCommandsMenu(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (showSlashCommandsMenu) {
|
||||
document.addEventListener("mousedown", handleClickOutsideSlashMenu)
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutsideSlashMenu)
|
||||
}
|
||||
}, [showSlashCommandsMenu])
|
||||
|
||||
const handleMentionSelect = useCallback(
|
||||
(type: ContextMenuOptionType, value?: string) => {
|
||||
if (type === ContextMenuOptionType.NoResults) {
|
||||
@@ -463,13 +482,18 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
event.preventDefault()
|
||||
setSelectedSlashCommandsIndex((prevIndex) => {
|
||||
const direction = event.key === "ArrowUp" ? -1 : 1
|
||||
const commands = getMatchingSlashCommands(slashCommandsQuery)
|
||||
// Get commands with workflow toggles
|
||||
const allCommands = getMatchingSlashCommands(slashCommandsQuery, workflowToggles)
|
||||
|
||||
if (commands.length === 0) {
|
||||
if (allCommands.length === 0) {
|
||||
return prevIndex
|
||||
}
|
||||
|
||||
const newIndex = (prevIndex + direction + commands.length) % commands.length
|
||||
// Calculate total command count
|
||||
const totalCommandCount = allCommands.length
|
||||
|
||||
// Create wraparound navigation - moves from last item to first and vice versa
|
||||
const newIndex = (prevIndex + direction + totalCommandCount) % totalCommandCount
|
||||
return newIndex
|
||||
})
|
||||
return
|
||||
@@ -477,7 +501,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
|
||||
if ((event.key === "Enter" || event.key === "Tab") && selectedSlashCommandsIndex !== -1) {
|
||||
event.preventDefault()
|
||||
const commands = getMatchingSlashCommands(slashCommandsQuery)
|
||||
const commands = getMatchingSlashCommands(slashCommandsQuery, workflowToggles)
|
||||
if (commands.length > 0) {
|
||||
handleSlashCommandsSelect(commands[selectedSlashCommandsIndex])
|
||||
}
|
||||
@@ -880,7 +904,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
|
||||
// extract and validate the exact command text
|
||||
const commandText = processedText.substring(slashIndex + 1, endIndex)
|
||||
const isValidCommand = validateSlashCommand(commandText)
|
||||
const isValidCommand = validateSlashCommand(commandText, workflowToggles)
|
||||
|
||||
if (isValidCommand) {
|
||||
const fullCommand = processedText.substring(slashIndex, endIndex) // includes slash
|
||||
@@ -893,7 +917,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
highlightLayerRef.current.innerHTML = processedText
|
||||
highlightLayerRef.current.scrollTop = textAreaRef.current.scrollTop
|
||||
highlightLayerRef.current.scrollLeft = textAreaRef.current.scrollLeft
|
||||
}, [])
|
||||
}, [workflowToggles])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
updateHighlights()
|
||||
@@ -1373,6 +1397,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
setSelectedIndex={setSelectedSlashCommandsIndex}
|
||||
onMouseDown={handleMenuMouseDown}
|
||||
query={slashCommandsQuery}
|
||||
workflowToggles={workflowToggles}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -18,7 +18,7 @@ import { combineCommandSequences } from "@shared/combineCommandSequences"
|
||||
import { getApiMetrics } from "@shared/getApiMetrics"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { TaskServiceClient, SlashServiceClient } from "@/services/grpc-client"
|
||||
import { TaskServiceClient, SlashServiceClient, FileServiceClient } from "@/services/grpc-client"
|
||||
import HistoryPreview from "@/components/history/HistoryPreview"
|
||||
import { normalizeApiConfiguration } from "@/components/settings/ApiOptions"
|
||||
import Announcement from "@/components/chat/Announcement"
|
||||
@@ -199,8 +199,14 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
}
|
||||
|
||||
if (textToCopy !== null) {
|
||||
vscode.postMessage({ type: "copyToClipboard", text: textToCopy })
|
||||
e.preventDefault()
|
||||
try {
|
||||
FileServiceClient.copyToClipboard({ value: textToCopy }).catch((err) => {
|
||||
console.error("Error copying to clipboard:", err)
|
||||
})
|
||||
e.preventDefault()
|
||||
} catch (error) {
|
||||
console.error("Error copying to clipboard:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -463,25 +469,22 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
case "resume_completed_task":
|
||||
case "mistake_limit_reached":
|
||||
case "new_task": // user can provide feedback or reject the new task suggestion
|
||||
vscode.postMessage({
|
||||
type: "askResponse",
|
||||
askResponse: "messageResponse",
|
||||
await TaskServiceClient.askResponse({
|
||||
responseType: "messageResponse",
|
||||
text: messageToSend,
|
||||
images,
|
||||
})
|
||||
break
|
||||
case "condense":
|
||||
vscode.postMessage({
|
||||
type: "askResponse",
|
||||
askResponse: "messageResponse",
|
||||
await TaskServiceClient.askResponse({
|
||||
responseType: "messageResponse",
|
||||
text: messageToSend,
|
||||
images,
|
||||
})
|
||||
break
|
||||
case "report_bug":
|
||||
vscode.postMessage({
|
||||
type: "askResponse",
|
||||
askResponse: "messageResponse",
|
||||
await TaskServiceClient.askResponse({
|
||||
responseType: "messageResponse",
|
||||
text: messageToSend,
|
||||
images,
|
||||
})
|
||||
@@ -525,16 +528,14 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
case "mistake_limit_reached":
|
||||
case "auto_approval_max_req_reached":
|
||||
if (trimmedInput || (images && images.length > 0)) {
|
||||
vscode.postMessage({
|
||||
type: "askResponse",
|
||||
askResponse: "yesButtonClicked",
|
||||
await TaskServiceClient.askResponse({
|
||||
responseType: "yesButtonClicked",
|
||||
text: trimmedInput,
|
||||
images: images,
|
||||
})
|
||||
} else {
|
||||
vscode.postMessage({
|
||||
type: "askResponse",
|
||||
askResponse: "yesButtonClicked",
|
||||
await TaskServiceClient.askResponse({
|
||||
responseType: "yesButtonClicked",
|
||||
})
|
||||
}
|
||||
// Clear input state after sending
|
||||
@@ -591,17 +592,15 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
case "browser_action_launch":
|
||||
case "use_mcp_server":
|
||||
if (trimmedInput || (images && images.length > 0)) {
|
||||
vscode.postMessage({
|
||||
type: "askResponse",
|
||||
askResponse: "noButtonClicked",
|
||||
await TaskServiceClient.askResponse({
|
||||
responseType: "noButtonClicked",
|
||||
text: trimmedInput,
|
||||
images: images,
|
||||
})
|
||||
} else {
|
||||
// responds to the API with a "This operation failed" and lets it try again
|
||||
vscode.postMessage({
|
||||
type: "askResponse",
|
||||
askResponse: "noButtonClicked",
|
||||
await TaskServiceClient.askResponse({
|
||||
responseType: "noButtonClicked",
|
||||
})
|
||||
}
|
||||
// Clear input state after sending
|
||||
@@ -632,8 +631,15 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
return normalizeApiConfiguration(apiConfiguration)
|
||||
}, [apiConfiguration])
|
||||
|
||||
const selectImages = useCallback(() => {
|
||||
vscode.postMessage({ type: "selectImages" })
|
||||
const selectImages = useCallback(async () => {
|
||||
try {
|
||||
const response = await FileServiceClient.selectImages({})
|
||||
if (response && response.values && response.values.length > 0) {
|
||||
setSelectedImages((prevImages) => [...prevImages, ...response.values].slice(0, MAX_IMAGES_PER_MESSAGE))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error selecting images:", error)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const shouldDisableImages = !selectedModelInfo.supportsImages || selectedImages.length >= MAX_IMAGES_PER_MESSAGE
|
||||
@@ -976,6 +982,14 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
)
|
||||
}
|
||||
|
||||
// We display certain statuses for the last message only
|
||||
// If the last message is a checkpoint, we want to show the status of the previous message
|
||||
const nextMessage = index < groupedMessages.length - 1 && groupedMessages[index + 1]
|
||||
const isNextCheckpoint = !Array.isArray(nextMessage) && nextMessage && nextMessage?.say === "checkpoint_created"
|
||||
const isLastMessageGroup = isNextCheckpoint && index === groupedMessages.length - 2
|
||||
|
||||
const isLast = index === groupedMessages.length - 1 || isLastMessageGroup
|
||||
|
||||
// regular message
|
||||
return (
|
||||
<ChatRow
|
||||
@@ -984,7 +998,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
isExpanded={expandedRows[messageOrGroup.ts] || false}
|
||||
onToggleExpand={() => toggleRowExpansion(messageOrGroup.ts)}
|
||||
lastModifiedMessage={modifiedMessages.at(-1)}
|
||||
isLast={index === groupedMessages.length - 1}
|
||||
isLast={isLast}
|
||||
onHeightChange={handleRowHeightChange}
|
||||
inputValue={inputValue}
|
||||
sendMessageFromChatRow={handleSendMessage}
|
||||
|
||||
@@ -7,9 +7,17 @@ interface SlashCommandMenuProps {
|
||||
setSelectedIndex: (index: number) => void
|
||||
onMouseDown: () => void
|
||||
query: string
|
||||
workflowToggles?: Record<string, boolean>
|
||||
}
|
||||
|
||||
const SlashCommandMenu: React.FC<SlashCommandMenuProps> = ({ onSelect, selectedIndex, setSelectedIndex, onMouseDown, query }) => {
|
||||
const SlashCommandMenu: React.FC<SlashCommandMenuProps> = ({
|
||||
onSelect,
|
||||
selectedIndex,
|
||||
setSelectedIndex,
|
||||
onMouseDown,
|
||||
query,
|
||||
workflowToggles = {},
|
||||
}) => {
|
||||
const menuRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const handleClick = useCallback(
|
||||
@@ -19,10 +27,9 @@ const SlashCommandMenu: React.FC<SlashCommandMenuProps> = ({ onSelect, selectedI
|
||||
[onSelect],
|
||||
)
|
||||
|
||||
// Auto-scroll logic remains the same...
|
||||
useEffect(() => {
|
||||
if (menuRef.current) {
|
||||
const selectedElement = menuRef.current.children[selectedIndex] as HTMLElement
|
||||
const selectedElement = menuRef.current.querySelector(`#slash-command-menu-item-${selectedIndex}`) as HTMLElement
|
||||
if (selectedElement) {
|
||||
const menuRect = menuRef.current.getBoundingClientRect()
|
||||
const selectedRect = selectedElement.getBoundingClientRect()
|
||||
@@ -37,7 +44,46 @@ const SlashCommandMenu: React.FC<SlashCommandMenuProps> = ({ onSelect, selectedI
|
||||
}, [selectedIndex])
|
||||
|
||||
// Filter commands based on query
|
||||
const filteredCommands = getMatchingSlashCommands(query)
|
||||
const filteredCommands = getMatchingSlashCommands(query, workflowToggles)
|
||||
const defaultCommands = filteredCommands.filter((cmd) => cmd.section === "default" || !cmd.section)
|
||||
const workflowCommands = filteredCommands.filter((cmd) => cmd.section === "custom")
|
||||
|
||||
// Create a reusable function for rendering a command section
|
||||
const renderCommandSection = (commands: SlashCommand[], title: string, indexOffset: number, showDescriptions: boolean) => {
|
||||
if (commands.length === 0) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="text-xs text-[var(--vscode-descriptionForeground)] px-3 py-1 font-bold border-b border-[var(--vscode-editorGroup-border)]">
|
||||
{title}
|
||||
</div>
|
||||
{commands.map((command, index) => {
|
||||
const itemIndex = index + indexOffset
|
||||
return (
|
||||
<div
|
||||
key={command.name}
|
||||
id={`slash-command-menu-item-${itemIndex}`}
|
||||
className={`slash-command-menu-item py-2 px-3 cursor-pointer flex flex-col border-b border-[var(--vscode-editorGroup-border)] ${
|
||||
itemIndex === selectedIndex
|
||||
? "bg-[var(--vscode-quickInputList-focusBackground)] text-[var(--vscode-quickInputList-focusForeground)]"
|
||||
: ""
|
||||
} hover:bg-[var(--vscode-list-hoverBackground)]`}
|
||||
onClick={() => handleClick(command)}
|
||||
onMouseEnter={() => setSelectedIndex(itemIndex)}>
|
||||
<div className="font-bold whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
<span className="ph-no-capture">/{command.name}</span>
|
||||
</div>
|
||||
{showDescriptions && command.description && (
|
||||
<div className="text-[0.85em] text-[var(--vscode-descriptionForeground)] whitespace-normal overflow-hidden text-ellipsis">
|
||||
<span className="ph-no-capture">{command.description}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -45,33 +91,15 @@ const SlashCommandMenu: React.FC<SlashCommandMenuProps> = ({ onSelect, selectedI
|
||||
onMouseDown={onMouseDown}>
|
||||
<div
|
||||
ref={menuRef}
|
||||
className="bg-[var(--vscode-dropdown-background)] border border-[var(--vscode-editorGroup-border)] rounded-[3px] shadow-[0_4px_10px_rgba(0,0,0,0.25)] flex flex-col max-h-[200px] overflow-y-auto" // Corrected rounded and shadow
|
||||
>
|
||||
className="bg-[var(--vscode-dropdown-background)] border border-[var(--vscode-editorGroup-border)] rounded-[3px] shadow-[0_4px_10px_rgba(0,0,0,0.25)] flex flex-col overflow-y-auto"
|
||||
style={{ maxHeight: "min(200px, calc(50vh))", overscrollBehavior: "contain" }}>
|
||||
{filteredCommands.length > 0 ? (
|
||||
filteredCommands.map((command, index) => (
|
||||
<div
|
||||
key={command.name}
|
||||
id={`slash-command-menu-item-${index}`}
|
||||
className={`slash-command-menu-item py-2 px-3 cursor-pointer flex flex-col border-b border-[var(--vscode-editorGroup-border)] ${
|
||||
// Corrected padding
|
||||
index === selectedIndex
|
||||
? "bg-[var(--vscode-quickInputList-focusBackground)] text-[var(--vscode-quickInputList-focusForeground)]"
|
||||
: "" // Removed bg-transparent
|
||||
} hover:bg-[var(--vscode-list-hoverBackground)]`}
|
||||
onClick={() => handleClick(command)}
|
||||
onMouseEnter={() => setSelectedIndex(index)}>
|
||||
<div className="font-bold whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
<span className="ph-no-capture">/{command.name}</span>
|
||||
</div>
|
||||
<div className="text-[0.85em] text-[var(--vscode-descriptionForeground)] whitespace-normal overflow-hidden text-ellipsis">
|
||||
<span className="ph-no-capture">{command.description}</span>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
<>
|
||||
{renderCommandSection(defaultCommands, "Default Commands", 0, true)}
|
||||
{renderCommandSection(workflowCommands, "Workflow Commands", defaultCommands.length, false)}
|
||||
</>
|
||||
) : (
|
||||
<div className="py-2 px-3 cursor-default flex flex-col">
|
||||
{" "}
|
||||
{/* Corrected padding, removed border, changed cursor */}
|
||||
<div className="text-[0.85em] text-[var(--vscode-descriptionForeground)]">No matching commands found</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useState, useEffect } from "react"
|
||||
import styled from "styled-components"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { TaskFeedbackType } from "@shared/WebviewMessage"
|
||||
import { TaskServiceClient } from "@/services/grpc-client"
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import { TaskFeedbackType } from "@shared/WebviewMessage"
|
||||
|
||||
interface TaskFeedbackButtonsProps {
|
||||
messageTs: number
|
||||
@@ -41,25 +41,27 @@ const TaskFeedbackButtons: React.FC<TaskFeedbackButtonsProps> = ({ messageTs, is
|
||||
return null
|
||||
}
|
||||
|
||||
const handleFeedback = (type: TaskFeedbackType) => {
|
||||
const handleFeedback = async (type: TaskFeedbackType) => {
|
||||
if (feedback !== null) return // Already provided feedback
|
||||
|
||||
setFeedback(type)
|
||||
|
||||
// Send feedback to extension
|
||||
vscode.postMessage({
|
||||
type: "taskFeedback",
|
||||
feedbackType: type,
|
||||
})
|
||||
|
||||
// Store in localStorage that feedback was provided for this message
|
||||
try {
|
||||
const feedbackHistory = localStorage.getItem("taskFeedbackHistory") || "{}"
|
||||
const history = JSON.parse(feedbackHistory)
|
||||
history[messageTs] = true
|
||||
localStorage.setItem("taskFeedbackHistory", JSON.stringify(history))
|
||||
} catch (e) {
|
||||
console.error("Error updating feedback history:", e)
|
||||
await TaskServiceClient.taskFeedback({
|
||||
value: type,
|
||||
})
|
||||
|
||||
// Store in localStorage that feedback was provided for this message
|
||||
try {
|
||||
const feedbackHistory = localStorage.getItem("taskFeedbackHistory") || "{}"
|
||||
const history = JSON.parse(feedbackHistory)
|
||||
history[messageTs] = true
|
||||
localStorage.setItem("taskFeedbackHistory", JSON.stringify(history))
|
||||
} catch (e) {
|
||||
console.error("Error updating feedback history:", e)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error sending task feedback:", error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,13 +20,7 @@ describe("Announcement", () => {
|
||||
|
||||
it("calls hideAnnouncement when close button is clicked", () => {
|
||||
render(<Announcement version="2.0.0" hideAnnouncement={hideAnnouncement} />)
|
||||
fireEvent.click(screen.getByRole("button"))
|
||||
fireEvent.click(screen.getByTestId("close-button"))
|
||||
expect(hideAnnouncement).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("renders the enhanced MCP support announcement", () => {
|
||||
render(<Announcement version="2.0.0" hideAnnouncement={hideAnnouncement} />)
|
||||
// Updated text based on actual component output
|
||||
expect(screen.getByText(/Enhanced MCP Support:/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useCallback, useRef, useState, useMemo } from "react"
|
||||
import { useRef, useState, useMemo } from "react"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { useAutoApproveActions } from "@/hooks/useAutoApproveActions"
|
||||
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
|
||||
import { getAsVar, VSC_TITLEBAR_INACTIVE_FOREGROUND } from "@/utils/vscStyles"
|
||||
import AutoApproveMenuItem from "./AutoApproveMenuItem"
|
||||
import AutoApproveModal from "./AutoApproveModal"
|
||||
import { ACTION_METADATA, NOTIFICATIONS_SETTING } from "./constants"
|
||||
import { ActionMetadata } from "./types"
|
||||
|
||||
interface AutoApproveBarProps {
|
||||
style?: React.CSSProperties
|
||||
@@ -13,10 +13,11 @@ interface AutoApproveBarProps {
|
||||
|
||||
const AutoApproveBar = ({ style }: AutoApproveBarProps) => {
|
||||
const { autoApprovalSettings } = useExtensionState()
|
||||
const { isChecked, isFavorited, updateAction } = useAutoApproveActions()
|
||||
|
||||
const [isModalVisible, setIsModalVisible] = useState(false)
|
||||
const buttonRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Favorites are derived from autoApprovalSettings
|
||||
const favorites = useMemo(() => autoApprovalSettings.favorites || [], [autoApprovalSettings.favorites])
|
||||
|
||||
// Render a favorited item with a checkbox
|
||||
@@ -32,6 +33,7 @@ const AutoApproveBar = ({ style }: AutoApproveBarProps) => {
|
||||
isFavorited={isFavorited}
|
||||
onToggle={updateAction}
|
||||
condensed={true}
|
||||
showIcon={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -67,25 +69,6 @@ const AutoApproveBar = ({ style }: AutoApproveBarProps) => {
|
||||
]
|
||||
}
|
||||
|
||||
const isChecked = (action: ActionMetadata): boolean => {
|
||||
if (action.id === "enableNotifications") {
|
||||
return autoApprovalSettings.enableNotifications
|
||||
}
|
||||
if (action.id === "enableAll") {
|
||||
return Object.values(autoApprovalSettings.actions).every(Boolean)
|
||||
}
|
||||
return autoApprovalSettings.actions[action.id] ?? false
|
||||
}
|
||||
|
||||
const isFavorited = (action: ActionMetadata): boolean => {
|
||||
return favorites.includes(action.id)
|
||||
}
|
||||
|
||||
const updateAction = useCallback(() => {
|
||||
// This is just a placeholder since we need to pass it to AutoApproveMenuItem
|
||||
// The actual implementation is in the modal component
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
className="px-[10px] mx-[5px] select-none rounded-[10px_10px_0_0]"
|
||||
|
||||
@@ -11,6 +11,7 @@ interface AutoApproveMenuItemProps {
|
||||
onToggle: (action: ActionMetadata, checked: boolean) => void
|
||||
onToggleFavorite?: (actionId: string) => void
|
||||
condensed?: boolean
|
||||
showIcon?: boolean
|
||||
}
|
||||
|
||||
const CheckboxContainer = styled.div<{
|
||||
@@ -77,6 +78,7 @@ const AutoApproveMenuItem = ({
|
||||
onToggle,
|
||||
onToggleFavorite,
|
||||
condensed = false,
|
||||
showIcon = true,
|
||||
}: AutoApproveMenuItemProps) => {
|
||||
const checked = isChecked(action)
|
||||
const favorited = isFavorited?.(action)
|
||||
@@ -93,7 +95,7 @@ const AutoApproveMenuItem = ({
|
||||
<CheckboxContainer isFavorited={favorited} onClick={onChange}>
|
||||
<div className="left-content">
|
||||
<VSCodeCheckbox checked={checked} />
|
||||
<span className={`codicon ${action.icon} icon`}></span>
|
||||
{showIcon && <span className={`codicon ${action.icon} icon`}></span>}
|
||||
<span className="label">{condensed ? action.shortName : action.label}</span>
|
||||
</div>
|
||||
{onToggleFavorite && !condensed && (
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import React, { useRef, useState, useEffect, useMemo, useCallback } from "react"
|
||||
import React, { useRef, useState, useEffect } from "react"
|
||||
import { useClickAway, useWindowSize } from "react-use"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { useAutoApproveActions } from "@/hooks/useAutoApproveActions"
|
||||
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { VSCodeTextField, VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import { getAsVar, VSC_FOREGROUND, VSC_TITLEBAR_INACTIVE_FOREGROUND } from "@/utils/vscStyles"
|
||||
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
|
||||
import { getAsVar, VSC_TITLEBAR_INACTIVE_FOREGROUND } from "@/utils/vscStyles"
|
||||
import HeroTooltip from "@/components/common/HeroTooltip"
|
||||
import AutoApproveMenuItem from "./AutoApproveMenuItem"
|
||||
import { ActionMetadata } from "./types"
|
||||
@@ -28,6 +27,8 @@ const AutoApproveModal: React.FC<AutoApproveModalProps> = ({
|
||||
NOTIFICATIONS_SETTING,
|
||||
}) => {
|
||||
const { autoApprovalSettings } = useExtensionState()
|
||||
const { isChecked, isFavorited, toggleFavorite, updateAction, updateMaxRequests } = useAutoApproveActions()
|
||||
|
||||
const modalRef = useRef<HTMLDivElement>(null)
|
||||
const itemsContainerRef = useRef<HTMLDivElement>(null)
|
||||
const { width: viewportWidth, height: viewportHeight } = useWindowSize()
|
||||
@@ -35,9 +36,6 @@ const AutoApproveModal: React.FC<AutoApproveModalProps> = ({
|
||||
const [menuPosition, setMenuPosition] = useState(0)
|
||||
const [containerWidth, setContainerWidth] = useState(0)
|
||||
|
||||
// Favorites are derived from autoApprovalSettings
|
||||
const favorites = useMemo(() => autoApprovalSettings.favorites || [], [autoApprovalSettings.favorites])
|
||||
|
||||
useClickAway(modalRef, (e) => {
|
||||
// Skip if click was on the button that toggles the modal
|
||||
if (buttonRef.current && buttonRef.current.contains(e.target as Node)) {
|
||||
@@ -83,141 +81,6 @@ const AutoApproveModal: React.FC<AutoApproveModalProps> = ({
|
||||
}
|
||||
}, [isVisible])
|
||||
|
||||
const toggleFavorite = useCallback(
|
||||
(actionId: string) => {
|
||||
const currentFavorites = autoApprovalSettings.favorites || []
|
||||
let newFavorites: string[]
|
||||
|
||||
if (currentFavorites.includes(actionId)) {
|
||||
newFavorites = currentFavorites.filter((id) => id !== actionId)
|
||||
} else {
|
||||
newFavorites = [...currentFavorites, actionId]
|
||||
}
|
||||
|
||||
vscode.postMessage({
|
||||
type: "autoApprovalSettings",
|
||||
autoApprovalSettings: {
|
||||
...autoApprovalSettings,
|
||||
version: (autoApprovalSettings.version ?? 1) + 1,
|
||||
favorites: newFavorites,
|
||||
},
|
||||
})
|
||||
},
|
||||
[autoApprovalSettings],
|
||||
)
|
||||
|
||||
const updateAction = useCallback(
|
||||
(action: ActionMetadata, value: boolean) => {
|
||||
const actionId = action.id
|
||||
const subActionId = action.subAction?.id
|
||||
|
||||
if (actionId === "enableAll" || subActionId === "enableAll") {
|
||||
toggleAll(action, value)
|
||||
return
|
||||
}
|
||||
|
||||
if (actionId === "enableNotifications" || subActionId === "enableNotifications") {
|
||||
updateNotifications(action, value)
|
||||
return
|
||||
}
|
||||
|
||||
let newActions = {
|
||||
...autoApprovalSettings.actions,
|
||||
[actionId]: value,
|
||||
}
|
||||
|
||||
if (value === false && subActionId) {
|
||||
newActions[subActionId] = false
|
||||
}
|
||||
|
||||
if (value === true && action.parentActionId) {
|
||||
newActions[action.parentActionId as keyof AutoApprovalSettings["actions"]] = true
|
||||
}
|
||||
|
||||
// Check if this will result in any enabled actions
|
||||
const willHaveEnabledActions = Object.values(newActions).some(Boolean)
|
||||
|
||||
vscode.postMessage({
|
||||
type: "autoApprovalSettings",
|
||||
autoApprovalSettings: {
|
||||
...autoApprovalSettings,
|
||||
version: (autoApprovalSettings.version ?? 1) + 1,
|
||||
actions: newActions,
|
||||
enabled: willHaveEnabledActions,
|
||||
},
|
||||
})
|
||||
},
|
||||
[autoApprovalSettings],
|
||||
)
|
||||
|
||||
const updateMaxRequests = useCallback(
|
||||
(maxRequests: number) => {
|
||||
const currentSettings = autoApprovalSettings
|
||||
vscode.postMessage({
|
||||
type: "autoApprovalSettings",
|
||||
autoApprovalSettings: {
|
||||
...currentSettings,
|
||||
version: (currentSettings.version ?? 1) + 1,
|
||||
maxRequests,
|
||||
},
|
||||
})
|
||||
},
|
||||
[autoApprovalSettings],
|
||||
)
|
||||
|
||||
const updateNotifications = useCallback(
|
||||
(action: ActionMetadata, checked: boolean) => {
|
||||
if (action.id === "enableNotifications") {
|
||||
const currentSettings = autoApprovalSettings
|
||||
vscode.postMessage({
|
||||
type: "autoApprovalSettings",
|
||||
autoApprovalSettings: {
|
||||
...currentSettings,
|
||||
version: (currentSettings.version ?? 1) + 1,
|
||||
enableNotifications: checked,
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
[autoApprovalSettings],
|
||||
)
|
||||
|
||||
const toggleAll = useCallback(
|
||||
(action: ActionMetadata, checked: boolean) => {
|
||||
let actions = { ...autoApprovalSettings.actions }
|
||||
|
||||
for (const action of Object.keys(actions)) {
|
||||
actions[action as keyof AutoApprovalSettings["actions"]] = checked
|
||||
}
|
||||
|
||||
vscode.postMessage({
|
||||
type: "autoApprovalSettings",
|
||||
autoApprovalSettings: {
|
||||
...autoApprovalSettings,
|
||||
version: (autoApprovalSettings.version ?? 1) + 1,
|
||||
actions,
|
||||
},
|
||||
})
|
||||
},
|
||||
[autoApprovalSettings],
|
||||
)
|
||||
|
||||
// Check if action is enabled
|
||||
const isChecked = (action: ActionMetadata): boolean => {
|
||||
if (action.id === "enableNotifications") {
|
||||
return autoApprovalSettings.enableNotifications
|
||||
}
|
||||
if (action.id === "enableAll") {
|
||||
return Object.values(autoApprovalSettings.actions).every(Boolean)
|
||||
}
|
||||
return autoApprovalSettings.actions[action.id] ?? false
|
||||
}
|
||||
|
||||
// Check if action is favorited
|
||||
const isFavorited = (action: ActionMetadata): boolean => {
|
||||
return favorites.includes(action.id)
|
||||
}
|
||||
|
||||
if (!isVisible) return null
|
||||
|
||||
return (
|
||||
@@ -240,19 +103,19 @@ const AutoApproveModal: React.FC<AutoApproveModalProps> = ({
|
||||
/>
|
||||
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<div className="m-0 text-base font-semibold">Auto-approve Settings</div>
|
||||
<HeroTooltip
|
||||
content="Auto-approve allows Cline to perform the following actions without asking for permission. Please use with caution and only enable if you understand the risks."
|
||||
placement="top">
|
||||
<div className="text-base font-semibold mb-1">Auto-approve Settings</div>
|
||||
</HeroTooltip>
|
||||
<VSCodeButton appearance="icon" onClick={() => setIsVisible(false)}>
|
||||
<span className="codicon codicon-close text-[10px]"></span>
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
|
||||
<HeroTooltip
|
||||
content="Auto-approve allows Cline to perform the following actions without asking for permission. Please use with caution and only enable if you understand the risks."
|
||||
placement="top">
|
||||
<div className="mb-3">
|
||||
<span className="text-[color:var(--vscode-foreground)] font-medium">Actions:</span>
|
||||
</div>
|
||||
</HeroTooltip>
|
||||
<div className="mb-2.5">
|
||||
<span className="text-[color:var(--vscode-foreground)] font-medium">Actions:</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={itemsContainerRef}
|
||||
@@ -285,7 +148,7 @@ const AutoApproveModal: React.FC<AutoApproveModalProps> = ({
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<div className="mb-2.5">
|
||||
<span className="text-[color:var(--vscode-foreground)] font-medium">Quick Settings:</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import { ActionMetadata } from "./types"
|
||||
|
||||
export const ACTION_METADATA: ActionMetadata[] = [
|
||||
{
|
||||
id: "enableAutoApprove",
|
||||
label: "Enable auto-approve",
|
||||
shortName: "Enabled",
|
||||
description: "Toggle the auto-approve feature on or off.",
|
||||
icon: "codicon-play-circle",
|
||||
},
|
||||
{
|
||||
id: "enableAll",
|
||||
label: "Enable all",
|
||||
label: "Toggle all",
|
||||
shortName: "All",
|
||||
description: "Enable all actions.",
|
||||
description: "Toggle all actions on or off.",
|
||||
icon: "codicon-checklist",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
|
||||
|
||||
export interface ActionMetadata {
|
||||
id: keyof AutoApprovalSettings["actions"] | "enableNotifications" | "enableAll"
|
||||
id: keyof AutoApprovalSettings["actions"] | "enableNotifications" | "enableAll" | "enableAutoApprove"
|
||||
label: string
|
||||
shortName: string
|
||||
description: string
|
||||
|
||||
@@ -6,6 +6,7 @@ import { vscode } from "@/utils/vscode"
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import RulesToggleList from "./RulesToggleList"
|
||||
import Tooltip from "@/components/common/Tooltip"
|
||||
import styled from "styled-components"
|
||||
|
||||
const ClineRulesToggleModal: React.FC = () => {
|
||||
const {
|
||||
@@ -13,6 +14,7 @@ const ClineRulesToggleModal: React.FC = () => {
|
||||
localClineRulesToggles = {},
|
||||
localCursorRulesToggles = {},
|
||||
localWindsurfRulesToggles = {},
|
||||
workflowToggles = {},
|
||||
} = useExtensionState()
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
const buttonRef = useRef<HTMLDivElement>(null)
|
||||
@@ -20,6 +22,7 @@ const ClineRulesToggleModal: React.FC = () => {
|
||||
const { width: viewportWidth, height: viewportHeight } = useWindowSize()
|
||||
const [arrowPosition, setArrowPosition] = useState(0)
|
||||
const [menuPosition, setMenuPosition] = useState(0)
|
||||
const [currentView, setCurrentView] = useState<"rules" | "workflows">("rules")
|
||||
|
||||
useEffect(() => {
|
||||
if (isVisible) {
|
||||
@@ -45,6 +48,10 @@ const ClineRulesToggleModal: React.FC = () => {
|
||||
.map(([path, enabled]): [string, boolean] => [path, enabled as boolean])
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
|
||||
const workflows = Object.entries(workflowToggles || {})
|
||||
.map(([path, enabled]): [string, boolean] => [path, enabled as boolean])
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
|
||||
// Handle toggle rule
|
||||
const toggleRule = (isGlobal: boolean, rulePath: string, enabled: boolean) => {
|
||||
vscode.postMessage({
|
||||
@@ -71,6 +78,14 @@ const ClineRulesToggleModal: React.FC = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const toggleWorkflow = (workflowPath: string, enabled: boolean) => {
|
||||
vscode.postMessage({
|
||||
type: "toggleWorkflow",
|
||||
workflowPath,
|
||||
enabled,
|
||||
})
|
||||
}
|
||||
|
||||
// Close modal when clicking outside
|
||||
useClickAway(modalRef, () => {
|
||||
setIsVisible(false)
|
||||
@@ -91,7 +106,7 @@ const ClineRulesToggleModal: React.FC = () => {
|
||||
return (
|
||||
<div ref={modalRef}>
|
||||
<div ref={buttonRef} className="inline-flex min-w-0 max-w-full">
|
||||
<Tooltip tipText="Manage Cline Rules" visible={isVisible ? false : undefined}>
|
||||
<Tooltip tipText="Manage Cline Rules & Workflows" visible={isVisible ? false : undefined}>
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
aria-label="Cline Rules"
|
||||
@@ -125,68 +140,146 @@ const ClineRulesToggleModal: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex justify-between items-center mb-2.5">
|
||||
<div className="m-0 text-base font-semibold">Cline Rules</div>
|
||||
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
onClick={() => {
|
||||
vscode.postMessage({
|
||||
type: "openExtensionSettings",
|
||||
})
|
||||
setIsVisible(false)
|
||||
}}></VSCodeButton>
|
||||
{/* Tabs container */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: "10px",
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "1px",
|
||||
borderBottom: "1px solid var(--vscode-panel-border)",
|
||||
}}>
|
||||
<TabButton isActive={currentView === "rules"} onClick={() => setCurrentView("rules")}>
|
||||
Rules
|
||||
</TabButton>
|
||||
<TabButton isActive={currentView === "workflows"} onClick={() => setCurrentView("workflows")}>
|
||||
Workflows
|
||||
</TabButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Global Rules Section */}
|
||||
<div className="mb-3">
|
||||
<div className="text-sm font-normal mb-2">Global Rules</div>
|
||||
<RulesToggleList
|
||||
rules={globalRules}
|
||||
toggleRule={(rulePath, enabled) => toggleRule(true, rulePath, enabled)}
|
||||
listGap="small"
|
||||
isGlobal={true}
|
||||
ruleType={"cline"}
|
||||
showNewRule={true}
|
||||
showNoRules={true}
|
||||
/>
|
||||
{/* Description text */}
|
||||
<div className="text-xs text-[var(--vscode-descriptionForeground)] mb-4">
|
||||
{currentView === "rules" ? (
|
||||
<p>
|
||||
Rules allow you to provide Cline with system-level guidance. Think of them as a persistent way to
|
||||
include context and preferences for your projects or globally for every conversation.
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
Workflows allow you to define a series of steps to guide Cline through a repetitive set of tasks,
|
||||
such as deploying a service or submitting a PR. To invoke a workflow, type{" "}
|
||||
<span
|
||||
className="
|
||||
text-[var(--vscode-foreground)] font-bold">
|
||||
/workflow-name
|
||||
</span>{" "}
|
||||
in the chat.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Local Rules Section */}
|
||||
<div style={{ marginBottom: -10 }}>
|
||||
<div className="text-sm font-normal mb-2">Workspace Rules</div>
|
||||
<RulesToggleList
|
||||
rules={localRules}
|
||||
toggleRule={(rulePath, enabled) => toggleRule(false, rulePath, enabled)}
|
||||
listGap="small"
|
||||
isGlobal={false}
|
||||
ruleType={"cline"}
|
||||
showNewRule={false}
|
||||
showNoRules={false}
|
||||
/>
|
||||
<RulesToggleList
|
||||
rules={cursorRules}
|
||||
toggleRule={toggleCursorRule}
|
||||
listGap="small"
|
||||
isGlobal={false}
|
||||
ruleType={"cursor"}
|
||||
showNewRule={false}
|
||||
showNoRules={false}
|
||||
/>
|
||||
<RulesToggleList
|
||||
rules={windsurfRules}
|
||||
toggleRule={toggleWindsurfRule}
|
||||
listGap="small"
|
||||
isGlobal={false}
|
||||
ruleType={"windsurf"}
|
||||
showNewRule={true}
|
||||
showNoRules={localRules.length === 0 && cursorRules.length === 0 && windsurfRules.length === 0}
|
||||
/>
|
||||
</div>
|
||||
{currentView === "rules" ? (
|
||||
<>
|
||||
{/* Global Rules Section */}
|
||||
<div className="mb-3">
|
||||
<div className="text-sm font-normal mb-2">Global Rules</div>
|
||||
<RulesToggleList
|
||||
rules={globalRules}
|
||||
toggleRule={(rulePath, enabled) => toggleRule(true, rulePath, enabled)}
|
||||
listGap="small"
|
||||
isGlobal={true}
|
||||
ruleType={"cline"}
|
||||
showNewRule={true}
|
||||
showNoRules={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Local Rules Section */}
|
||||
<div style={{ marginBottom: -10 }}>
|
||||
<div className="text-sm font-normal mb-2">Workspace Rules</div>
|
||||
<RulesToggleList
|
||||
rules={localRules}
|
||||
toggleRule={(rulePath, enabled) => toggleRule(false, rulePath, enabled)}
|
||||
listGap="small"
|
||||
isGlobal={false}
|
||||
ruleType={"cline"}
|
||||
showNewRule={false}
|
||||
showNoRules={false}
|
||||
/>
|
||||
<RulesToggleList
|
||||
rules={cursorRules}
|
||||
toggleRule={toggleCursorRule}
|
||||
listGap="small"
|
||||
isGlobal={false}
|
||||
ruleType={"cursor"}
|
||||
showNewRule={false}
|
||||
showNoRules={false}
|
||||
/>
|
||||
<RulesToggleList
|
||||
rules={windsurfRules}
|
||||
toggleRule={toggleWindsurfRule}
|
||||
listGap="small"
|
||||
isGlobal={false}
|
||||
ruleType={"windsurf"}
|
||||
showNewRule={true}
|
||||
showNoRules={false}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
/* Workflows section */
|
||||
<div style={{ marginBottom: -10 }}>
|
||||
<div className="text-sm font-normal mb-2">Workspace Workflows</div>
|
||||
<RulesToggleList
|
||||
rules={workflows}
|
||||
toggleRule={toggleWorkflow}
|
||||
listGap="small"
|
||||
isGlobal={false}
|
||||
ruleType={"workflow"}
|
||||
showNewRule={true}
|
||||
showNoRules={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledTabButton = styled.button<{ isActive: boolean }>`
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 2px solid ${(props) => (props.isActive ? "var(--vscode-foreground)" : "transparent")};
|
||||
color: ${(props) => (props.isActive ? "var(--vscode-foreground)" : "var(--vscode-descriptionForeground)")};
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
margin-bottom: -1px;
|
||||
font-family: inherit;
|
||||
|
||||
&:hover {
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
`
|
||||
|
||||
export const TabButton = ({
|
||||
children,
|
||||
isActive,
|
||||
onClick,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
isActive: boolean
|
||||
onClick: () => void
|
||||
}) => (
|
||||
<StyledTabButton isActive={isActive} onClick={onClick}>
|
||||
{children}
|
||||
</StyledTabButton>
|
||||
)
|
||||
|
||||
export default ClineRulesToggleModal
|
||||
|
||||
@@ -7,9 +7,10 @@ import { CreateRuleFileRequest } from "@shared/proto-conversions/file/rule-files
|
||||
|
||||
interface NewRuleRowProps {
|
||||
isGlobal: boolean
|
||||
ruleType?: string
|
||||
}
|
||||
|
||||
const NewRuleRow: React.FC<NewRuleRowProps> = ({ isGlobal }) => {
|
||||
const NewRuleRow: React.FC<NewRuleRowProps> = ({ isGlobal, ruleType }) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const [filename, setFilename] = useState("")
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
@@ -64,6 +65,7 @@ const NewRuleRow: React.FC<NewRuleRowProps> = ({ isGlobal }) => {
|
||||
CreateRuleFileRequest.create({
|
||||
isGlobal,
|
||||
filename: finalFilename,
|
||||
type: ruleType || "cline",
|
||||
}),
|
||||
)
|
||||
} catch (err) {
|
||||
@@ -97,7 +99,11 @@ const NewRuleRow: React.FC<NewRuleRowProps> = ({ isGlobal }) => {
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
placeholder="rule-name (.md, .txt, or no extension)"
|
||||
placeholder={
|
||||
ruleType === "workflow"
|
||||
? "workflow-name (.md, .txt, or no extension)"
|
||||
: "rule-name (.md, .txt, or no extension)"
|
||||
}
|
||||
value={filename}
|
||||
onChange={(e) => setFilename(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
@@ -121,7 +127,7 @@ const NewRuleRow: React.FC<NewRuleRowProps> = ({ isGlobal }) => {
|
||||
) : (
|
||||
<>
|
||||
<span className="flex-1 text-[var(--vscode-descriptionForeground)] bg-[var(--vscode-input-background)] italic text-xs">
|
||||
New rule file...
|
||||
{ruleType === "workflow" ? "New workflow file..." : "New rule file..."}
|
||||
</span>
|
||||
<div className="flex items-center ml-2 space-x-2">
|
||||
<VSCodeButton
|
||||
|
||||
@@ -62,6 +62,7 @@ const RuleRow: React.FC<{
|
||||
DeleteRuleFileRequest.create({
|
||||
rulePath: rulePath,
|
||||
isGlobal: isGlobal,
|
||||
type: ruleType || "cline",
|
||||
}),
|
||||
).catch((err) => console.error("Failed to delete rule file:", err))
|
||||
}
|
||||
|
||||
@@ -40,16 +40,16 @@ const RulesToggleList = ({
|
||||
ruleType={ruleType}
|
||||
/>
|
||||
))}
|
||||
{showNewRule && <NewRuleRow isGlobal={isGlobal} />}
|
||||
{showNewRule && <NewRuleRow isGlobal={isGlobal} ruleType={ruleType} />}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{showNoRules && (
|
||||
<div className="flex flex-col items-center gap-3 my-3 text-[var(--vscode-descriptionForeground)]">
|
||||
No rules found
|
||||
{ruleType === "workflow" ? "No workflows found" : "No rules found"}
|
||||
</div>
|
||||
)}
|
||||
{showNewRule && <NewRuleRow isGlobal={isGlobal} />}
|
||||
{showNewRule && <NewRuleRow isGlobal={isGlobal} ruleType={ruleType} />}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { memo } from "react"
|
||||
import { memo, useState } from "react"
|
||||
import { TaskServiceClient } from "@/services/grpc-client"
|
||||
import { formatLargeNumber } from "@/utils/format"
|
||||
|
||||
@@ -11,10 +11,16 @@ type HistoryPreviewProps = {
|
||||
|
||||
const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
|
||||
const { taskHistory } = useExtensionState()
|
||||
const [isExpanded, setIsExpanded] = useState(true)
|
||||
|
||||
const handleHistorySelect = (id: string) => {
|
||||
TaskServiceClient.showTaskWithId({ value: id }).catch((error) => console.error("Error showing task:", error))
|
||||
}
|
||||
|
||||
const toggleExpanded = () => {
|
||||
setIsExpanded(!isExpanded)
|
||||
}
|
||||
|
||||
const formatDate = (timestamp: number) => {
|
||||
const date = new Date(timestamp)
|
||||
return date
|
||||
@@ -48,16 +54,31 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.history-header {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.history-header:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
<div
|
||||
className="history-header"
|
||||
onClick={toggleExpanded}
|
||||
style={{
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
margin: "10px 20px 10px 20px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}>
|
||||
<span
|
||||
className={`codicon codicon-chevron-${isExpanded ? "down" : "right"}`}
|
||||
style={{
|
||||
marginRight: "4px",
|
||||
transform: "scale(0.9)",
|
||||
}}></span>
|
||||
<span
|
||||
className="codicon codicon-comment-discussion"
|
||||
style={{
|
||||
@@ -74,102 +95,122 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style={{ padding: "0px 20px 0 20px" }}>
|
||||
{taskHistory
|
||||
.filter((item) => item.ts && item.task)
|
||||
.slice(0, 3)
|
||||
.map((item) => (
|
||||
<div key={item.id} className="history-preview-item" onClick={() => handleHistorySelect(item.id)}>
|
||||
<div style={{ padding: "12px" }}>
|
||||
<div style={{ marginBottom: "8px" }}>
|
||||
<span
|
||||
style={{
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
fontWeight: 500,
|
||||
fontSize: "0.85em",
|
||||
textTransform: "uppercase",
|
||||
}}>
|
||||
{formatDate(item.ts)}
|
||||
</span>
|
||||
</div>
|
||||
{item.isFavorited && (
|
||||
{isExpanded && (
|
||||
<div style={{ padding: "0px 20px 0 20px" }}>
|
||||
{taskHistory.filter((item) => item.ts && item.task).length > 0 ? (
|
||||
<>
|
||||
{taskHistory
|
||||
.filter((item) => item.ts && item.task)
|
||||
.slice(0, 3)
|
||||
.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="history-preview-item"
|
||||
onClick={() => handleHistorySelect(item.id)}>
|
||||
<div style={{ padding: "12px" }}>
|
||||
<div style={{ marginBottom: "8px" }}>
|
||||
<span
|
||||
style={{
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
fontWeight: 500,
|
||||
fontSize: "0.85em",
|
||||
textTransform: "uppercase",
|
||||
}}>
|
||||
{formatDate(item.ts)}
|
||||
</span>
|
||||
</div>
|
||||
{item.isFavorited && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "12px",
|
||||
right: "12px",
|
||||
color: "var(--vscode-button-background)",
|
||||
}}>
|
||||
<span className="codicon codicon-star-full" aria-label="Favorited" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
id={`history-preview-task-${item.id}`}
|
||||
className="history-preview-task"
|
||||
style={{
|
||||
fontSize: "var(--vscode-font-size)",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
marginBottom: "8px",
|
||||
display: "-webkit-box",
|
||||
WebkitLineClamp: 3,
|
||||
WebkitBoxOrient: "vertical",
|
||||
overflow: "hidden",
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-word",
|
||||
overflowWrap: "anywhere",
|
||||
}}>
|
||||
<span className="ph-no-capture">{item.task}</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "0.85em",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
<span>
|
||||
Tokens: ↑{formatLargeNumber(item.tokensIn || 0)} ↓
|
||||
{formatLargeNumber(item.tokensOut || 0)}
|
||||
</span>
|
||||
{!!item.cacheWrites && (
|
||||
<>
|
||||
{" • "}
|
||||
<span>
|
||||
Cache: +{formatLargeNumber(item.cacheWrites || 0)} →{" "}
|
||||
{formatLargeNumber(item.cacheReads || 0)}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{!!item.totalCost && (
|
||||
<>
|
||||
{" • "}
|
||||
<span>API Cost: ${item.totalCost?.toFixed(4)}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}>
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
onClick={() => showHistoryView()}
|
||||
style={{
|
||||
opacity: 0.9,
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "12px",
|
||||
right: "12px",
|
||||
color: "var(--vscode-button-background)",
|
||||
fontSize: "var(--vscode-font-size)",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
<span className="codicon codicon-star-full" aria-label="Favorited" />
|
||||
View all history
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
id={`history-preview-task-${item.id}`}
|
||||
className="history-preview-task"
|
||||
style={{
|
||||
fontSize: "var(--vscode-font-size)",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
marginBottom: "8px",
|
||||
display: "-webkit-box",
|
||||
WebkitLineClamp: 3,
|
||||
WebkitBoxOrient: "vertical",
|
||||
overflow: "hidden",
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-word",
|
||||
overflowWrap: "anywhere",
|
||||
}}>
|
||||
<span className="ph-no-capture">{item.task}</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "0.85em",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
<span>
|
||||
Tokens: ↑{formatLargeNumber(item.tokensIn || 0)} ↓{formatLargeNumber(item.tokensOut || 0)}
|
||||
</span>
|
||||
{!!item.cacheWrites && (
|
||||
<>
|
||||
{" • "}
|
||||
<span>
|
||||
Cache: +{formatLargeNumber(item.cacheWrites || 0)} →{" "}
|
||||
{formatLargeNumber(item.cacheReads || 0)}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{!!item.totalCost && (
|
||||
<>
|
||||
{" • "}
|
||||
<span>API Cost: ${item.totalCost?.toFixed(4)}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}>
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
onClick={() => showHistoryView()}
|
||||
style={{
|
||||
opacity: 0.9,
|
||||
}}>
|
||||
</>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
fontSize: "var(--vscode-font-size)",
|
||||
textAlign: "center",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
fontSize: "var(--vscode-font-size)",
|
||||
padding: "10px 0",
|
||||
}}>
|
||||
View all history
|
||||
No recent tasks
|
||||
</div>
|
||||
</VSCodeButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { vscode } from "@/utils/vscode"
|
||||
import DOMPurify from "dompurify"
|
||||
import { getSafeHostname, normalizeRelativeUrl } from "./utils/mcpRichUtil"
|
||||
import ChatErrorBoundary from "@/components/chat/ChatErrorBoundary"
|
||||
import { WebServiceClient } from "@/services/grpc-client"
|
||||
|
||||
interface OpenGraphData {
|
||||
title?: string
|
||||
@@ -102,45 +103,47 @@ class LinkPreview extends React.Component<LinkPreviewProps, LinkPreviewState> {
|
||||
}
|
||||
}
|
||||
|
||||
private fetchOpenGraphData() {
|
||||
private async fetchOpenGraphData() {
|
||||
try {
|
||||
// Record fetch start time
|
||||
const startTime = Date.now()
|
||||
this.setState({ fetchStartTime: startTime })
|
||||
|
||||
// Send a message to the extension to fetch Open Graph data
|
||||
vscode.postMessage({
|
||||
type: "fetchOpenGraphData",
|
||||
text: this.props.url,
|
||||
// Use the gRPC client to fetch Open Graph data
|
||||
const response = await WebServiceClient.fetchOpenGraphData({
|
||||
value: this.props.url,
|
||||
})
|
||||
|
||||
// Set up a listener for the response
|
||||
this.messageListener = (event: MessageEvent) => {
|
||||
const message = event.data
|
||||
if (message.type === "openGraphData" && message.url === this.props.url) {
|
||||
// Check if there was an error in the response
|
||||
if (message.error) {
|
||||
this.setState({
|
||||
error: "network",
|
||||
errorMessage: message.error,
|
||||
loading: false,
|
||||
hasCompletedFetch: true,
|
||||
})
|
||||
} else {
|
||||
this.setState({
|
||||
ogData: message.openGraphData,
|
||||
loading: false,
|
||||
hasCompletedFetch: true, // Mark as completed
|
||||
})
|
||||
}
|
||||
this.cleanup()
|
||||
// Process the response
|
||||
if (response) {
|
||||
const ogData: OpenGraphData = {
|
||||
title: response.title || undefined,
|
||||
description: response.description || undefined,
|
||||
image: response.image || undefined,
|
||||
url: response.url || undefined,
|
||||
siteName: response.siteName || undefined,
|
||||
type: response.type || undefined,
|
||||
}
|
||||
|
||||
this.setState({
|
||||
ogData,
|
||||
loading: false,
|
||||
hasCompletedFetch: true,
|
||||
})
|
||||
} else {
|
||||
this.setState({
|
||||
error: "network",
|
||||
errorMessage: "Failed to fetch Open Graph data",
|
||||
loading: false,
|
||||
hasCompletedFetch: true,
|
||||
})
|
||||
}
|
||||
|
||||
window.addEventListener("message", this.messageListener)
|
||||
// Clean up the heartbeat interval
|
||||
// (No message listener is needed with gRPC, unlike the previous message-based approach)
|
||||
this.cleanup()
|
||||
|
||||
// Instead of a fixed timeout, use a heartbeat to update the loading message
|
||||
// with the elapsed time, but don't actually timeout
|
||||
// Set up heartbeat for loading indicator
|
||||
this.heartbeatId = setInterval(() => {
|
||||
const elapsedSeconds = Math.floor((Date.now() - startTime) / 1000)
|
||||
if (elapsedSeconds > 0) {
|
||||
|
||||
+24
-8
@@ -19,6 +19,7 @@ import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { McpServiceClient } from "@/services/grpc-client"
|
||||
import { convertProtoMcpServersToMcpServers } from "@shared/proto-conversions/mcp/mcp-server-conversion"
|
||||
import { McpServers, UpdateMcpTimeoutRequest } from "@shared/proto/mcp"
|
||||
import { StringRequest } from "@shared/proto/common"
|
||||
// constant JSX.Elements
|
||||
const TimeoutOptions = [
|
||||
{ value: "30", label: "30 seconds" },
|
||||
@@ -46,6 +47,7 @@ const ServerRow = ({
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
const [isRestarting, setIsRestarting] = useState(false)
|
||||
|
||||
const getStatusColor = useCallback((status: McpServer["status"]) => {
|
||||
switch (status) {
|
||||
@@ -93,10 +95,24 @@ const ServerRow = ({
|
||||
}
|
||||
|
||||
const handleRestart = () => {
|
||||
vscode.postMessage({
|
||||
type: "restartMcpServer",
|
||||
text: server.name,
|
||||
})
|
||||
// Set local state to show "connecting" status
|
||||
setIsRestarting(true)
|
||||
|
||||
// Make the gRPC call
|
||||
McpServiceClient.restartMcpServer({
|
||||
value: server.name,
|
||||
} as StringRequest)
|
||||
.then((response: McpServers) => {
|
||||
// Update with the final state from the server
|
||||
const mcpServers = convertProtoMcpServersToMcpServers(response.mcpServers)
|
||||
setMcpServers(mcpServers)
|
||||
setIsRestarting(false)
|
||||
})
|
||||
.catch((error) => {
|
||||
// Reset the restarting state
|
||||
setIsRestarting(false)
|
||||
console.error("Error restarting MCP server", error)
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = () => {
|
||||
@@ -171,7 +187,7 @@ const ServerRow = ({
|
||||
e.stopPropagation()
|
||||
handleRestart()
|
||||
}}
|
||||
disabled={server.status === "connecting"}>
|
||||
disabled={server.status === "connecting" || isRestarting}>
|
||||
<span className="codicon codicon-sync"></span>
|
||||
</VSCodeButton>
|
||||
{hasTrashIcon && (
|
||||
@@ -267,7 +283,7 @@ const ServerRow = ({
|
||||
width: "calc(100% - 20px)",
|
||||
margin: "0 10px 10px 10px",
|
||||
}}>
|
||||
{server.status === "connecting" ? "Retrying..." : "Retry Connection"}
|
||||
{server.status === "connecting" || isRestarting ? "Retrying..." : "Retry Connection"}
|
||||
</VSCodeButton>
|
||||
|
||||
<DangerButton
|
||||
@@ -363,12 +379,12 @@ const ServerRow = ({
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
onClick={handleRestart}
|
||||
disabled={server.status === "connecting"}
|
||||
disabled={server.status === "connecting" || isRestarting}
|
||||
style={{
|
||||
width: "calc(100% - 14px)",
|
||||
margin: "0 7px 3px 7px",
|
||||
}}>
|
||||
{server.status === "connecting" ? "Restarting..." : "Restart Server"}
|
||||
{server.status === "connecting" || isRestarting ? "Restarting..." : "Restart Server"}
|
||||
</VSCodeButton>
|
||||
|
||||
<DangerButton
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
internationalQwenDefaultModelId,
|
||||
vertexDefaultModelId,
|
||||
vertexModels,
|
||||
vertexGlobalModels,
|
||||
askSageModels,
|
||||
askSageDefaultModelId,
|
||||
askSageDefaultURL,
|
||||
@@ -933,6 +934,7 @@ const ApiOptions = ({
|
||||
<VSCodeOption value="europe-west1">europe-west1</VSCodeOption>
|
||||
<VSCodeOption value="europe-west4">europe-west4</VSCodeOption>
|
||||
<VSCodeOption value="asia-southeast1">asia-southeast1</VSCodeOption>
|
||||
<VSCodeOption value="global">global</VSCodeOption>
|
||||
</VSCodeDropdown>
|
||||
</DropdownContainer>
|
||||
<p
|
||||
@@ -2056,7 +2058,8 @@ const ApiOptions = ({
|
||||
<span style={{ fontWeight: 500 }}>Model</span>
|
||||
</label>
|
||||
{selectedProvider === "anthropic" && createDropdown(anthropicModels)}
|
||||
{selectedProvider === "vertex" && createDropdown(vertexModels)}
|
||||
{selectedProvider === "vertex" &&
|
||||
createDropdown(apiConfiguration?.vertexRegion === "global" ? vertexGlobalModels : vertexModels)}
|
||||
{selectedProvider === "gemini" && createDropdown(geminiModels)}
|
||||
{selectedProvider === "openai-native" && createDropdown(openAiNativeModels)}
|
||||
{selectedProvider === "deepseek" && createDropdown(deepSeekModels)}
|
||||
|
||||
@@ -17,13 +17,6 @@ export const ClineAccountInfoCard = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
// First notify extension to clear API keys and state
|
||||
vscode.postMessage({ type: "accountLogoutClicked" })
|
||||
// Then sign out of Firebase
|
||||
handleSignOut()
|
||||
}
|
||||
|
||||
const handleShowAccount = () => {
|
||||
vscode.postMessage({ type: "showAccountViewClicked" })
|
||||
}
|
||||
@@ -35,38 +28,6 @@ export const ClineAccountInfoCard = () => {
|
||||
View Billing & Usage
|
||||
</VSCodeButton>
|
||||
) : (
|
||||
// <div className="p-2 rounded-[2px] bg-[var(--vscode-dropdown-background)]">
|
||||
// <div className="flex items-center gap-3">
|
||||
// {user.photoURL ? (
|
||||
// <img src={user.photoURL} alt="Profile" className="w-[38px] h-[38px] rounded-full flex-shrink-0" />
|
||||
// ) : (
|
||||
// <div className="w-[38px] h-[38px] rounded-full bg-[var(--vscode-button-background)] flex items-center justify-center text-xl text-[var(--vscode-button-foreground)] flex-shrink-0">
|
||||
// {user.displayName?.[0] || user.email?.[0] || "?"}
|
||||
// </div>
|
||||
// )}
|
||||
// <div className="flex flex-col gap-1 flex-1 overflow-hidden">
|
||||
// {user.displayName && (
|
||||
// <div className="text-[13px] font-bold text-[var(--vscode-foreground)] break-words">
|
||||
// {user.displayName}
|
||||
// </div>
|
||||
// )}
|
||||
// {user.email && (
|
||||
// <div className="text-[13px] text-[var(--vscode-descriptionForeground)] break-words overflow-hidden text-ellipsis">
|
||||
// {user.email}
|
||||
// </div>
|
||||
// )}
|
||||
// <div className="flex gap-2 flex-wrap mt-1">
|
||||
|
||||
// <VSCodeButton
|
||||
// appearance="secondary"
|
||||
// onClick={handleLogout}
|
||||
// className="scale-[0.85] origin-left w-fit mt-0.5 mb-0 -mr-3">
|
||||
// Log out
|
||||
// </VSCodeButton>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
<div>
|
||||
<VSCodeButton onClick={handleLogin} className="mt-0">
|
||||
Sign Up with Cline
|
||||
|
||||
@@ -17,6 +17,7 @@ import ApiOptions from "./ApiOptions"
|
||||
import { TabButton } from "../mcp/configuration/McpConfigurationView"
|
||||
import { useEvent } from "react-use"
|
||||
import { ExtensionMessage } from "@shared/ExtensionMessage"
|
||||
import { StateServiceClient } from "@/services/grpc-client"
|
||||
import FeatureSettingsSection from "./FeatureSettingsSection"
|
||||
import BrowserSettingsSection from "./BrowserSettingsSection"
|
||||
import TerminalSettingsSection from "./TerminalSettingsSection"
|
||||
@@ -148,8 +149,12 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
|
||||
useEvent("message", handleMessage)
|
||||
|
||||
const handleResetState = () => {
|
||||
vscode.postMessage({ type: "resetState" })
|
||||
const handleResetState = async () => {
|
||||
try {
|
||||
await StateServiceClient.resetState({})
|
||||
} catch (error) {
|
||||
console.error("Failed to reset state:", error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleTabChange = (tab: "plan" | "act") => {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { render, screen, fireEvent } from "@testing-library/react"
|
||||
import { describe, it, expect, vi } from "vitest"
|
||||
import ApiOptions from "../ApiOptions"
|
||||
import { ExtensionStateContextProvider } from "@/context/ExtensionStateContext"
|
||||
import { ExtensionStateContextProvider, useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { ApiConfiguration } from "@shared/api"
|
||||
|
||||
vi.mock("../../../context/ExtensionStateContext", async (importOriginal) => {
|
||||
const actual = await importOriginal()
|
||||
@@ -16,10 +17,20 @@ vi.mock("../../../context/ExtensionStateContext", async (importOriginal) => {
|
||||
},
|
||||
setApiConfiguration: vi.fn(),
|
||||
uriScheme: "vscode",
|
||||
requestyModels: {},
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
||||
const mockExtensionState = (apiConfiguration: Partial<ApiConfiguration>) => {
|
||||
vi.mocked(useExtensionState).mockReturnValue({
|
||||
apiConfiguration,
|
||||
setApiConfiguration: vi.fn(),
|
||||
uriScheme: "vscode",
|
||||
requestyModels: {},
|
||||
} as any)
|
||||
}
|
||||
|
||||
describe("ApiOptions Component", () => {
|
||||
vi.clearAllMocks()
|
||||
const mockPostMessage = vi.fn()
|
||||
@@ -27,6 +38,9 @@ describe("ApiOptions Component", () => {
|
||||
beforeEach(() => {
|
||||
//@ts-expect-error - vscode is not defined in the global namespace in test environment
|
||||
global.vscode = { postMessage: mockPostMessage }
|
||||
mockExtensionState({
|
||||
apiProvider: "requesty",
|
||||
})
|
||||
})
|
||||
|
||||
it("renders Requesty API Key input", () => {
|
||||
@@ -45,28 +59,11 @@ describe("ApiOptions Component", () => {
|
||||
<ApiOptions showModelOptions={true} />
|
||||
</ExtensionStateContextProvider>,
|
||||
)
|
||||
const modelIdInput = screen.getByPlaceholderText("Enter Model ID...")
|
||||
const modelIdInput = screen.getByPlaceholderText("Search and select a model...")
|
||||
expect(modelIdInput).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
vi.mock("../../../context/ExtensionStateContext", async (importOriginal) => {
|
||||
const actual = await importOriginal()
|
||||
return {
|
||||
...(actual || {}),
|
||||
// your mocked methods
|
||||
useExtensionState: vi.fn(() => ({
|
||||
apiConfiguration: {
|
||||
apiProvider: "together",
|
||||
requestyApiKey: "",
|
||||
requestyModelId: "",
|
||||
},
|
||||
setApiConfiguration: vi.fn(),
|
||||
uriScheme: "vscode",
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
||||
describe("ApiOptions Component", () => {
|
||||
vi.clearAllMocks()
|
||||
const mockPostMessage = vi.fn()
|
||||
@@ -74,6 +71,9 @@ describe("ApiOptions Component", () => {
|
||||
beforeEach(() => {
|
||||
//@ts-expect-error - vscode is not defined in the global namespace in test environment
|
||||
global.vscode = { postMessage: mockPostMessage }
|
||||
mockExtensionState({
|
||||
apiProvider: "together",
|
||||
})
|
||||
})
|
||||
|
||||
it("renders Together API Key input", () => {
|
||||
@@ -97,24 +97,6 @@ describe("ApiOptions Component", () => {
|
||||
})
|
||||
})
|
||||
|
||||
vi.mock("../../../context/ExtensionStateContext", async (importOriginal) => {
|
||||
const actual = await importOriginal()
|
||||
return {
|
||||
...(actual || {}),
|
||||
useExtensionState: vi.fn(() => ({
|
||||
apiConfiguration: {
|
||||
apiProvider: "fireworks",
|
||||
fireworksApiKey: "",
|
||||
fireworksModelId: "",
|
||||
fireworksModelMaxCompletionTokens: 2000,
|
||||
fireworksModelMaxTokens: 4000,
|
||||
},
|
||||
setApiConfiguration: vi.fn(),
|
||||
uriScheme: "vscode",
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
||||
describe("ApiOptions Component", () => {
|
||||
vi.clearAllMocks()
|
||||
const mockPostMessage = vi.fn()
|
||||
@@ -122,6 +104,14 @@ describe("ApiOptions Component", () => {
|
||||
beforeEach(() => {
|
||||
//@ts-expect-error - vscode is not defined in the global namespace in test environment
|
||||
global.vscode = { postMessage: mockPostMessage }
|
||||
|
||||
mockExtensionState({
|
||||
apiProvider: "fireworks",
|
||||
fireworksApiKey: "",
|
||||
fireworksModelId: "",
|
||||
fireworksModelMaxCompletionTokens: 2000,
|
||||
fireworksModelMaxTokens: 4000,
|
||||
})
|
||||
})
|
||||
|
||||
it("renders Fireworks API Key input", () => {
|
||||
@@ -165,23 +155,6 @@ describe("ApiOptions Component", () => {
|
||||
})
|
||||
})
|
||||
|
||||
vi.mock("../../../context/ExtensionStateContext", async (importOriginal) => {
|
||||
const actual = await importOriginal()
|
||||
return {
|
||||
...(actual || {}),
|
||||
// your mocked methods
|
||||
useExtensionState: vi.fn(() => ({
|
||||
apiConfiguration: {
|
||||
apiProvider: "openai",
|
||||
requestyApiKey: "",
|
||||
requestyModelId: "",
|
||||
},
|
||||
setApiConfiguration: vi.fn(),
|
||||
uriScheme: "vscode",
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
||||
describe("OpenApiInfoOptions", () => {
|
||||
const mockPostMessage = vi.fn()
|
||||
|
||||
@@ -189,6 +162,9 @@ describe("OpenApiInfoOptions", () => {
|
||||
vi.clearAllMocks()
|
||||
//@ts-expect-error - vscode is not defined in the global namespace in test environment
|
||||
global.vscode = { postMessage: mockPostMessage }
|
||||
mockExtensionState({
|
||||
apiProvider: "openai",
|
||||
})
|
||||
})
|
||||
|
||||
it("renders OpenAI Supports Images input", () => {
|
||||
|
||||
@@ -78,7 +78,9 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
localClineRulesToggles: {},
|
||||
localCursorRulesToggles: {},
|
||||
localWindsurfRulesToggles: {},
|
||||
workflowToggles: {},
|
||||
shellIntegrationTimeout: 4000, // default timeout for shell integration
|
||||
isNewUser: false,
|
||||
})
|
||||
const [didHydrateState, setDidHydrateState] = useState(false)
|
||||
const [showWelcome, setShowWelcome] = useState(false)
|
||||
@@ -98,6 +100,59 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
const handleMessage = useCallback((event: MessageEvent) => {
|
||||
const message: ExtensionMessage = event.data
|
||||
switch (message.type) {
|
||||
case "state": {
|
||||
// Handler for direct state messages
|
||||
if (message.state) {
|
||||
const stateData = message.state as ExtensionState
|
||||
console.log("[Webview Context Test Revert] Received direct 'state' message, updating state.")
|
||||
setState((prevState) => {
|
||||
// Versioning logic for autoApprovalSettings (copied from original onResponse)
|
||||
const incomingVersion = stateData.autoApprovalSettings?.version ?? 1
|
||||
const currentVersion = prevState.autoApprovalSettings?.version ?? 1
|
||||
const shouldUpdateAutoApproval = incomingVersion > currentVersion
|
||||
|
||||
const newState = {
|
||||
...stateData,
|
||||
autoApprovalSettings: shouldUpdateAutoApproval
|
||||
? stateData.autoApprovalSettings
|
||||
: prevState.autoApprovalSettings,
|
||||
}
|
||||
|
||||
// Update welcome screen state based on API configuration (copied from original onResponse)
|
||||
const config = stateData.apiConfiguration
|
||||
const hasKey = config
|
||||
? [
|
||||
config.apiKey,
|
||||
config.openRouterApiKey,
|
||||
config.awsRegion,
|
||||
config.vertexProjectId,
|
||||
config.openAiApiKey,
|
||||
config.ollamaModelId,
|
||||
config.lmStudioModelId,
|
||||
config.liteLlmApiKey,
|
||||
config.geminiApiKey,
|
||||
config.openAiNativeApiKey,
|
||||
config.deepSeekApiKey,
|
||||
config.requestyApiKey,
|
||||
config.togetherApiKey,
|
||||
config.qwenApiKey,
|
||||
config.doubaoApiKey,
|
||||
config.mistralApiKey,
|
||||
config.vsCodeLmModelSelector,
|
||||
config.clineApiKey,
|
||||
config.asksageApiKey,
|
||||
config.xaiApiKey,
|
||||
config.sambanovaApiKey,
|
||||
].some((key) => key !== undefined)
|
||||
: false
|
||||
|
||||
setShowWelcome(!hasKey)
|
||||
setDidHydrateState(true)
|
||||
return newState
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
case "theme": {
|
||||
if (message.text) {
|
||||
setTheme(convertTextMateToHljs(JSON.parse(message.text)))
|
||||
@@ -167,32 +222,33 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
const stateSubscriptionRef = useRef<(() => void) | null>(null)
|
||||
|
||||
// Subscribe to state updates using the new gRPC streaming API
|
||||
/* // TEST REVERT: Commenting out gRPC state subscription
|
||||
useEffect(() => {
|
||||
// Set up state subscription
|
||||
stateSubscriptionRef.current = StateServiceClient.subscribeToState(
|
||||
{},
|
||||
{
|
||||
onResponse: (response) => {
|
||||
console.log("[DEBUG] got state update via subscription", response)
|
||||
console.log("[DEBUG] got state update via subscription", response);
|
||||
if (response.stateJson) {
|
||||
try {
|
||||
const stateData = JSON.parse(response.stateJson) as ExtensionState
|
||||
console.log("[DEBUG] parsed state JSON, updating state")
|
||||
const stateData = JSON.parse(response.stateJson) as ExtensionState;
|
||||
console.log("[DEBUG] parsed state JSON, updating state");
|
||||
setState((prevState) => {
|
||||
// Versioning logic for autoApprovalSettings
|
||||
const incomingVersion = stateData.autoApprovalSettings?.version ?? 1
|
||||
const currentVersion = prevState.autoApprovalSettings?.version ?? 1
|
||||
const shouldUpdateAutoApproval = incomingVersion > currentVersion
|
||||
const incomingVersion = stateData.autoApprovalSettings?.version ?? 1;
|
||||
const currentVersion = prevState.autoApprovalSettings?.version ?? 1;
|
||||
const shouldUpdateAutoApproval = incomingVersion > currentVersion;
|
||||
|
||||
const newState = {
|
||||
...stateData,
|
||||
autoApprovalSettings: shouldUpdateAutoApproval
|
||||
? stateData.autoApprovalSettings
|
||||
: prevState.autoApprovalSettings,
|
||||
}
|
||||
};
|
||||
|
||||
// Update welcome screen state based on API configuration
|
||||
const config = stateData.apiConfiguration
|
||||
const config = stateData.apiConfiguration;
|
||||
const hasKey = config
|
||||
? [
|
||||
config.apiKey,
|
||||
@@ -217,41 +273,52 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
config.xaiApiKey,
|
||||
config.sambanovaApiKey,
|
||||
].some((key) => key !== undefined)
|
||||
: false
|
||||
: false;
|
||||
|
||||
setShowWelcome(!hasKey)
|
||||
setDidHydrateState(true)
|
||||
setShowWelcome(!hasKey);
|
||||
setDidHydrateState(true);
|
||||
|
||||
console.log("[DEBUG] returning new state in ESC")
|
||||
console.log("[DEBUG] returning new state in ESC");
|
||||
|
||||
return newState
|
||||
})
|
||||
return newState;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error parsing state JSON:", error)
|
||||
console.log("[DEBUG] ERR getting state", error)
|
||||
console.error("Error parsing state JSON:", error);
|
||||
console.log("[DEBUG] ERR getting state", error);
|
||||
}
|
||||
}
|
||||
console.log('[DEBUG] ended "got subscribed state"')
|
||||
console.log('[DEBUG] ended "got subscribed state"');
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Error in state subscription:", error)
|
||||
console.error("Error in state subscription:", error);
|
||||
},
|
||||
onComplete: () => {
|
||||
console.log("State subscription completed")
|
||||
console.log("State subscription completed");
|
||||
},
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
// Still send the webviewDidLaunch message for other initialization
|
||||
vscode.postMessage({ type: "webviewDidLaunch" })
|
||||
vscode.postMessage({ type: "webviewDidLaunch" });
|
||||
|
||||
// Clean up subscription when component unmounts
|
||||
return () => {
|
||||
if (stateSubscriptionRef.current) {
|
||||
stateSubscriptionRef.current()
|
||||
stateSubscriptionRef.current = null
|
||||
stateSubscriptionRef.current();
|
||||
stateSubscriptionRef.current = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
*/ // END TEST REVERT
|
||||
|
||||
// For the test revert, ensure webviewDidLaunch is still sent if not done by the above useEffect
|
||||
useEffect(() => {
|
||||
// This effect now only sends webviewDidLaunch if the gRPC subscription is commented out.
|
||||
// If the gRPC subscription is active, it sends webviewDidLaunch.
|
||||
// To avoid sending it twice if you uncomment the above, you might add a flag.
|
||||
// For this specific test (gRPC sub commented out), this is fine.
|
||||
console.log("[Webview Context Test Revert] Sending webviewDidLaunch from separate useEffect.")
|
||||
vscode.postMessage({ type: "webviewDidLaunch" })
|
||||
}, [])
|
||||
|
||||
const contextValue: ExtensionStateContextType = {
|
||||
@@ -272,6 +339,7 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
localClineRulesToggles: state.localClineRulesToggles || {},
|
||||
localCursorRulesToggles: state.localCursorRulesToggles || {},
|
||||
localWindsurfRulesToggles: state.localWindsurfRulesToggles || {},
|
||||
workflowToggles: state.workflowToggles || {},
|
||||
enableCheckpointsSetting: state.enableCheckpointsSetting,
|
||||
setApiConfiguration: (value) =>
|
||||
setState((prevState) => ({
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
import { useCallback } from "react"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
|
||||
import { ActionMetadata } from "@/components/chat/auto-approve-menu/types"
|
||||
|
||||
export function useAutoApproveActions() {
|
||||
const { autoApprovalSettings } = useExtensionState()
|
||||
|
||||
// Check if action is enabled
|
||||
const isChecked = useCallback(
|
||||
(action: ActionMetadata): boolean => {
|
||||
switch (action.id) {
|
||||
case "enableAll":
|
||||
return Object.values(autoApprovalSettings.actions).every(Boolean)
|
||||
case "enableNotifications":
|
||||
return autoApprovalSettings.enableNotifications
|
||||
case "enableAutoApprove":
|
||||
return autoApprovalSettings.enabled
|
||||
default:
|
||||
return autoApprovalSettings.actions[action.id] ?? false
|
||||
}
|
||||
},
|
||||
[autoApprovalSettings],
|
||||
)
|
||||
|
||||
// Check if action is favorited
|
||||
const isFavorited = useCallback(
|
||||
(action: ActionMetadata): boolean => {
|
||||
const favorites = autoApprovalSettings.favorites || []
|
||||
return favorites.includes(action.id)
|
||||
},
|
||||
[autoApprovalSettings.favorites],
|
||||
)
|
||||
|
||||
// Toggle favorite status
|
||||
const toggleFavorite = useCallback(
|
||||
(actionId: string) => {
|
||||
const currentFavorites = autoApprovalSettings.favorites || []
|
||||
let newFavorites: string[]
|
||||
|
||||
if (currentFavorites.includes(actionId)) {
|
||||
newFavorites = currentFavorites.filter((id) => id !== actionId)
|
||||
} else {
|
||||
newFavorites = [...currentFavorites, actionId]
|
||||
}
|
||||
|
||||
vscode.postMessage({
|
||||
type: "autoApprovalSettings",
|
||||
autoApprovalSettings: {
|
||||
...autoApprovalSettings,
|
||||
version: (autoApprovalSettings.version ?? 1) + 1,
|
||||
favorites: newFavorites,
|
||||
},
|
||||
})
|
||||
},
|
||||
[autoApprovalSettings],
|
||||
)
|
||||
|
||||
// Update action state
|
||||
const updateAction = useCallback(
|
||||
(action: ActionMetadata, value: boolean) => {
|
||||
const actionId = action.id
|
||||
const subActionId = action.subAction?.id
|
||||
|
||||
if (actionId === "enableAutoApprove") {
|
||||
updateAutoApproveEnabled(value)
|
||||
return
|
||||
}
|
||||
|
||||
if (actionId === "enableAll" || subActionId === "enableAll") {
|
||||
toggleAll(action, value)
|
||||
return
|
||||
}
|
||||
|
||||
if (actionId === "enableNotifications" || subActionId === "enableNotifications") {
|
||||
updateNotifications(action, value)
|
||||
return
|
||||
}
|
||||
|
||||
let newActions = {
|
||||
...autoApprovalSettings.actions,
|
||||
[actionId]: value,
|
||||
}
|
||||
|
||||
if (value === false && subActionId) {
|
||||
// @ts-expect-error: TODO: See how we can fix this
|
||||
newActions[subActionId] = false
|
||||
}
|
||||
|
||||
if (value === true && action.parentActionId) {
|
||||
newActions[action.parentActionId as keyof AutoApprovalSettings["actions"]] = true
|
||||
}
|
||||
|
||||
// Check if this will result in any enabled actions
|
||||
const willHaveEnabledActions = Object.values(newActions).some(Boolean)
|
||||
|
||||
vscode.postMessage({
|
||||
type: "autoApprovalSettings",
|
||||
autoApprovalSettings: {
|
||||
...autoApprovalSettings,
|
||||
version: (autoApprovalSettings.version ?? 1) + 1,
|
||||
actions: newActions,
|
||||
enabled: willHaveEnabledActions,
|
||||
},
|
||||
})
|
||||
},
|
||||
[autoApprovalSettings],
|
||||
)
|
||||
|
||||
// Update max requests
|
||||
const updateMaxRequests = useCallback(
|
||||
(maxRequests: number) => {
|
||||
vscode.postMessage({
|
||||
type: "autoApprovalSettings",
|
||||
autoApprovalSettings: {
|
||||
...autoApprovalSettings,
|
||||
version: (autoApprovalSettings.version ?? 1) + 1,
|
||||
maxRequests,
|
||||
},
|
||||
})
|
||||
},
|
||||
[autoApprovalSettings],
|
||||
)
|
||||
|
||||
// Update auto-approve enabled state
|
||||
const updateAutoApproveEnabled = useCallback(
|
||||
(checked: boolean) => {
|
||||
vscode.postMessage({
|
||||
type: "autoApprovalSettings",
|
||||
autoApprovalSettings: {
|
||||
...autoApprovalSettings,
|
||||
version: (autoApprovalSettings.version ?? 1) + 1,
|
||||
enabled: checked,
|
||||
},
|
||||
})
|
||||
},
|
||||
[autoApprovalSettings],
|
||||
)
|
||||
|
||||
// Toggle all actions
|
||||
const toggleAll = useCallback(
|
||||
(action: ActionMetadata, checked: boolean) => {
|
||||
let actions = { ...autoApprovalSettings.actions }
|
||||
|
||||
for (const action of Object.keys(actions)) {
|
||||
actions[action as keyof AutoApprovalSettings["actions"]] = checked
|
||||
}
|
||||
|
||||
vscode.postMessage({
|
||||
type: "autoApprovalSettings",
|
||||
autoApprovalSettings: {
|
||||
...autoApprovalSettings,
|
||||
version: (autoApprovalSettings.version ?? 1) + 1,
|
||||
actions,
|
||||
enabled: checked,
|
||||
},
|
||||
})
|
||||
},
|
||||
[autoApprovalSettings],
|
||||
)
|
||||
|
||||
// Update notifications setting
|
||||
const updateNotifications = useCallback(
|
||||
(action: ActionMetadata, checked: boolean) => {
|
||||
if (action.id === "enableNotifications") {
|
||||
vscode.postMessage({
|
||||
type: "autoApprovalSettings",
|
||||
autoApprovalSettings: {
|
||||
...autoApprovalSettings,
|
||||
version: (autoApprovalSettings.version ?? 1) + 1,
|
||||
enableNotifications: checked,
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
[autoApprovalSettings],
|
||||
)
|
||||
|
||||
return {
|
||||
isChecked,
|
||||
isFavorited,
|
||||
toggleFavorite,
|
||||
updateAction,
|
||||
updateMaxRequests,
|
||||
updateAutoApproveEnabled,
|
||||
toggleAll,
|
||||
updateNotifications,
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,52 @@
|
||||
export interface SlashCommand {
|
||||
name: string
|
||||
description: string
|
||||
description?: string
|
||||
section?: "default" | "custom"
|
||||
}
|
||||
|
||||
export const SUPPORTED_SLASH_COMMANDS: SlashCommand[] = [
|
||||
export const DEFAULT_SLASH_COMMANDS: SlashCommand[] = [
|
||||
{
|
||||
name: "newtask",
|
||||
description: "Create a new task with context from the current task",
|
||||
section: "default",
|
||||
},
|
||||
{
|
||||
name: "smol",
|
||||
description: "Condenses your current context window",
|
||||
section: "default",
|
||||
},
|
||||
{
|
||||
name: "newrule",
|
||||
description: "Create a new Cline rule based on your conversation",
|
||||
section: "default",
|
||||
},
|
||||
{
|
||||
name: "reportbug",
|
||||
description: "Create a Github issue with Cline",
|
||||
section: "default",
|
||||
},
|
||||
]
|
||||
|
||||
export function getWorkflowCommands(workflowToggles: Record<string, boolean>): SlashCommand[] {
|
||||
return Object.entries(workflowToggles)
|
||||
.filter(([_, enabled]) => enabled)
|
||||
.map(([filePath, _]) => {
|
||||
// potentially remove the file extension if there is one, but this would then require
|
||||
// that we prevent users from having the same fname with different extensions
|
||||
const fileName = filePath.replace(/^.*[/\\]/, "")
|
||||
|
||||
return {
|
||||
name: fileName,
|
||||
section: "custom",
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Regex for detecting slash commands in text
|
||||
export const slashCommandRegex = /\/([a-zA-Z0-9_-]+)(\s|$)/
|
||||
// currently doesn't allow whitespace inside of the filename
|
||||
export const slashCommandRegex = /\/([a-zA-Z0-9_\.-]+)(\s|$)/
|
||||
export const slashCommandRegexGlobal = new RegExp(slashCommandRegex.source, "g")
|
||||
export const slashCommandDeleteRegex = /^\s*\/([a-zA-Z0-9_-]+)$/
|
||||
export const slashCommandDeleteRegex = /^\s*\/([a-zA-Z0-9_\.-]+)$/
|
||||
|
||||
/**
|
||||
* Removes a slash command at the cursor position
|
||||
@@ -81,13 +102,16 @@ export function shouldShowSlashCommandsMenu(text: string, cursorPosition: number
|
||||
/**
|
||||
* Gets filtered slash commands that match the current input
|
||||
*/
|
||||
export function getMatchingSlashCommands(query: string): SlashCommand[] {
|
||||
export function getMatchingSlashCommands(query: string, workflowToggles: Record<string, boolean> = {}): SlashCommand[] {
|
||||
const workflowCommands = getWorkflowCommands(workflowToggles)
|
||||
const allCommands = [...DEFAULT_SLASH_COMMANDS, ...workflowCommands]
|
||||
|
||||
if (!query) {
|
||||
return [...SUPPORTED_SLASH_COMMANDS]
|
||||
return allCommands
|
||||
}
|
||||
|
||||
// filter commands that start with the query (case sensitive)
|
||||
return SUPPORTED_SLASH_COMMANDS.filter((cmd) => cmd.name.startsWith(query))
|
||||
return allCommands.filter((cmd) => cmd.name.startsWith(query))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -110,19 +134,22 @@ export function insertSlashCommand(text: string, commandName: string): { newValu
|
||||
* Determines the validation state of a slash command
|
||||
* Returns partial if we have a partial match against valid commands, or full for full match
|
||||
*/
|
||||
export function validateSlashCommand(command: string): "full" | "partial" | null {
|
||||
export function validateSlashCommand(command: string, workflowToggles: Record<string, boolean> = {}): "full" | "partial" | null {
|
||||
if (!command) {
|
||||
return null
|
||||
}
|
||||
|
||||
const workflowCommands = getWorkflowCommands(workflowToggles)
|
||||
const allCommands = [...DEFAULT_SLASH_COMMANDS, ...workflowCommands]
|
||||
|
||||
// case sensitive matching
|
||||
const exactMatch = SUPPORTED_SLASH_COMMANDS.some((cmd) => cmd.name === command)
|
||||
const exactMatch = allCommands.some((cmd) => cmd.name === command)
|
||||
|
||||
if (exactMatch) {
|
||||
return "full"
|
||||
}
|
||||
|
||||
const partialMatch = SUPPORTED_SLASH_COMMANDS.some((cmd) => cmd.name.startsWith(command))
|
||||
const partialMatch = allCommands.some((cmd) => cmd.name.startsWith(command))
|
||||
|
||||
if (partialMatch) {
|
||||
return "partial"
|
||||
|
||||
Reference in New Issue
Block a user