Compare commits

...
Author SHA1 Message Date
arafatkatze 1d9b9445a0 Lower border radius 2025-05-06 09:12:20 +04:00
Araandellipsis-dev[bot] 21c3c33fed Update src/services/telemetry/TelemetryService.ts
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-05-06 10:36:29 +05:30
Trevor Hudsonandellipsis-dev[bot] fb43add6df Update src/core/controller/task/clearTask.ts
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-05-05 16:25:16 -07:00
Trevor Hudson 362b05b25b remove check to send events anytime a new task is created while on an existing task 2025-05-05 16:13:10 -07:00
Trevor Hudson 59894c40e9 remove commented out parts 2025-05-02 18:56:18 -07:00
Trevor Hudson 88980a8e1b changeset 2025-05-02 18:26:01 -07:00
Trevor Hudson bd52febe90 collect messages 2025-05-01 23:05:36 -07:00
Trevor Hudson 9395d25f7a add collection method 2025-05-01 19:17:49 -07:00
canvrno 5802b6847e [PROTOBUS] Move createRuleFile to protobus 🚌💨 (#3122)
* createRuleFile protobus migration

* deleteRuleFile protobus

* mend

* mend

* mend

* Generic response for delete request

* refactored deleteRuleFile for consolidation

* rename ruleFileResult to ruleFile

* rebase and cleanup

* createRuleFile protobus migration

* ellipsis changes

* consolidated ruleFile protos

* prep for merge on to 3124

* removed ruleFileOperations
2025-05-01 17:25:29 -07:00
Dennis Bartlett 4a768702aa Fix Changeset (#3239) 2025-05-01 16:50:15 -07:00
canvrno 4565e067af [PROTOBUS] file checkIsImageUrl (#3109)
* checkIsImageURL protobus migration

* changeset

* rebase and move to web-content

* cleanup

* metadata

* rename return message

* removed old import, metadata
2025-05-01 16:11:58 -07:00
canvrno 4650ffa86b [PROTOBUS] Move deleteRuleFile to protobus 🚌💨 (#3124)
* createRuleFile protobus migration

* deleteRuleFile protobus

* mend

* mend

* mend

* ellipsis changes

* Generic response for delete request

* refactored deleteRuleFile for consolidation

* rename ruleFileResult to ruleFile
2025-05-01 16:06:54 -07:00
EvanandElephant Lumps f6d50ead3f Setup docs (#3230)
* rename old docs folder

* set up mintlify docs base

* add back script

---------

Co-authored-by: Elephant Lumps <celestial_vault@mac.mynetworksettings.com>
2025-05-01 16:04:07 -07:00
David Nanyan 70cc437d71 [ISSUE-3145] Fix Windows path issue: Correct handling of import.meta.url to avoid leading slash in pathname (#3237)
* [ISSUE-3145] Fix Windows path issue: Correct handling of import.meta.url to avoid leading slash in pathname

* Add changeset file
2025-05-01 14:42:35 -07:00
watany bdfda6f908 feat(bedrock): Introduce Amazon Nova Premier (#3225)
* feat(bedrock): Introduce Amazon Nova Premier

* changeset
2025-05-01 14:39:32 -07:00
David Nanyan c5de50fdd2 [3093] Fix Handle @withRetry() SyntaxError when running extension locally issue (#3190) 2025-05-02 02:14:13 +05:30
Toshii 77c9863b50 Create .clinerules directory when adding new cline rule in ui and .clinerules is currently a file (#3217)
* create clinerules dir

* change default

* changeset

* md
2025-05-01 10:27:37 -07:00
nomavenandShlomoCode 03d44105cc Adding copy button to code blocks (#3011)
* feat: add copy button to code blocks

* Adding ability to copy Code blocks

* feat: add OpenRouter base URL and balance display component

---------

Co-authored-by: ShlomoCode <78599753+ShlomoCode@users.noreply.github.com>
2025-05-01 00:51:28 -07:00
clicube 79b76fd783 feat: Support AWS Bedrock Application Inference Profiles for Cost Tracking (#2078)
* feat: Add support for custom model ID in AWS Bedrock provider

* preserve settings when switching Act-Plan modes

* Use base model ID for ApiHandler behavior determination when using a custom model on AWS Bedrock.
2025-05-01 12:17:28 +05:30
EvanandElephant Lumps 19cc8bc9f8 Add terminal connection timeout (#3218)
* add terminal connection timeout

* changeset

---------

Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-04-30 22:40:58 -05:00
Dennis Bartlett 08c04a3c67 Disable Codespell (#3224) 2025-04-30 19:21:09 -07:00
canvrno 41ae7326c0 Fix: Git mentions w/ no commits in workspace repo (#3179)
* fix for git mentions with no commits in repo
2025-04-30 19:15:35 -07:00
Trevor Hudson b0961f4538 Trevhud/remove linear (#3174)
* remove linear pull request action

* changeset
2025-04-30 18:51:16 -07:00
Tomás Barreiro 26242f6378 Fix tests: compile esmodules to cjs and bundle them (#3030)
* Run pretest in CI to build all tests

* Alias paths when running tests

* Bundle ES modules with esbuild

* alias packages
2025-04-30 17:51:32 -07:00
Tomás Barreiro 13228ed46f Fail the test workflow on test failures (#3197)
* Run pretest in CI to build all tests

* Alias paths when running tests

* Bundle ES modules with esbuild

* alias packages

* Preserve the test scripts exit code and display the output

* Remove outdated test
2025-04-30 17:51:03 -07:00
Tomás Barreiro d162a4b420 Alias paths on integration tests (#3196)
* Run pretest in CI to build all tests

* Alias paths when running tests
2025-04-30 17:49:40 -07:00
dependabot[bot] 1704684af8 Bump vite from 6.2.6 to 6.3.4 in /webview-ui in the npm_and_yarn group (#3214)
Bumps the npm_and_yarn group in /webview-ui with 1 update: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `vite` from 6.2.6 to 6.3.4
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.3.4/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.3.4
  dependency-type: direct:development
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-30 17:25:43 -07:00
pashpashpashandCline Evaluation c63d9a13a5 Fix text to say "drop" instead of "drag" (#3221)
* drag -> drop

* text fix

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-04-30 17:18:48 -07:00
Frostbourne 65243adb24 [ENG-514] Introduce UI library (#3222)
* update tailwind

* install heroui

* Introduce HeroUIProvider and reorganize providers

* changeset

* use tailwind config

* use custom theme

* changeset

* Delete .changeset/large-boxes-behave.md
2025-04-30 17:18:18 -07:00
Tomás Barreiro e35f7b4e21 Run pretest in CI to build all tests (#2930) 2025-04-30 17:17:29 -07:00
96 changed files with 13657 additions and 731 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
create clinerules folder if its currently a file and creating new rule
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
updated drag and drop text to say "drop" instead of "drag"
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Allow option to collect events to send them in a bundle to avoid sending too many events
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Add support for custom model ID in AWS Bedrock provider, enabling use of Application Inference Profile.
+1 -1
View File
@@ -1,5 +1,5 @@
---
"cline": minor
"claude-dev": minor
---
Add support for custom API request timeout. Previously, timeouts were hardcoded to 30 seconds for providers like Ollama or 15 seconds for OpenRouter and Cline. Now users can set a custom timeout value in milliseconds through the settings interface.
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Remove linear pull request action
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
createRuleFile protobus migration
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Fix Handle @withRetry() SyntaxError when running extension locally issue
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fix for git commit mentions in repos with no git commits
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
feat(bedrock): Introduce Amazon Nova Premier
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Add a timeout setting for the terminal connection, allowing users to adjust this if they are having timeout issues
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Added copy button to code blocks.
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Introduce UI library for future UI development
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
checkIsImageURL migrated to protobus
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Fix Windows path issue: Correct handling of import.meta.url to avoid leading slash in pathname
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
deleteRuleFile protobus migration
+1
View File
@@ -13,6 +13,7 @@ permissions:
jobs:
codespell:
if: false
name: Check for spelling errors
runs-on: ubuntu-latest
-177
View File
@@ -1,177 +0,0 @@
name: Create Linear Issue on Pull Request
on:
workflow_dispatch:
pull_request:
branches: [main]
types: [opened]
permissions:
pull-requests: write
jobs:
create-linear-issue-on-pull-request:
runs-on: ubuntu-latest
steps:
- name: Check for existing Linear link
id: check-linear
uses: actions/github-script@v6
with:
result-encoding: string
script: |
const pr = context.payload.pull_request;
// 1) PR body
if (/https?:\/\/linear\.app/.test(pr.body||"")) {
return "true";
}
// 2) Any linked GitHub issues?
const res = await github.graphql(
`query($owner:String!,$repo:String!,$prNumber:Int!){
repository(owner:$owner,name:$repo){
pullRequest(number:$prNumber){
closingIssuesReferences(first:10){
nodes{number}
}
}
}
}`,
{
owner: context.repo.owner,
repo: context.repo.repo,
prNumber: pr.number
}
);
for (const {number} of res.repository.pullRequest.closingIssuesReferences.nodes) {
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: number
});
if (comments.data.some(c=>/https?:\/\/linear\.app/.test(c.body))) {
return "true";
}
}
return "false";
- name: Find or create Linear issue via GraphQL
if: steps.check-linear.outputs.result == 'false'
id: linear
uses: actions/github-script@v6
env:
LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
with:
result-encoding: string
script: |
const API = 'https://api.linear.app/graphql';
const apiKey = process.env.LINEAR_API_KEY;
// Check if API key exists
if (!apiKey) {
core.setFailed('LINEAR_API_KEY is not set. Please add it to your repository secrets.');
core.setOutput('error', 'true');
core.setOutput('error-message', 'LINEAR_API_KEY is not set. Please add it to your repository secrets.');
return;
}
// Helper to call Linear with error handling
async function gql(q, v) {
try {
const r = await fetch(API, {
method:'POST',
headers:{
'Content-Type':'application/json',
'Authorization': apiKey
},
body: JSON.stringify({ query: q, variables: v })
});
if (!r.ok) {
throw new Error(`Linear API responded with status ${r.status}: ${await r.text()}`);
}
const json = await r.json();
// Check for GraphQL errors
if (json.errors && json.errors.length > 0) {
const errorMessages = json.errors.map(e => e.message).join(', ');
throw new Error(`Linear GraphQL errors: ${errorMessages}`);
}
return json.data;
} catch (error) {
core.error(`Error calling Linear API: ${error.message}`);
throw error;
}
}
try {
// 1) Set team ID
const teamId = "19b9c1b2-5f58-498c-b1bf-23ee8f52a677"
// 2) Look for existing issue by PR URL
const pr = context.payload.pull_request;
const searchData = await gql(
`query($team:ID!,$q:String!){
issues(filter: { team: { id: { eq: $team } } attachments: { some: { url: { eq: $q } } } }){nodes{id,url}}
}`,
{ team: teamId, q: pr.html_url }
);
let issue = searchData.issues.nodes[0];
// 3) Create if missing
if (!issue) {
const createData = await gql(
`mutation($input:IssueCreateInput!){
issueCreate(input:$input){issue{id,url}}
}`,
{
input: {
teamId,
title: `[GITHUB] ${pr.title}`,
description: `${pr.body||''}\n\n${pr.html_url}`,
stateId: "4d9bcba2-6712-47e3-b577-6ec1ee023dc2",
labelIds: ["504e7d60-5037-483f-a9b8-7e298bdf116f"]
}
}
);
issue = createData.issueCreate.issue;
}
// Set output for next steps
core.setOutput('linear-issue-url', issue.url);
core.setOutput('error', 'false');
} catch (error) {
core.setOutput('error', 'true');
core.setOutput('error-message', error.message);
core.setFailed(`Failed to create or find Linear issue: ${error.message}`);
}
- name: Comment PR with Linear link
if: steps.check-linear.outputs.result == 'false'
uses: actions/github-script@v6
with:
script: |
const pr = context.payload.pull_request;
const url = `${{ steps.linear.outputs.linear-issue-url }}`;
const body = `🔗 Linear issue created: ${url}`;
// Fetch existing comments
const { data: comments } = await github.rest.issues.listComments({
...context.repo,
issue_number: pr.number
});
const botComment = comments.find(c =>
c.user.type === "Bot" && c.body.startsWith("🔗 Linear issue created:")
);
if (botComment) {
await github.rest.issues.updateComment({
...context.repo,
comment_id: botComment.id,
body
});
} else {
await github.rest.issues.createComment({
...context.repo,
issue_number: pr.number,
body
});
}
+15 -3
View File
@@ -70,8 +70,8 @@ jobs:
run: npm run format
# Build the extension before running tests
- name: Build Extension
run: npm run compile
- name: Build Tests and Extension
run: npm run pretest
- name: Unit Tests
run: npm run test:unit
@@ -81,7 +81,7 @@ jobs:
id: extension_coverage
continue-on-error: true
run: |
xvfb-run -a npm run test:coverage > extension_coverage.txt 2>&1 || true
xvfb-run -a npm run test:coverage > extension_coverage.txt 2>&1
PYTHONPATH=.github/scripts python -m coverage_check extract-coverage extension_coverage.txt --type=extension --github-output --verbose
# Run webview tests with coverage
@@ -106,6 +106,18 @@ jobs:
webview-ui/webview_coverage.txt
retention-period: workflow # Artifacts are automatically deleted when the workflow completes
# Set the check as failed if any of the tests failed
- name: Check for test failures
run: |
# Check if any of the test steps failed
# https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs#steps-context
if [ "${{ steps.extension_coverage.outcome }}" != "success" ] || [ "${{ steps.webview_coverage.outcome }}" != "success" ]; then
echo "Tests failed."
cat extension_coverage.txt
cat webview-ui/webview_coverage.txt
exit 1
fi
coverage:
needs: test
runs-on: ubuntu-latest
+4
View File
@@ -6,6 +6,10 @@ export default defineConfig({
mocha: {
ui: "bdd",
timeout: 20000, // Maximum time (in ms) that a test can run before failing
/** Set up alias path resolution during tests
* @See {@link file://./test-setup.js}
*/
require: ["./test-setup.js"],
},
workspaceFolder: "test-workspace",
version: "stable",
Binary file not shown.

After

Width:  |  Height:  |  Size: 902 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 B

+71
View File
@@ -0,0 +1,71 @@
{
"$schema": "https://mintlify.com/docs.json",
"theme": "linden",
"name": "Cline",
"description": "AI-powered coding assistant for VSCode",
"colors": {
"primary": "#9D4EDD",
"light": "#F0E6FF",
"dark": "#000000"
},
"logo": {
"light": "/assets/robot_panel_light.png",
"dark": "/assets/robot_panel_dark.png"
},
"favicon": {
"light": "/assets/robot_panel_light.png",
"dark": "/assets/robot_panel_dark.png"
},
"background": {
"color": {
"light": "#F0E6FF",
"dark": "#000000"
},
"decoration": "gradient"
},
"styling": {
"eyebrows": "breadcrumbs",
"codeblocks": "system"
},
"appearance": {
"default": "system",
"strict": false
},
"fonts": {
"family": "Roboto",
"weight": 400
},
"navbar": {
"links": [
{
"label": "GitHub",
"href": "https://github.com/cline/cline"
},
{
"label": "Discord",
"href": "https://discord.gg/cline"
}
],
"primary": {
"type": "button",
"label": "Install Cline",
"href": "https://cline.bot/install?utm_source=website&utm_medium=header"
}
},
"navigation": {
"pages": ["introduction"]
},
"footer": {
"socials": {
"x": "https://x.com/cline",
"github": "https://github.com/cline/cline",
"discord": "https://discord.gg/cline"
}
},
"search": {
"prompt": "Search Cline documentation..."
},
"contextual": {
"options": ["copy"]
}
}
+4
View File
@@ -0,0 +1,4 @@
---
title: "Hello World"
description: "This is the introduction to the documentation"
---
+2
View File
@@ -19,6 +19,7 @@ const aliasResolverPlugin = {
"@services": path.resolve(__dirname, "src/services"),
"@shared": path.resolve(__dirname, "src/shared"),
"@utils": path.resolve(__dirname, "src/utils"),
"@packages": path.resolve(__dirname, "src/packages"),
}
// For each alias entry, create a resolver
@@ -124,6 +125,7 @@ const extensionConfig = {
define: {
"process.env.IS_DEV": JSON.stringify(!production),
},
tsconfig: path.resolve(__dirname, "tsconfig.json"),
plugins: [
copyWasmFiles,
aliasResolverPlugin,
+7559 -2
View File
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -292,7 +292,7 @@
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
"package": "npm run build:webview && npm run check-types && npm run lint && node esbuild.js --production",
"protos": "node proto/build-proto.js && prettier src/shared/proto --write && prettier src/core/controller --write",
"compile-tests": "tsc -p ./tsconfig.test.json --outDir out",
"compile-tests": "node ./scripts/build-tests.js",
"watch-tests": "tsc -p . -w --outDir out",
"pretest": "npm run compile-tests && npm run compile && npm run lint",
"check-types": "tsc --noEmit",
@@ -312,7 +312,8 @@
"publish:marketplace:prerelease": "vsce publish --pre-release && ovsx publish --pre-release",
"prepare": "husky",
"changeset": "changeset",
"version-packages": "changeset version"
"version-packages": "changeset version",
"docs:preview": "cd docs && mintlify dev"
},
"devDependencies": {
"@changesets/cli": "^2.27.12",
@@ -338,6 +339,7 @@
"eslint": "^8.57.0",
"grpc-tools": "^1.13.0",
"husky": "^9.1.7",
"mintlify": "^4.0.515",
"npm-run-all": "^4.1.5",
"prettier": "^3.3.3",
"protoc-gen-ts": "^0.8.7",
+4 -1
View File
@@ -2,6 +2,7 @@
import * as fs from "fs/promises"
import * as path from "path"
import { fileURLToPath } from "url"
import { execSync } from "child_process"
import { globby } from "globby"
import chalk from "chalk"
@@ -12,7 +13,8 @@ const protoc = path.join(require.resolve("grpc-tools"), "../bin/protoc")
const tsProtoPlugin = require.resolve("ts-proto/protoc-gen-ts_proto")
// Get script directory and root directory
const SCRIPT_DIR = path.dirname(new URL(import.meta.url).pathname)
const __filename = fileURLToPath(import.meta.url)
const SCRIPT_DIR = path.dirname(__filename)
const ROOT_DIR = path.resolve(SCRIPT_DIR, "..")
async function main() {
@@ -83,6 +85,7 @@ async function generateMethodRegistrations() {
path.join(ROOT_DIR, "src", "core", "controller", "file"),
path.join(ROOT_DIR, "src", "core", "controller", "mcp"),
path.join(ROOT_DIR, "src", "core", "controller", "task"),
path.join(ROOT_DIR, "src", "core", "controller", "web-content"),
// Add more service directories here as needed
]
+22
View File
@@ -13,4 +13,26 @@ service FileService {
// Opens an image in the system viewer
rpc openImage(StringRequest) returns (Empty);
// Deletes a rule file from either global or workspace rules directory
rpc deleteRuleFile(RuleFileRequest) returns (RuleFile);
// Creates a rule file from either global or workspace rules directory
rpc createRuleFile(RuleFileRequest) returns (RuleFile);
}
// Unified request for all rule file operations
message RuleFileRequest {
Metadata metadata = 1;
bool is_global = 2; // Common field for all operations
optional string rule_path = 3; // Path field for deleteRuleFile (optional)
optional string filename = 4; // Filename field for createRuleFile (optional)
}
// Result for rule file operations with meaningful data only
message RuleFile {
string file_path = 1; // Path to the rule file
string display_name = 2; // Filename for display purposes
bool already_exists = 3; // For createRuleFile, indicates if file already existed
}
+16
View File
@@ -0,0 +1,16 @@
syntax = "proto3";
package cline;
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
service WebContentService {
rpc checkIsImageUrl(StringRequest) returns (IsImageUrl);
}
message IsImageUrl {
bool is_image = 1;
string url = 2;
}
+61
View File
@@ -0,0 +1,61 @@
const { execSync } = require("child_process")
const esbuild = require("esbuild")
const watch = process.argv.includes("--watch")
/**
* @type {import('esbuild').Plugin}
*/
const esbuildProblemMatcherPlugin = {
name: "esbuild-problem-matcher",
setup(build) {
build.onStart(() => {
console.log("[watch] build started")
})
build.onEnd((result) => {
result.errors.forEach(({ text, location }) => {
console.error(`✘ [ERROR] ${text}`)
console.error(` ${location.file}:${location.line}:${location.column}:`)
})
console.log("[watch] build finished")
})
},
}
const srcConfig = {
bundle: true,
minify: false,
sourcemap: true,
sourcesContent: true,
logLevel: "silent",
entryPoints: ["src/packages/**/*.ts"],
outdir: "out/packages",
format: "cjs",
platform: "node",
define: {
"process.env.IS_DEV": "true",
"process.env.IS_TEST": "true",
},
external: ["vscode"],
plugins: [esbuildProblemMatcherPlugin],
}
async function main() {
const srcCtx = await esbuild.context(srcConfig)
if (watch) {
await srcCtx.watch()
} else {
await srcCtx.rebuild()
await srcCtx.dispose()
}
}
execSync("tsc -p ./tsconfig.test.json --outDir out", { encoding: "utf-8" })
main().catch((e) => {
console.error(e)
process.exit(1)
})
+24 -7
View File
@@ -28,20 +28,26 @@ export class AwsBedrockHandler implements ApiHandler {
const modelId = await this.getModelId()
const model = this.getModel()
// This baseModelId is used to indicate the capabilities of the model.
// If the user selects a custom model, baseModelId will be set to the base model ID of the custom model.
// Otherwise, baseModelId will be the same as modelId.
const baseModelId =
(this.options.awsBedrockCustomSelected ? this.options.awsBedrockCustomModelBaseId : modelId) || modelId
// Check if this is an Amazon Nova model
if (modelId.includes("amazon.nova")) {
if (baseModelId.includes("amazon.nova")) {
yield* this.createNovaMessage(systemPrompt, messages, modelId, model)
return
}
// Check if this is a Deepseek model
if (modelId.includes("deepseek")) {
if (baseModelId.includes("deepseek")) {
yield* this.createDeepseekMessage(systemPrompt, messages, modelId, model)
return
}
const budget_tokens = this.options.thinkingBudgetTokens || 0
const reasoningOn = modelId.includes("3-7") && budget_tokens !== 0 ? true : false
const reasoningOn = baseModelId.includes("3-7") && budget_tokens !== 0 ? true : false
// Get model info and message indices for caching
const userMsgIndices = messages.reduce((acc, msg, index) => (msg.role === "user" ? [...acc, index] : acc), [] as number[])
@@ -167,12 +173,23 @@ export class AwsBedrockHandler implements ApiHandler {
}
}
getModel(): { id: BedrockModelId; info: ModelInfo } {
getModel(): { id: string; info: ModelInfo } {
const modelId = this.options.apiModelId
if (modelId && modelId in bedrockModels) {
const id = modelId as BedrockModelId
return { id, info: bedrockModels[id] }
}
const customSelected = this.options.awsBedrockCustomSelected
const baseModel = this.options.awsBedrockCustomModelBaseId
if (customSelected && modelId && baseModel && baseModel in bedrockModels) {
// Use the user-input model ID but inherit capabilities from the base model
return {
id: modelId,
info: bedrockModels[baseModel],
}
}
return {
id: bedrockDefaultModelId,
info: bedrockModels[bedrockDefaultModelId],
@@ -290,7 +307,7 @@ export class AwsBedrockHandler implements ApiHandler {
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
modelId: string,
model: { id: BedrockModelId; info: ModelInfo },
model: { id: string; info: ModelInfo },
): ApiStream {
// Get Bedrock client with proper credentials
const client = await this.getBedrockClient()
@@ -476,13 +493,13 @@ export class AwsBedrockHandler implements ApiHandler {
/**
* Creates a message using Amazon Nova models through AWS Bedrock
* Implements support for Nova Micro, Nova Lite, and Nova Pro models
* Implements support for Amazon Nova models
*/
private async *createNovaMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
modelId: string,
model: { id: BedrockModelId; info: ModelInfo },
model: { id: string; info: ModelInfo },
): ApiStream {
// Get Bedrock client with proper credentials
const client = await this.getBedrockClient()
@@ -76,26 +76,6 @@ describe("ModelContextTracker", () => {
}
})
it("should throw an error when controller is dereferenced", async () => {
// Create a new tracker with a controller that will be garbage collected
const weakTracker = new ModelContextTracker(mockContext, taskId)
// Force the WeakRef to return null by overriding the deref method
const weakRef = { deref: sandbox.stub().returns(null) }
sandbox.stub(WeakRef.prototype, "deref").callsFake(() => weakRef.deref())
try {
// Try to call the method - this should throw
await weakTracker.recordModelUsage("any-provider", "any-model", "any-mode")
// If we get here, the test should fail
expect.fail("Expected an error to be thrown")
} catch (error) {
// Verify the error message
expect(error.message).to.equal("Unable to access extension context")
}
})
it("should append model usage to existing entries", async () => {
// Add an existing model usage entry
const existingTimestamp = 1617200000000
@@ -221,7 +221,14 @@ export const createRuleFile = async (isGlobal: boolean, filename: string, cwd: s
filePath = path.join(globalClineRulesFilePath, filename)
} else {
const localClineRulesFilePath = path.resolve(cwd, GlobalFileNames.clineRules)
const hasError = await ensureLocalClinerulesDirExists(cwd)
if (hasError === true) {
return { filePath: null, fileExists: false }
}
await fs.mkdir(localClineRulesFilePath, { recursive: true })
filePath = path.join(localClineRulesFilePath, filename)
}
@@ -0,0 +1,55 @@
import { Controller } from ".."
import { RuleFileRequest, RuleFile } from "@shared/proto/file"
import { FileMethodHandler } from "./index"
import {
createRuleFile as createRuleFileImpl,
refreshClineRulesToggles,
} from "@core/context/instructions/user-instructions/cline-rules"
import * as vscode from "vscode"
import * as path from "path"
import { handleFileServiceRequest } from "./index"
import { cwd } from "@core/task"
/**
* Creates a rule file in either global or workspace rules directory
* @param controller The controller instance
* @param request The request containing filename and isGlobal flag
* @returns Result with file path and display name
* @throws Error if operation fails
*/
export const createRuleFile: FileMethodHandler = async (controller: Controller, request: RuleFileRequest): Promise<RuleFile> => {
if (typeof request.isGlobal !== "boolean" || typeof request.filename !== "string" || !request.filename) {
console.error("createRuleFile: Missing or invalid parameters", {
isGlobal: typeof request.isGlobal === "boolean" ? request.isGlobal : `Invalid: ${typeof request.isGlobal}`,
filename: typeof request.filename === "string" ? request.filename : `Invalid: ${typeof request.filename}`,
})
throw new Error("Missing or invalid parameters")
}
const { filePath, fileExists } = await createRuleFileImpl(request.isGlobal, request.filename, cwd)
if (!filePath) {
throw new Error("Failed to create rule file.")
}
if (fileExists) {
vscode.window.showWarningMessage(`Rule file "${request.filename}" already exists.`)
// Still open it for editing
await handleFileServiceRequest(controller, "openFile", { value: filePath })
} else {
await refreshClineRulesToggles(controller.context, cwd)
await controller.postStateToWebview()
await handleFileServiceRequest(controller, "openFile", { value: filePath })
vscode.window.showInformationMessage(
`Created new ${request.isGlobal ? "global" : "workspace"} rule file: ${request.filename}`,
)
}
return RuleFile.create({
filePath: filePath,
displayName: path.basename(filePath),
alreadyExists: fileExists,
})
}
@@ -0,0 +1,45 @@
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 * as vscode from "vscode"
import * as path from "path"
import { cwd } from "@core/task"
/**
* Deletes a rule file from either global or workspace rules directory
* @param controller The controller instance
* @param request The request containing rule path and isGlobal flag
* @returns Result with file path and display name
* @throws Error if operation fails
*/
export const deleteRuleFile: FileMethodHandler = async (controller: Controller, request: RuleFileRequest): Promise<RuleFile> => {
if (typeof request.isGlobal !== "boolean" || typeof request.rulePath !== "string" || !request.rulePath) {
console.error("deleteRuleFile: Missing or invalid parameters", {
isGlobal: typeof request.isGlobal === "boolean" ? request.isGlobal : `Invalid: ${typeof request.isGlobal}`,
rulePath: typeof request.rulePath === "string" ? request.rulePath : `Invalid: ${typeof request.rulePath}`,
})
throw new Error("Missing or invalid parameters")
}
const result = await deleteRuleFileImpl(controller.context, request.rulePath, request.isGlobal)
if (!result.success) {
throw new Error(result.message || "Failed to delete rule file")
}
await refreshClineRulesToggles(controller.context, cwd)
await controller.postStateToWebview()
const fileName = path.basename(request.rulePath)
vscode.window.showInformationMessage(`Rule file "${fileName}" deleted successfully`)
return RuleFile.create({
filePath: request.rulePath,
displayName: fileName,
alreadyExists: false,
})
}
+4
View File
@@ -3,12 +3,16 @@
// Import all method implementations
import { registerMethod } from "./index"
import { createRuleFile } from "./createRuleFile"
import { deleteRuleFile } from "./deleteRuleFile"
import { openFile } from "./openFile"
import { openImage } from "./openImage"
// Register all file service methods
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("createRuleFile", createRuleFile)
registerMethod("deleteRuleFile", deleteRuleFile)
registerMethod("openFile", openFile)
registerMethod("openImage", openImage)
}
+6
View File
@@ -5,6 +5,7 @@ import { handleFileServiceRequest } from "./file"
import { handleTaskServiceRequest } from "./task"
import { handleCheckpointsServiceRequest } from "./checkpoints"
import { handleMcpServiceRequest } from "./mcp"
import { handleWebContentServiceRequest } from "./web-content"
/**
* Handles gRPC requests from the webview
@@ -62,6 +63,11 @@ export class GrpcHandler {
message: await handleMcpServiceRequest(this.controller, method, message),
request_id: requestId,
}
case "cline.WebContentService":
return {
message: await handleWebContentServiceRequest(this.controller, method, message),
request_id: requestId,
}
default:
throw new Error(`Unknown service: ${service}`)
}
+51 -80
View File
@@ -11,7 +11,7 @@ import { handleGrpcRequest } from "./grpc-handler"
import { buildApiHandler } from "@api/index"
import { cleanupLegacyCheckpoints } from "@integrations/checkpoints/CheckpointMigration"
import { downloadTask } from "@integrations/misc/export-markdown"
import { fetchOpenGraphData, isImageUrl } from "@integrations/misc/link-preview"
import { fetchOpenGraphData } from "@integrations/misc/link-preview"
import { handleFileServiceRequest } from "./file"
import { selectImages } from "@integrations/misc/process-images"
import { getTheme } from "@integrations/theme/getTheme"
@@ -48,7 +48,7 @@ import {
} from "../storage/state"
import { Task, cwd } from "../task"
import { ClineRulesToggles } from "@shared/cline-rules"
import { createRuleFile, deleteRuleFile, refreshClineRulesToggles } from "../context/instructions/user-instructions/cline-rules"
import { createRuleFile, refreshClineRulesToggles } from "../context/instructions/user-instructions/cline-rules"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -136,8 +136,14 @@ export class Controller {
async initTask(task?: string, images?: string[], historyItem?: HistoryItem) {
await this.clearTask() // ensures that an existing task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one
const { apiConfiguration, customInstructions, autoApprovalSettings, browserSettings, chatSettings } =
await getAllExtensionState(this.context)
const {
apiConfiguration,
customInstructions,
autoApprovalSettings,
browserSettings,
chatSettings,
shellIntegrationTimeout,
} = await getAllExtensionState(this.context)
if (autoApprovalSettings) {
const updatedAutoApprovalSettings = {
@@ -159,6 +165,7 @@ export class Controller {
autoApprovalSettings,
browserSettings,
chatSettings,
shellIntegrationTimeout,
customInstructions,
task,
images,
@@ -377,39 +384,6 @@ export class Controller {
break
case "fetchOpenGraphData":
this.fetchOpenGraphData(message.text!)
break
case "checkIsImageUrl":
this.checkIsImageUrl(message.text!)
break
case "createRuleFile":
if (typeof message.isGlobal !== "boolean" || typeof message.filename !== "string" || !message.filename) {
console.error("createRuleFile: Missing or invalid parameters", {
isGlobal:
typeof message.isGlobal === "boolean" ? message.isGlobal : `Invalid: ${typeof message.isGlobal}`,
filename: typeof message.filename === "string" ? message.filename : `Invalid: ${typeof message.filename}`,
})
return
}
const { filePath, fileExists } = await createRuleFile(message.isGlobal, message.filename, cwd)
if (fileExists && filePath) {
vscode.window.showWarningMessage(`Rule file "${message.filename}" already exists.`)
// Still open it for editing
await handleFileServiceRequest(this, "openFile", { value: filePath })
return
} else if (filePath && !fileExists) {
await refreshClineRulesToggles(this.context, cwd)
await this.postStateToWebview()
await handleFileServiceRequest(this, "openFile", { value: filePath })
vscode.window.showInformationMessage(
`Created new ${message.isGlobal ? "global" : "workspace"} rule file: ${message.filename}`,
)
} else {
// null filePath
vscode.window.showErrorMessage(`Failed to create rule file.`)
}
break
case "openMention":
openMention(message.text)
@@ -542,24 +516,6 @@ export class Controller {
}
break
}
case "deleteClineRule": {
const { isGlobal, rulePath } = message
if (rulePath && typeof isGlobal === "boolean") {
const result = await deleteRuleFile(this.context, rulePath, isGlobal)
if (result.success) {
await refreshClineRulesToggles(this.context, cwd)
await this.postStateToWebview()
} else {
console.error("Failed to delete rule file:", result.message)
}
} else {
console.error("deleteClineRule: Missing or invalid parameters", {
rulePath,
isGlobal: typeof isGlobal === "boolean" ? isGlobal : `Invalid: ${typeof isGlobal}`,
})
}
break
}
case "requestTotalTasksSize": {
this.refreshTotalTasksSize()
break
@@ -784,6 +740,21 @@ export class Controller {
}
break
}
case "updateTerminalConnectionTimeout": {
if (message.shellIntegrationTimeout !== undefined) {
const timeout = message.shellIntegrationTimeout
if (typeof timeout === "number" && !isNaN(timeout) && timeout > 0) {
await updateGlobalState(this.context, "shellIntegrationTimeout", timeout)
await this.postStateToWebview()
} else {
console.warn(
`Invalid shell integration timeout value received: ${timeout}. ` + `Expected a positive number.`,
)
}
}
break
}
// Add more switch case statements here as more webview message commands
// are created within the webview context (i.e. inside media/main.js)
}
@@ -810,6 +781,8 @@ export class Controller {
previousModeVsCodeLmModelSelector: newVsCodeLmModelSelector,
previousModeThinkingBudgetTokens: newThinkingBudgetTokens,
previousModeReasoningEffort: newReasoningEffort,
previousModeAwsBedrockCustomSelected: newAwsBedrockCustomSelected,
previousModeAwsBedrockCustomModelBaseId: newAwsBedrockCustomModelBaseId,
planActSeparateModelsSetting,
} = await getAllExtensionState(this.context)
@@ -822,7 +795,6 @@ export class Controller {
await updateGlobalState(this.context, "previousModeReasoningEffort", apiConfiguration.reasoningEffort)
switch (apiConfiguration.apiProvider) {
case "anthropic":
case "bedrock":
case "vertex":
case "gemini":
case "asksage":
@@ -832,6 +804,19 @@ export class Controller {
case "xai":
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.apiModelId)
break
case "bedrock":
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.apiModelId)
await updateGlobalState(
this.context,
"previousModeAwsBedrockCustomSelected",
apiConfiguration.awsBedrockCustomSelected,
)
await updateGlobalState(
this.context,
"previousModeAwsBedrockCustomModelBaseId",
apiConfiguration.awsBedrockCustomModelBaseId,
)
break
case "openrouter":
case "cline":
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.openRouterModelId)
@@ -877,7 +862,6 @@ export class Controller {
await updateGlobalState(this.context, "reasoningEffort", newReasoningEffort)
switch (newApiProvider) {
case "anthropic":
case "bedrock":
case "vertex":
case "gemini":
case "asksage":
@@ -887,6 +871,11 @@ export class Controller {
case "xai":
await updateGlobalState(this.context, "apiModelId", newModelId)
break
case "bedrock":
await updateGlobalState(this.context, "apiModelId", newModelId)
await updateGlobalState(this.context, "awsBedrockCustomSelected", newAwsBedrockCustomSelected)
await updateGlobalState(this.context, "awsBedrockCustomModelBaseId", newAwsBedrockCustomModelBaseId)
break
case "openrouter":
case "cline":
await updateGlobalState(this.context, "openRouterModelId", newModelId)
@@ -1769,6 +1758,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
telemetrySetting,
planActSeparateModelsSetting,
globalClineRulesToggles,
shellIntegrationTimeout,
} = await getAllExtensionState(this.context)
const localClineRulesToggles =
@@ -1798,10 +1788,14 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
vscMachineId: vscode.env.machineId,
globalClineRulesToggles: globalClineRulesToggles || {},
localClineRulesToggles: localClineRulesToggles || {},
shellIntegrationTimeout,
}
}
async clearTask() {
if (this.task) {
await telemetryService.sendCollectedEvents(this.task.taskId)
}
this.task?.abortTask()
this.task = undefined // removes reference to it, so once promises end it will be garbage collected
}
@@ -1894,29 +1888,6 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
}
}
// Check if a URL is an image
async checkIsImageUrl(url: string) {
try {
// Check if the URL is an image
const isImage = await isImageUrl(url)
// Send the result back to the webview
await this.postMessageToWebview({
type: "isImageUrlResult",
isImage,
url,
})
} catch (error) {
console.error(`Error checking if URL is an image: ${url}`, error)
// Send an error response
await this.postMessageToWebview({
type: "isImageUrlResult",
isImage: false,
url,
})
}
}
// dev
async resetState() {
+1
View File
@@ -8,6 +8,7 @@ import { Empty, EmptyRequest } from "../../../shared/proto/common"
* @returns Empty response
*/
export async function clearTask(controller: Controller, _request: EmptyRequest): Promise<Empty> {
// clearTask is called here when the user closes the task
await controller.clearTask()
await controller.postStateToWebview()
return Empty.create()
@@ -0,0 +1,29 @@
import { Controller } from "../index"
import { StringRequest } from "../../../shared/proto/common"
import { IsImageUrl } from "../../../shared/proto/web_content"
import { detectImageUrl } from "@integrations/misc/link-preview"
/**
* Checks if a URL is an image URL
* @param controller The controller instance
* @param request The request containing the URL to check
* @returns A result indicating if the URL is an image and the URL that was checked
*/
export async function checkIsImageUrl(controller: Controller, request: StringRequest): Promise<IsImageUrl> {
try {
const url = request.value || ""
// Check if the URL is an image
const isImage = await detectImageUrl(url)
return {
isImage,
url,
}
} catch (error) {
console.error(`Error checking if URL is an image: ${request.value}`, error)
return {
isImage: false,
url: request.value || "",
}
}
}
+15
View File
@@ -0,0 +1,15 @@
import { createServiceRegistry, ServiceMethodHandler } from "../grpc-service"
import { registerAllMethods } from "./methods"
// Create web content service registry
const webContentService = createServiceRegistry("web-content")
// Export the method handler type and registration function
export type WebContentMethodHandler = ServiceMethodHandler
export const registerMethod = webContentService.registerMethod
// Export the request handler
export const handleWebContentServiceRequest = webContentService.handleRequest
// Register all web content methods
registerAllMethods()
@@ -0,0 +1,12 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Import all method implementations
import { registerMethod } from "./index"
import { checkIsImageUrl } from "./checkIsImageUrl"
// Register all web-content service methods
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("checkIsImageUrl", checkIsImageUrl)
}
+1 -1
View File
@@ -6,7 +6,7 @@ import { fileExistsAtPath } from "@utils/fs"
import { ClineMessage } from "@shared/ExtensionMessage"
import { TaskMetadata } from "@core/context/context-tracking/ContextTrackerTypes"
import os from "os"
import { execa } from "execa"
import { execa } from "@packages/execa"
export const GlobalFileNames = {
apiConversationHistory: "api_conversation_history.json",
+5
View File
@@ -29,6 +29,8 @@ export type GlobalStateKey =
| "awsBedrockEndpoint"
| "awsProfile"
| "awsUseProfile"
| "awsBedrockCustomSelected"
| "awsBedrockCustomModelBaseId"
| "vertexProjectId"
| "vertexRegion"
| "lastShownAnnouncementId"
@@ -60,6 +62,8 @@ export type GlobalStateKey =
| "previousModeThinkingBudgetTokens"
| "previousModeReasoningEffort"
| "previousModeVsCodeLmModelSelector"
| "previousModeAwsBedrockCustomSelected"
| "previousModeAwsBedrockCustomModelBaseId"
| "previousModeModelInfo"
| "liteLlmBaseUrl"
| "liteLlmModelId"
@@ -76,5 +80,6 @@ export type GlobalStateKey =
| "planActSeparateModelsSetting"
| "favoritedModelIds"
| "requestTimeoutMs"
| "shellIntegrationTimeout"
export type LocalStateKey = "localClineRulesToggles"
+20 -1
View File
@@ -3,7 +3,7 @@ import { DEFAULT_CHAT_SETTINGS } from "@shared/ChatSettings"
import { DEFAULT_BROWSER_SETTINGS } from "@shared/BrowserSettings"
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "@shared/AutoApprovalSettings"
import { GlobalStateKey, SecretKey } from "./state-keys"
import { ApiConfiguration, ApiProvider, ModelInfo } from "@shared/api"
import { ApiConfiguration, ApiProvider, BedrockModelId, ModelInfo } from "@shared/api"
import { HistoryItem } from "@shared/HistoryItem"
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
import { BrowserSettings } from "@shared/BrowserSettings"
@@ -67,6 +67,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
awsBedrockEndpoint,
awsProfile,
awsUseProfile,
awsBedrockCustomSelected,
awsBedrockCustomModelBaseId,
vertexProjectId,
vertexRegion,
openAiBaseUrl,
@@ -113,6 +115,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
previousModeVsCodeLmModelSelector,
previousModeThinkingBudgetTokens,
previousModeReasoningEffort,
previousModeAwsBedrockCustomSelected,
previousModeAwsBedrockCustomModelBaseId,
qwenApiLine,
liteLlmApiKey,
telemetrySetting,
@@ -126,6 +130,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
favoritedModelIds,
globalClineRulesToggles,
requestTimeoutMs,
shellIntegrationTimeout,
] = await Promise.all([
getGlobalState(context, "apiProvider") as Promise<ApiProvider | undefined>,
getGlobalState(context, "apiModelId") as Promise<string | undefined>,
@@ -141,6 +146,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
getGlobalState(context, "awsBedrockEndpoint") as Promise<string | undefined>,
getGlobalState(context, "awsProfile") as Promise<string | undefined>,
getGlobalState(context, "awsUseProfile") as Promise<boolean | undefined>,
getGlobalState(context, "awsBedrockCustomSelected") as Promise<boolean | undefined>,
getGlobalState(context, "awsBedrockCustomModelBaseId") as Promise<BedrockModelId | undefined>,
getGlobalState(context, "vertexProjectId") as Promise<string | undefined>,
getGlobalState(context, "vertexRegion") as Promise<string | undefined>,
getGlobalState(context, "openAiBaseUrl") as Promise<string | undefined>,
@@ -187,6 +194,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
getGlobalState(context, "previousModeVsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
getGlobalState(context, "previousModeThinkingBudgetTokens") as Promise<number | undefined>,
getGlobalState(context, "previousModeReasoningEffort") as Promise<string | undefined>,
getGlobalState(context, "previousModeAwsBedrockCustomSelected") as Promise<boolean | undefined>,
getGlobalState(context, "previousModeAwsBedrockCustomModelBaseId") as Promise<BedrockModelId | undefined>,
getGlobalState(context, "qwenApiLine") as Promise<string | undefined>,
getSecret(context, "liteLlmApiKey") as Promise<string | undefined>,
getGlobalState(context, "telemetrySetting") as Promise<TelemetrySetting | undefined>,
@@ -200,6 +209,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
getGlobalState(context, "favoritedModelIds") as Promise<string[] | undefined>,
getGlobalState(context, "globalClineRulesToggles") as Promise<ClineRulesToggles | undefined>,
getGlobalState(context, "requestTimeoutMs") as Promise<number | undefined>,
getGlobalState(context, "shellIntegrationTimeout") as Promise<number | undefined>,
])
let apiProvider: ApiProvider
@@ -256,6 +266,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
awsBedrockEndpoint,
awsProfile,
awsUseProfile,
awsBedrockCustomSelected,
awsBedrockCustomModelBaseId,
vertexProjectId,
vertexRegion,
openAiBaseUrl,
@@ -316,9 +328,12 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
previousModeVsCodeLmModelSelector,
previousModeThinkingBudgetTokens,
previousModeReasoningEffort,
previousModeAwsBedrockCustomSelected,
previousModeAwsBedrockCustomModelBaseId,
mcpMarketplaceEnabled,
telemetrySetting: telemetrySetting || "unset",
planActSeparateModelsSetting,
shellIntegrationTimeout: shellIntegrationTimeout || 4000,
}
}
@@ -337,6 +352,8 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
awsBedrockEndpoint,
awsProfile,
awsUseProfile,
awsBedrockCustomSelected,
awsBedrockCustomModelBaseId,
vertexProjectId,
vertexRegion,
openAiBaseUrl,
@@ -394,6 +411,8 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
await updateGlobalState(context, "awsBedrockEndpoint", awsBedrockEndpoint)
await updateGlobalState(context, "awsProfile", awsProfile)
await updateGlobalState(context, "awsUseProfile", awsUseProfile)
await updateGlobalState(context, "awsBedrockCustomSelected", awsBedrockCustomSelected)
await updateGlobalState(context, "awsBedrockCustomModelBaseId", awsBedrockCustomModelBaseId)
await updateGlobalState(context, "vertexProjectId", vertexProjectId)
await updateGlobalState(context, "vertexRegion", vertexRegion)
await updateGlobalState(context, "openAiBaseUrl", openAiBaseUrl)
+17 -3
View File
@@ -173,6 +173,7 @@ export class Task {
autoApprovalSettings: AutoApprovalSettings,
browserSettings: BrowserSettings,
chatSettings: ChatSettings,
shellIntegrationTimeout: number,
customInstructions?: string,
task?: string,
images?: string[],
@@ -191,6 +192,7 @@ export class Task {
console.error("Failed to initialize ClineIgnoreController:", error)
})
this.terminalManager = new TerminalManager()
this.terminalManager.setShellIntegrationTimeout(shellIntegrationTimeout)
this.urlContentFetcher = new UrlContentFetcher(context)
this.browserSession = new BrowserSession(context, browserSettings)
this.contextManager = new ContextManager()
@@ -3541,7 +3543,7 @@ export class Task {
content: userContent,
})
telemetryService.captureConversationTurnEvent(this.taskId, currentProviderId, this.api.getModel().id, "user")
telemetryService.captureConversationTurnEvent(this.taskId, currentProviderId, this.api.getModel().id, "user", true)
// since we sent off a placeholder api_req_started message to update the webview while waiting to actually start the API request (to load potential details for example), we need to update the text of that message
const lastApiReqIndex = findLastIndex(this.clineMessages, (m) => m.say === "api_req_started")
@@ -3618,7 +3620,13 @@ export class Task {
updateApiReqMsg(cancelReason, streamingFailedMessage)
await this.saveClineMessagesAndUpdateHistory()
telemetryService.captureConversationTurnEvent(this.taskId, currentProviderId, this.api.getModel().id, "assistant")
telemetryService.captureConversationTurnEvent(
this.taskId,
currentProviderId,
this.api.getModel().id,
"assistant",
true,
)
// signals to provider that it can retrieve the saved messages from disk, as abortTask can not be awaited on in nature
this.didFinishAbortingStream = true
@@ -3761,7 +3769,13 @@ export class Task {
// need to save assistant responses to file before proceeding to tool use since user can exit at any moment and we wouldn't be able to save the assistant's response
let didEndLoop = false
if (assistantMessage.length > 0) {
telemetryService.captureConversationTurnEvent(this.taskId, currentProviderId, this.api.getModel().id, "assistant")
telemetryService.captureConversationTurnEvent(
this.taskId,
currentProviderId,
this.api.getModel().id,
"assistant",
true,
)
await this.addToApiConversationHistory({
role: "assistant",
+4 -3
View File
@@ -434,11 +434,12 @@ export function activate(context: vscode.ExtensionContext) {
const { IS_DEV, DEV_WORKSPACE_FOLDER } = process.env
// This method is called when your extension is deactivated
export function deactivate() {
export async function deactivate() {
await telemetryService.sendCollectedEvents()
// Clean up test mode
cleanupTestMode()
telemetryService.shutdown()
await telemetryService.shutdown()
Logger.log("Cline extension deactivated")
}
+1 -1
View File
@@ -87,7 +87,7 @@ export async function fetchOpenGraphData(url: string): Promise<OpenGraphData> {
* @param url The URL to check
* @returns Promise resolving to boolean indicating if the URL is an image
*/
export async function isImageUrl(url: string): Promise<boolean> {
export async function detectImageUrl(url: string): Promise<boolean> {
try {
const response = await axios.head(url, {
headers: {
+29 -7
View File
@@ -39,7 +39,7 @@ const terminalManager = new TerminalManager(context);
const process = terminalManager.runCommand('npm install', '/path/to/project');
process.on('line', (line) => {
console.log(line);
console.log(line);
});
// To wait for the process to complete naturally:
@@ -93,6 +93,7 @@ export class TerminalManager {
private terminalIds: Set<number> = new Set()
private processes: Map<number, TerminalProcess> = new Map()
private disposables: vscode.Disposable[] = []
private shellIntegrationTimeout: number = 4000
constructor() {
let disposable: vscode.Disposable | undefined
@@ -144,13 +145,30 @@ export class TerminalManager {
process.run(terminalInfo.terminal, command)
} else {
// docs recommend waiting 3s for shell integration to activate
pWaitFor(() => terminalInfo.terminal.shellIntegration !== undefined, { timeout: 4000 }).finally(() => {
const existingProcess = this.processes.get(terminalInfo.id)
if (existingProcess && existingProcess.waitForShellIntegration) {
existingProcess.waitForShellIntegration = false
existingProcess.run(terminalInfo.terminal, command)
}
console.log(
`[TerminalManager Test] Waiting for shell integration for terminal ${terminalInfo.id} with timeout ${this.shellIntegrationTimeout}ms`,
)
pWaitFor(() => terminalInfo.terminal.shellIntegration !== undefined, {
timeout: this.shellIntegrationTimeout,
})
.then(() => {
console.log(
`[TerminalManager Test] Shell integration activated for terminal ${terminalInfo.id} within timeout.`,
)
})
.catch((err) => {
console.warn(
`[TerminalManager Test] Shell integration timed out or failed for terminal ${terminalInfo.id}: ${err.message}`,
)
})
.finally(() => {
console.log(`[TerminalManager Test] Proceeding with command execution for terminal ${terminalInfo.id}.`)
const existingProcess = this.processes.get(terminalInfo.id)
if (existingProcess && existingProcess.waitForShellIntegration) {
existingProcess.waitForShellIntegration = false
existingProcess.run(terminalInfo.terminal, command)
}
})
}
return mergePromise(process, promise)
@@ -219,4 +237,8 @@ export class TerminalManager {
this.disposables.forEach((disposable) => disposable.dispose())
this.disposables = []
}
setShellIntegrationTimeout(timeout: number): void {
this.shellIntegrationTimeout = timeout
}
}
+1
View File
@@ -0,0 +1 @@
export { execa } from "execa"
+349 -191
View File
@@ -10,7 +10,20 @@ import type { BrowserSettings } from "@shared/BrowserSettings"
* Uses PostHog analytics to track user interactions and system events
* Respects user privacy settings and VSCode's global telemetry configuration
*/
interface CollectedTasks {
taskId: string
collection: Collection[]
}
interface Collection {
event: string
properties: any
}
class PostHogClient {
// Stores events when collect=true
private collectedTasks: CollectedTasks[] = []
// Event constants for tracking user interactions and system events
private static readonly EVENTS = {
// Task-related events for tracking conversation and execution flow
@@ -49,6 +62,8 @@ class PostHogClient {
BROWSER_TOOL_END: "task.browser_tool_end",
// Tracks when browser errors occur
BROWSER_ERROR: "task.browser_error",
// Collection of all task events
TASK_COLLECTION: "task.collection",
},
// UI interaction events for tracking user engagement
UI: {
@@ -87,6 +102,8 @@ class PostHogClient {
private telemetryEnabled: boolean = false
/** Current version of the extension */
private readonly version: string = extensionVersion
/** Whether the extension is running in development mode */
private readonly isDev = process.env.IS_DEV
/**
* Private constructor to enforce singleton pattern
@@ -136,17 +153,36 @@ class PostHogClient {
}
/**
* Captures a telemetry event if telemetry is enabled
* Captures a telemetry event if telemetry is enabled or collects if collect=true
* @param event The event to capture with its properties
* @param collect If true, store the event in collectedEvents instead of sending to PostHog
*/
public capture(event: { event: string; properties?: any }): void {
// Only send events if telemetry is enabled
if (this.telemetryEnabled) {
// Include extension version in all event properties
const propertiesWithVersion = {
...event.properties,
extension_version: this.version,
public capture(event: { event: string; properties?: any }, collect: boolean = false): void {
const taskId = event.properties.taskId
const propertiesWithVersion = {
...event.properties,
extension_version: this.version,
is_dev: this.isDev,
}
if (collect) {
const existingTask = this.collectedTasks.find((task) => task.taskId === taskId)
if (existingTask) {
existingTask.collection.push({
event: event.event,
properties: propertiesWithVersion,
})
} else {
this.collectedTasks.push({
taskId,
collection: [
{
event: event.event,
properties: propertiesWithVersion,
},
],
})
}
} else if (this.telemetryEnabled) {
this.client.capture({ distinctId: this.distinctId, event: event.event, properties: propertiesWithVersion })
}
}
@@ -155,34 +191,48 @@ class PostHogClient {
/**
* Records when a new task/conversation is started
* @param taskId Unique identifier for the new task
* @param apiProvider Optional API provider
* @param collect If true, collect event instead of sending
*/
public captureTaskCreated(taskId: string, apiProvider?: string) {
this.capture({
event: PostHogClient.EVENTS.TASK.CREATED,
properties: { taskId, apiProvider },
})
public captureTaskCreated(taskId: string, apiProvider?: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.CREATED,
properties: { taskId, apiProvider },
},
collect,
)
}
/**
* Records when a task/conversation is restarted
* @param taskId Unique identifier for the new task
* @param apiProvider Optional API provider
* @param collect If true, collect event instead of sending
*/
public captureTaskRestarted(taskId: string, apiProvider?: string) {
this.capture({
event: PostHogClient.EVENTS.TASK.RESTARTED,
properties: { taskId, apiProvider },
})
public captureTaskRestarted(taskId: string, apiProvider?: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.RESTARTED,
properties: { taskId, apiProvider },
},
collect,
)
}
/**
* Records when cline calls the task completion_result tool signifying that cline is done with the task
* @param taskId Unique identifier for the task
* @param collect If true, collect event instead of sending
*/
public captureTaskCompleted(taskId: string) {
this.capture({
event: PostHogClient.EVENTS.TASK.COMPLETED,
properties: { taskId },
})
public captureTaskCompleted(taskId: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.COMPLETED,
properties: { taskId },
},
collect,
)
}
/**
@@ -197,6 +247,7 @@ class PostHogClient {
provider: string = "unknown",
model: string = "unknown",
source: "user" | "assistant",
collect: boolean = false,
) {
// Ensure required parameters are provided
if (!taskId || !provider || !model || !source) {
@@ -212,10 +263,13 @@ class PostHogClient {
timestamp: new Date().toISOString(), // Add timestamp for message sequencing
}
this.capture({
event: PostHogClient.EVENTS.TASK.CONVERSATION_TURN,
properties,
})
this.capture(
{
event: PostHogClient.EVENTS.TASK.CONVERSATION_TURN,
properties,
},
collect,
)
}
/**
@@ -226,16 +280,19 @@ class PostHogClient {
* @param tokensOut Number of output tokens generated
* @param model The model used for token calculation
*/
public captureTokenUsage(taskId: string, tokensIn: number, tokensOut: number, model: string) {
this.capture({
event: PostHogClient.EVENTS.TASK.TOKEN_USAGE,
properties: {
taskId,
tokensIn,
tokensOut,
model,
public captureTokenUsage(taskId: string, tokensIn: number, tokensOut: number, model: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.TOKEN_USAGE,
properties: {
taskId,
tokensIn,
tokensOut,
model,
},
},
})
collect,
)
}
/**
@@ -243,14 +300,17 @@ class PostHogClient {
* @param taskId Unique identifier for the task
* @param mode The mode being switched to (plan or act)
*/
public captureModeSwitch(taskId: string, mode: "plan" | "act") {
this.capture({
event: PostHogClient.EVENTS.TASK.MODE_SWITCH,
properties: {
taskId,
mode,
public captureModeSwitch(taskId: string, mode: "plan" | "act", collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.MODE_SWITCH,
properties: {
taskId,
mode,
},
},
})
collect,
)
}
/**
@@ -258,15 +318,18 @@ class PostHogClient {
* @param taskId Unique identifier for the task
* @param feedbackType The type of feedback ("thumbs_up" or "thumbs_down")
*/
public captureTaskFeedback(taskId: string, feedbackType: TaskFeedbackType) {
public captureTaskFeedback(taskId: string, feedbackType: TaskFeedbackType, collect: boolean = false) {
console.info("TelemetryService: Capturing task feedback", { taskId, feedbackType })
this.capture({
event: PostHogClient.EVENTS.TASK.FEEDBACK,
properties: {
taskId,
feedbackType,
this.capture(
{
event: PostHogClient.EVENTS.TASK.FEEDBACK,
properties: {
taskId,
feedbackType,
},
},
})
collect,
)
}
// Tool events
@@ -277,16 +340,19 @@ class PostHogClient {
* @param autoApproved Whether the tool was auto-approved based on settings
* @param success Whether the tool execution was successful
*/
public captureToolUsage(taskId: string, tool: string, autoApproved: boolean, success: boolean) {
this.capture({
event: PostHogClient.EVENTS.TASK.TOOL_USED,
properties: {
taskId,
tool,
autoApproved,
success,
public captureToolUsage(taskId: string, tool: string, autoApproved: boolean, success: boolean, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.TOOL_USED,
properties: {
taskId,
tool,
autoApproved,
success,
},
},
})
collect,
)
}
/**
@@ -299,15 +365,19 @@ class PostHogClient {
taskId: string,
action: "shadow_git_initialized" | "commit_created" | "restored" | "diff_generated",
durationMs?: number,
collect: boolean = false,
) {
this.capture({
event: PostHogClient.EVENTS.TASK.CHECKPOINT_USED,
properties: {
taskId,
action,
durationMs,
this.capture(
{
event: PostHogClient.EVENTS.TASK.CHECKPOINT_USED,
properties: {
taskId,
action,
durationMs,
},
},
})
collect,
)
}
// UI events
@@ -318,16 +388,25 @@ class PostHogClient {
* @param location Where the switch occurred (settings panel or bottom bar)
* @param taskId Optional task identifier if switch occurred during a task
*/
public captureProviderSwitch(from: string, to: string, location: "settings" | "bottom", taskId?: string) {
this.capture({
event: PostHogClient.EVENTS.UI.PROVIDER_SWITCH,
properties: {
from,
to,
location,
taskId,
public captureProviderSwitch(
from: string,
to: string,
location: "settings" | "bottom",
taskId?: string,
collect: boolean = false,
) {
this.capture(
{
event: PostHogClient.EVENTS.UI.PROVIDER_SWITCH,
properties: {
from,
to,
location,
taskId,
},
},
})
collect,
)
}
/**
@@ -335,14 +414,17 @@ class PostHogClient {
* @param taskId Unique identifier for the task
* @param imageCount Number of images attached
*/
public captureImageAttached(taskId: string, imageCount: number) {
this.capture({
event: PostHogClient.EVENTS.UI.IMAGE_ATTACHED,
properties: {
taskId,
imageCount,
public captureImageAttached(taskId: string, imageCount: number, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.UI.IMAGE_ATTACHED,
properties: {
taskId,
imageCount,
},
},
})
collect,
)
}
/**
@@ -350,66 +432,81 @@ class PostHogClient {
* @param button Identifier for the button that was clicked
* @param taskId Optional task identifier if click occurred during a task
*/
public captureButtonClick(button: string, taskId?: string) {
this.capture({
event: PostHogClient.EVENTS.UI.BUTTON_CLICK,
properties: {
button,
taskId,
public captureButtonClick(button: string, taskId?: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.UI.BUTTON_CLICK,
properties: {
button,
taskId,
},
},
})
collect,
)
}
/**
* Records when the marketplace view is opened
* @param taskId Optional task identifier if marketplace was opened during a task
*/
public captureMarketplaceOpened(taskId?: string) {
this.capture({
event: PostHogClient.EVENTS.UI.MARKETPLACE_OPENED,
properties: {
taskId,
public captureMarketplaceOpened(taskId?: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.UI.MARKETPLACE_OPENED,
properties: {
taskId,
},
},
})
collect,
)
}
/**
* Records when the settings panel is opened
* @param taskId Optional task identifier if settings were opened during a task
*/
public captureSettingsOpened(taskId?: string) {
this.capture({
event: PostHogClient.EVENTS.UI.SETTINGS_OPENED,
properties: {
taskId,
public captureSettingsOpened(taskId?: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.UI.SETTINGS_OPENED,
properties: {
taskId,
},
},
})
collect,
)
}
/**
* Records when the task history view is opened
* @param taskId Optional task identifier if history was opened during a task
*/
public captureHistoryOpened(taskId?: string) {
this.capture({
event: PostHogClient.EVENTS.UI.HISTORY_OPENED,
properties: {
taskId,
public captureHistoryOpened(taskId?: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.UI.HISTORY_OPENED,
properties: {
taskId,
},
},
})
collect,
)
}
/**
* Records when a task is removed from the task history
* @param taskId Unique identifier for the task being removed
*/
public captureTaskPopped(taskId: string) {
this.capture({
event: PostHogClient.EVENTS.UI.TASK_POPPED,
properties: {
taskId,
public captureTaskPopped(taskId: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.UI.TASK_POPPED,
properties: {
taskId,
},
},
})
collect,
)
}
/**
@@ -417,14 +514,17 @@ class PostHogClient {
* @param taskId Unique identifier for the task
* @param errorType Type of error that occurred (e.g., "search_not_found", "invalid_format")
*/
public captureDiffEditFailure(taskId: string, errorType?: string) {
this.capture({
event: PostHogClient.EVENTS.TASK.DIFF_EDIT_FAILED,
properties: {
taskId,
errorType,
public captureDiffEditFailure(taskId: string, errorType?: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.DIFF_EDIT_FAILED,
properties: {
taskId,
errorType,
},
},
})
collect,
)
}
/**
@@ -433,41 +533,50 @@ class PostHogClient {
* @param provider Provider of the selected model
* @param taskId Optional task identifier if model was selected during a task
*/
public captureModelSelected(model: string, provider: string, taskId?: string) {
this.capture({
event: PostHogClient.EVENTS.UI.MODEL_SELECTED,
properties: {
model,
provider,
taskId,
public captureModelSelected(model: string, provider: string, taskId?: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.UI.MODEL_SELECTED,
properties: {
model,
provider,
taskId,
},
},
})
collect,
)
}
/**
* Records when a historical task is loaded from storage
* @param taskId Unique identifier for the historical task
*/
public captureHistoricalTaskLoaded(taskId: string) {
this.capture({
event: PostHogClient.EVENTS.TASK.HISTORICAL_LOADED,
properties: {
taskId,
public captureHistoricalTaskLoaded(taskId: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.HISTORICAL_LOADED,
properties: {
taskId,
},
},
})
collect,
)
}
/**
* Records when the retry button is clicked for failed operations
* @param taskId Unique identifier for the task being retried
*/
public captureRetryClicked(taskId: string) {
this.capture({
event: PostHogClient.EVENTS.TASK.RETRY_CLICKED,
properties: {
taskId,
public captureRetryClicked(taskId: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.RETRY_CLICKED,
properties: {
taskId,
},
},
})
collect,
)
}
/**
@@ -475,17 +584,20 @@ class PostHogClient {
* @param taskId Unique identifier for the task
* @param browserSettings The browser settings being used
*/
public captureBrowserToolStart(taskId: string, browserSettings: BrowserSettings) {
this.capture({
event: PostHogClient.EVENTS.TASK.BROWSER_TOOL_START,
properties: {
taskId,
viewport: browserSettings.viewport,
isRemote: !!browserSettings.remoteBrowserEnabled,
remoteBrowserHost: browserSettings.remoteBrowserHost,
timestamp: new Date().toISOString(),
public captureBrowserToolStart(taskId: string, browserSettings: BrowserSettings, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.BROWSER_TOOL_START,
properties: {
taskId,
viewport: browserSettings.viewport,
isRemote: !!browserSettings.remoteBrowserEnabled,
remoteBrowserHost: browserSettings.remoteBrowserHost,
timestamp: new Date().toISOString(),
},
},
})
collect,
)
}
/**
@@ -500,17 +612,21 @@ class PostHogClient {
duration: number
actions?: string[]
},
collect: boolean = false,
) {
this.capture({
event: PostHogClient.EVENTS.TASK.BROWSER_TOOL_END,
properties: {
taskId,
actionCount: stats.actionCount,
duration: stats.duration,
actions: stats.actions,
timestamp: new Date().toISOString(),
this.capture(
{
event: PostHogClient.EVENTS.TASK.BROWSER_TOOL_END,
properties: {
taskId,
actionCount: stats.actionCount,
duration: stats.duration,
actions: stats.actions,
timestamp: new Date().toISOString(),
},
},
})
collect,
)
}
/**
@@ -530,17 +646,21 @@ class PostHogClient {
isRemote?: boolean
[key: string]: any
},
collect: boolean = false,
) {
this.capture({
event: PostHogClient.EVENTS.TASK.BROWSER_ERROR,
properties: {
taskId,
errorType,
errorMessage,
context,
timestamp: new Date().toISOString(),
this.capture(
{
event: PostHogClient.EVENTS.TASK.BROWSER_ERROR,
properties: {
taskId,
errorType,
errorMessage,
context,
timestamp: new Date().toISOString(),
},
},
})
collect,
)
}
/**
@@ -549,15 +669,18 @@ class PostHogClient {
* @param qty The quantity of options that were presented
* @param mode The mode in which the option was selected ("plan" or "act")
*/
public captureOptionSelected(taskId: string, qty: number, mode: "plan" | "act") {
this.capture({
event: PostHogClient.EVENTS.TASK.OPTION_SELECTED,
properties: {
taskId,
qty,
mode,
public captureOptionSelected(taskId: string, qty: number, mode: "plan" | "act", collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.OPTION_SELECTED,
properties: {
taskId,
qty,
mode,
},
},
})
collect,
)
}
/**
@@ -566,15 +689,18 @@ class PostHogClient {
* @param qty The quantity of options that were presented
* @param mode The mode in which the custom response was provided ("plan" or "act")
*/
public captureOptionsIgnored(taskId: string, qty: number, mode: "plan" | "act") {
this.capture({
event: PostHogClient.EVENTS.TASK.OPTIONS_IGNORED,
properties: {
taskId,
qty,
mode,
public captureOptionsIgnored(taskId: string, qty: number, mode: "plan" | "act", collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.OPTIONS_IGNORED,
properties: {
taskId,
qty,
mode,
},
},
})
collect,
)
}
/**
@@ -582,20 +708,52 @@ class PostHogClient {
* @param model The name of the model the user has interacted with
* @param isFavorited Whether the model is being favorited (true) or unfavorited (false)
*/
public captureModelFavoritesUsage(model: string, isFavorited: boolean) {
this.capture({
event: PostHogClient.EVENTS.UI.MODEL_FAVORITE_TOGGLED,
properties: {
model,
isFavorited,
public captureModelFavoritesUsage(model: string, isFavorited: boolean, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.UI.MODEL_FAVORITE_TOGGLED,
properties: {
model,
isFavorited,
},
},
})
collect,
)
}
public isTelemetryEnabled(): boolean {
return this.telemetryEnabled
}
public async sendCollectedEvents(taskId?: string): Promise<void> {
if (this.collectedTasks.length > 0) {
if (taskId) {
const task = this.collectedTasks.find((t) => t.taskId === taskId)
if (task) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.TASK_COLLECTION,
properties: { taskId, events: task.collection },
},
false,
)
this.collectedTasks = this.collectedTasks.filter((t) => t.taskId !== taskId)
}
} else {
for (const task of this.collectedTasks) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.TASK_COLLECTION,
properties: { taskId: task.taskId, events: task.collection },
},
false,
)
this.collectedTasks = this.collectedTasks.filter((t) => t.taskId !== task.taskId)
}
}
}
}
public async shutdown(): Promise<void> {
await this.client.shutdown()
}
+1 -1
View File
@@ -35,7 +35,6 @@ export interface ExtensionMessage {
| "mcpDownloadDetails"
| "commitSearchResults"
| "openGraphData"
| "isImageUrlResult"
| "didUpdateSettings"
| "userCreditsBalance"
| "userCreditsUsage"
@@ -132,6 +131,7 @@ export interface ExtensionState {
shouldShowAnnouncement: boolean
taskHistory: HistoryItem[]
telemetrySetting: TelemetrySetting
shellIntegrationTimeout: number
uriScheme?: string
userInfo?: {
displayName: string | null
+2 -2
View File
@@ -24,7 +24,6 @@ export interface WebviewMessage {
| "requestOllamaModels"
| "requestLmStudioModels"
| "openInBrowser"
| "createRuleFile"
| "openMention"
| "showChatView"
| "refreshOpenRouterModels"
@@ -54,7 +53,6 @@ export interface WebviewMessage {
| "telemetrySetting"
| "openSettings"
| "fetchOpenGraphData"
| "checkIsImageUrl"
| "invoke"
| "updateSettings"
| "clearAllTaskHistory"
@@ -71,6 +69,7 @@ export interface WebviewMessage {
| "toggleClineRule"
| "deleteClineRule"
| "copyToClipboard"
| "updateTerminalConnectionTimeout"
// | "relaunchChromeDebugMode"
text?: string
@@ -121,6 +120,7 @@ export interface WebviewMessage {
filename?: string
offset?: number
shellIntegrationTimeout?: number
}
export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse"
+11
View File
@@ -47,6 +47,8 @@ export interface ApiHandlerOptions {
awsUseProfile?: boolean
awsProfile?: string
awsBedrockEndpoint?: string
awsBedrockCustomSelected?: boolean
awsBedrockCustomModelBaseId?: BedrockModelId
vertexProjectId?: string
vertexRegion?: string
openAiBaseUrl?: string
@@ -183,6 +185,15 @@ export const anthropicModels = {
export type BedrockModelId = keyof typeof bedrockModels
export const bedrockDefaultModelId: BedrockModelId = "anthropic.claude-3-7-sonnet-20250219-v1:0"
export const bedrockModels = {
"amazon.nova-premier-v1:0": {
maxTokens: 10_000,
contextWindow: 1_000_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 2.5,
outputPrice: 12.5,
},
"amazon.nova-pro-v1:0": {
maxTokens: 5000,
contextWindow: 300_000,
+27
View File
@@ -0,0 +1,27 @@
import { RuleFileRequest } from "../proto/file"
/**
* Simplified clean interface for RuleFile requests
*/
// Helper for creating delete requests
export const DeleteRuleFileRequest = {
create: (params: { rulePath: string; isGlobal: boolean; metadata?: any }): RuleFileRequest => {
return RuleFileRequest.create({
rulePath: params.rulePath,
isGlobal: params.isGlobal,
metadata: params.metadata,
})
},
}
// Helper for creating create requests
export const CreateRuleFileRequest = {
create: (params: { filename: string; isGlobal: boolean; metadata?: any }): RuleFileRequest => {
return RuleFileRequest.create({
filename: params.filename,
isGlobal: params.isGlobal,
metadata: params.metadata,
})
},
}
+1
View File
@@ -0,0 +1 @@
export * from "./file"
+272 -1
View File
@@ -5,10 +5,233 @@
// source: file.proto
/* eslint-disable */
import { Empty, StringRequest } from "./common"
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"
import { Empty, Metadata, StringRequest } from "./common"
export const protobufPackage = "cline"
/** Unified request for all rule file operations */
export interface RuleFileRequest {
metadata?: Metadata | undefined
/** Common field for all operations */
isGlobal: boolean
/** Path field for deleteRuleFile (optional) */
rulePath?: string | undefined
/** Filename field for createRuleFile (optional) */
filename?: string | undefined
}
/** Result for rule file operations with meaningful data only */
export interface RuleFile {
/** Path to the rule file */
filePath: string
/** Filename for display purposes */
displayName: string
/** For createRuleFile, indicates if file already existed */
alreadyExists: boolean
}
function createBaseRuleFileRequest(): RuleFileRequest {
return { metadata: undefined, isGlobal: false, rulePath: undefined, filename: undefined }
}
export const RuleFileRequest: MessageFns<RuleFileRequest> = {
encode(message: RuleFileRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.metadata !== undefined) {
Metadata.encode(message.metadata, writer.uint32(10).fork()).join()
}
if (message.isGlobal !== false) {
writer.uint32(16).bool(message.isGlobal)
}
if (message.rulePath !== undefined) {
writer.uint32(26).string(message.rulePath)
}
if (message.filename !== undefined) {
writer.uint32(34).string(message.filename)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): RuleFileRequest {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseRuleFileRequest()
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 !== 16) {
break
}
message.isGlobal = reader.bool()
continue
}
case 3: {
if (tag !== 26) {
break
}
message.rulePath = reader.string()
continue
}
case 4: {
if (tag !== 34) {
break
}
message.filename = reader.string()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): RuleFileRequest {
return {
metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined,
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,
}
},
toJSON(message: RuleFileRequest): unknown {
const obj: any = {}
if (message.metadata !== undefined) {
obj.metadata = Metadata.toJSON(message.metadata)
}
if (message.isGlobal !== false) {
obj.isGlobal = message.isGlobal
}
if (message.rulePath !== undefined) {
obj.rulePath = message.rulePath
}
if (message.filename !== undefined) {
obj.filename = message.filename
}
return obj
},
create<I extends Exact<DeepPartial<RuleFileRequest>, I>>(base?: I): RuleFileRequest {
return RuleFileRequest.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<RuleFileRequest>, I>>(object: I): RuleFileRequest {
const message = createBaseRuleFileRequest()
message.metadata =
object.metadata !== undefined && object.metadata !== null ? Metadata.fromPartial(object.metadata) : undefined
message.isGlobal = object.isGlobal ?? false
message.rulePath = object.rulePath ?? undefined
message.filename = object.filename ?? undefined
return message
},
}
function createBaseRuleFile(): RuleFile {
return { filePath: "", displayName: "", alreadyExists: false }
}
export const RuleFile: MessageFns<RuleFile> = {
encode(message: RuleFile, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.filePath !== "") {
writer.uint32(10).string(message.filePath)
}
if (message.displayName !== "") {
writer.uint32(18).string(message.displayName)
}
if (message.alreadyExists !== false) {
writer.uint32(24).bool(message.alreadyExists)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): RuleFile {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseRuleFile()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.filePath = reader.string()
continue
}
case 2: {
if (tag !== 18) {
break
}
message.displayName = reader.string()
continue
}
case 3: {
if (tag !== 24) {
break
}
message.alreadyExists = reader.bool()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): RuleFile {
return {
filePath: isSet(object.filePath) ? globalThis.String(object.filePath) : "",
displayName: isSet(object.displayName) ? globalThis.String(object.displayName) : "",
alreadyExists: isSet(object.alreadyExists) ? globalThis.Boolean(object.alreadyExists) : false,
}
},
toJSON(message: RuleFile): unknown {
const obj: any = {}
if (message.filePath !== "") {
obj.filePath = message.filePath
}
if (message.displayName !== "") {
obj.displayName = message.displayName
}
if (message.alreadyExists !== false) {
obj.alreadyExists = message.alreadyExists
}
return obj
},
create<I extends Exact<DeepPartial<RuleFile>, I>>(base?: I): RuleFile {
return RuleFile.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<RuleFile>, I>>(object: I): RuleFile {
const message = createBaseRuleFile()
message.filePath = object.filePath ?? ""
message.displayName = object.displayName ?? ""
message.alreadyExists = object.alreadyExists ?? false
return message
},
}
/** Service for file-related operations */
export type FileServiceDefinition = typeof FileServiceDefinition
export const FileServiceDefinition = {
@@ -33,5 +256,53 @@ export const FileServiceDefinition = {
responseStream: false,
options: {},
},
/** Deletes a rule file from either global or workspace rules directory */
deleteRuleFile: {
name: "deleteRuleFile",
requestType: RuleFileRequest,
requestStream: false,
responseType: RuleFile,
responseStream: false,
options: {},
},
/** Creates a rule file from either global or workspace rules directory */
createRuleFile: {
name: "createRuleFile",
requestType: RuleFileRequest,
requestStream: false,
responseType: RuleFile,
responseStream: false,
options: {},
},
},
} as const
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined
export type DeepPartial<T> = T extends Builtin
? T
: T extends globalThis.Array<infer U>
? globalThis.Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: T extends {}
? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>
type KeysOfUnion<T> = T extends T ? keyof T : never
export type Exact<P, I extends P> = P extends Builtin
? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never }
function isSet(value: any): boolean {
return value !== null && value !== undefined
}
export interface MessageFns<T> {
encode(message: T, writer?: BinaryWriter): BinaryWriter
decode(input: BinaryReader | Uint8Array, length?: number): T
fromJSON(object: any): T
toJSON(message: T): unknown
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T
}
+138
View File
@@ -0,0 +1,138 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.7.0
// protoc v3.19.1
// source: web_content.proto
/* eslint-disable */
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"
import { StringRequest } from "./common"
export const protobufPackage = "cline"
export interface IsImageUrl {
isImage: boolean
url: string
}
function createBaseIsImageUrl(): IsImageUrl {
return { isImage: false, url: "" }
}
export const IsImageUrl: MessageFns<IsImageUrl> = {
encode(message: IsImageUrl, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.isImage !== false) {
writer.uint32(8).bool(message.isImage)
}
if (message.url !== "") {
writer.uint32(18).string(message.url)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): IsImageUrl {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseIsImageUrl()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 8) {
break
}
message.isImage = reader.bool()
continue
}
case 2: {
if (tag !== 18) {
break
}
message.url = reader.string()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): IsImageUrl {
return {
isImage: isSet(object.isImage) ? globalThis.Boolean(object.isImage) : false,
url: isSet(object.url) ? globalThis.String(object.url) : "",
}
},
toJSON(message: IsImageUrl): unknown {
const obj: any = {}
if (message.isImage !== false) {
obj.isImage = message.isImage
}
if (message.url !== "") {
obj.url = message.url
}
return obj
},
create<I extends Exact<DeepPartial<IsImageUrl>, I>>(base?: I): IsImageUrl {
return IsImageUrl.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<IsImageUrl>, I>>(object: I): IsImageUrl {
const message = createBaseIsImageUrl()
message.isImage = object.isImage ?? false
message.url = object.url ?? ""
return message
},
}
export type WebContentServiceDefinition = typeof WebContentServiceDefinition
export const WebContentServiceDefinition = {
name: "WebContentService",
fullName: "cline.WebContentService",
methods: {
checkIsImageUrl: {
name: "checkIsImageUrl",
requestType: StringRequest,
requestStream: false,
responseType: IsImageUrl,
responseStream: false,
options: {},
},
},
} as const
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined
export type DeepPartial<T> = T extends Builtin
? T
: T extends globalThis.Array<infer U>
? globalThis.Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: T extends {}
? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>
type KeysOfUnion<T> = T extends T ? keyof T : never
export type Exact<P, I extends P> = P extends Builtin
? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never }
function isSet(value: any): boolean {
return value !== null && value !== undefined
}
export interface MessageFns<T> {
encode(message: T, writer?: BinaryWriter): BinaryWriter
decode(input: BinaryReader | Uint8Array, length?: number): T
fromJSON(object: any): T
toJSON(message: T): unknown
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T
}
+30 -2
View File
@@ -30,6 +30,15 @@ async function checkGitInstalled(): Promise<boolean> {
}
}
async function checkGitRepoHasCommits(cwd: string): Promise<boolean> {
try {
await execAsync("git rev-parse HEAD", { cwd })
return true
} catch (error) {
return false
}
}
export async function searchCommits(query: string, cwd: string): Promise<GitCommit[]> {
try {
const isInstalled = await checkGitInstalled()
@@ -44,6 +53,12 @@ export async function searchCommits(query: string, cwd: string): Promise<GitComm
return []
}
// Check if repo has any commits
if (!(await checkGitRepoHasCommits(cwd))) {
// No commits yet in the repository
return []
}
// Search commits by hash or message, limiting to 10 results
const { stdout } = await execAsync(
`git log -n 10 --format="%H%n%h%n%s%n%an%n%ad" --date=short ` + `--grep="${query}" --regexp-ignore-case`,
@@ -100,6 +115,11 @@ export async function getCommitInfo(hash: string, cwd: string): Promise<string>
return "Not a git repository"
}
// Check if repo has any commits
if (!(await checkGitRepoHasCommits(cwd))) {
return "Repository has no commits yet"
}
// Get commit info, stats, and diff separately
const { stdout: info } = await execAsync(`git show --format="%H%n%h%n%s%n%an%n%ad%n%b" --no-patch ${hash}`, {
cwd,
@@ -147,8 +167,16 @@ export async function getWorkingState(cwd: string): Promise<string> {
return "No changes in working directory"
}
// Get all changes (both staged and unstaged) compared to HEAD
const { stdout: diff } = await execAsync("git diff HEAD", { cwd })
// Check if repo has any commits before trying to diff against HEAD
let diff = ""
if (await checkGitRepoHasCommits(cwd)) {
// Only run git diff if there are commits
const { stdout: diffOutput } = await execAsync("git diff HEAD", { cwd })
diff = diffOutput
} else {
// No commits yet, use status output only
return `Working directory changes (new repository):\n\n${status}`
}
const output = `Working directory changes:\n\n${status}\n\n${diff}`.trim()
return truncateOutput(output)
} catch (error) {
+22
View File
@@ -0,0 +1,22 @@
const tsConfigPaths = require("tsconfig-paths")
const fs = require("fs")
const tsConfig = JSON.parse(fs.readFileSync("./tsconfig.json", "utf-8"))
/**
* The aliases point towards the `src` directory.
* However, `tsc` doesn't compile paths by itself
* (https://www.typescriptlang.org/docs/handbook/modules/reference.html#paths-does-not-affect-emit)
* So we need to use tsconfig-paths to resolve the aliases when running tests,
* but pointing to `out` instead.
*/
const outPaths = {}
Object.keys(tsConfig.compilerOptions.paths).forEach((key) => {
const value = tsConfig.compilerOptions.paths[key]
outPaths[key] = value.map((path) => path.replace("src", "out"))
})
tsConfigPaths.register({
baseUrl: ".",
paths: outPaths,
})
+2 -1
View File
@@ -27,7 +27,8 @@
"@integrations/*": ["src/integrations/*"],
"@services/*": ["src/services/*"],
"@shared/*": ["src/shared/*"],
"@utils/*": ["src/utils/*"]
"@utils/*": ["src/utils/*"],
"@packages/*": ["src/packages/*"]
}
},
"include": ["src/**/*", "scripts/**/*"],
+1
View File
@@ -0,0 +1 @@
25463
+4295 -162
View File
File diff suppressed because it is too large Load Diff
+5 -3
View File
@@ -15,11 +15,13 @@
},
"dependencies": {
"@floating-ui/react": "^0.27.4",
"@heroui/react": "^2.8.0-beta.2",
"@vscode/webview-ui-toolkit": "^1.4.0",
"debounce": "^2.1.1",
"dompurify": "^3.2.4",
"fast-deep-equal": "^3.1.3",
"firebase": "^11.3.0",
"framer-motion": "^12.7.4",
"fuse.js": "^7.0.0",
"fzf": "^0.5.2",
"mermaid": "^11.4.1",
@@ -42,7 +44,7 @@
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@tailwindcss/vite": "^4.0.12",
"@tailwindcss/vite": "^4.1.4",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^14.6.1",
@@ -60,10 +62,10 @@
"eslint-plugin-react-refresh": "^0.4.16",
"globals": "^15.14.0",
"jsdom": "^26.0.0",
"tailwindcss": "^4.0.12",
"tailwindcss": "^4.1.4",
"typescript": "^5.7.3",
"typescript-eslint": "^8.18.2",
"vite": "^6.2.6",
"vite": "^6.3.4",
"vitest": "^3.0.5"
}
}
+5 -7
View File
@@ -6,10 +6,10 @@ import HistoryView from "./components/history/HistoryView"
import SettingsView from "./components/settings/SettingsView"
import WelcomeView from "./components/welcome/WelcomeView"
import AccountView from "./components/account/AccountView"
import { ExtensionStateContextProvider, useExtensionState } from "./context/ExtensionStateContext"
import { FirebaseAuthProvider } from "./context/FirebaseAuthContext"
import { useExtensionState } from "./context/ExtensionStateContext"
import { vscode } from "./utils/vscode"
import McpView from "./components/mcp/configuration/McpConfigurationView"
import { Providers } from "./Providers"
const AppContent = () => {
const { didHydrateState, showWelcome, shouldShowAnnouncement, showMcp, mcpTab } = useExtensionState()
@@ -126,11 +126,9 @@ const AppContent = () => {
const App = () => {
return (
<ExtensionStateContextProvider>
<FirebaseAuthProvider>
<AppContent />
</FirebaseAuthProvider>
</ExtensionStateContextProvider>
<Providers>
<AppContent />
</Providers>
)
}
+15
View File
@@ -0,0 +1,15 @@
import type { ReactNode } from "react"
import { ExtensionStateContextProvider } from "./context/ExtensionStateContext"
import { FirebaseAuthProvider } from "./context/FirebaseAuthContext"
import { HeroUIProvider } from "@heroui/react"
export function Providers({ children }: { children: ReactNode }) {
return (
<ExtensionStateContextProvider>
<FirebaseAuthProvider>
<HeroUIProvider>{children}</HeroUIProvider>
</FirebaseAuthProvider>
</ExtensionStateContextProvider>
)
}
@@ -1616,7 +1616,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
color: "var(--vscode-descriptionForeground)",
whiteSpace: "nowrap",
}}>
Hold Shift to Drag Files
Hold Shift to Drop Files
</span>
</div>
</div>
@@ -2,6 +2,8 @@ import { useState, useRef, useEffect } from "react"
import { vscode } from "@/utils/vscode"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { useClickAway } from "react-use"
import { FileServiceClient } from "@/services/grpc-client"
import { CreateRuleFileRequest } from "@shared/proto-conversions"
interface NewRuleRowProps {
isGlobal: boolean
@@ -40,7 +42,7 @@ const NewRuleRow: React.FC<NewRuleRowProps> = ({ isGlobal }) => {
return ext === "" || ext === ".md" || ext === ".txt"
}
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (filename.trim()) {
@@ -57,11 +59,16 @@ const NewRuleRow: React.FC<NewRuleRowProps> = ({ isGlobal }) => {
finalFilename = `${trimmedFilename}.md`
}
vscode.postMessage({
type: "createRuleFile",
isGlobal,
filename: finalFilename,
})
try {
await FileServiceClient.createRuleFile(
CreateRuleFileRequest.create({
isGlobal,
filename: finalFilename,
}),
)
} catch (err) {
console.error("Error creating rule file:", err)
}
setFilename("")
setError(null)
@@ -1,6 +1,6 @@
import { vscode } from "@/utils/vscode"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { FileServiceClient } from "@/services/grpc-client"
import { DeleteRuleFileRequest } from "@shared/proto-conversions"
const RuleRow: React.FC<{
rulePath: string
@@ -16,11 +16,12 @@ const RuleRow: React.FC<{
}
const handleDeleteClick = () => {
vscode.postMessage({
type: "deleteClineRule",
rulePath: rulePath,
isGlobal: isGlobal,
})
FileServiceClient.deleteRuleFile(
DeleteRuleFileRequest.create({
rulePath: rulePath,
isGlobal: isGlobal,
}),
).catch((err) => console.error("Failed to delete rule file:", err))
}
return (
@@ -1,4 +1,4 @@
import React, { memo, useEffect } from "react"
import React, { memo, useEffect, useRef, useState } from "react"
import type { ComponentProps } from "react"
import { useRemark } from "react-remark"
import rehypeHighlight, { Options } from "rehype-highlight"
@@ -91,15 +91,34 @@ const remarkPreventBoldFilenames = () => {
}
}
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
const CopyButton = styled(VSCodeButton)`
position: absolute;
top: 5px;
right: 5px;
z-index: 1;
opacity: 0;
`
const CodeBlockContainer = styled.div`
position: relative;
&:hover ${CopyButton} {
opacity: 1;
}
`
const StyledMarkdown = styled.div`
pre {
background-color: ${CODE_BLOCK_BG_COLOR};
border-radius: 3px;
margin: 13x 0;
margin: 13px 0;
padding: 10px 10px;
max-width: calc(100vw - 20px);
overflow-x: auto;
overflow-y: hidden;
padding-right: 70px;
}
pre > code {
@@ -197,8 +216,42 @@ const StyledPre = styled.pre<{ theme: any }>`
.join("")}
`
const PreWithCopyButton = ({
children,
theme,
...preProps
}: { theme: Record<string, string> } & React.HTMLAttributes<HTMLPreElement>) => {
const preRef = useRef<HTMLPreElement>(null)
const [copied, setCopied] = useState(false)
const handleCopy = () => {
if (preRef.current) {
const codeElement = preRef.current.querySelector("code")
const textToCopy = codeElement ? codeElement.textContent : preRef.current.textContent
if (!textToCopy) return
navigator.clipboard.writeText(textToCopy).then(() => {
setCopied(true)
setTimeout(() => setCopied(false), 1500)
})
}
}
return (
<CodeBlockContainer>
<CopyButton appearance="icon" onClick={handleCopy} aria-label={copied ? "Copied" : "Copy"}>
<span className={`codicon codicon-${copied ? "check" : "copy"}`}></span>
</CopyButton>
<StyledPre {...preProps} theme={theme} ref={preRef}>
{children}
</StyledPre>
</CodeBlockContainer>
)
}
const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => {
const { theme } = useExtensionState()
const [reactContent, setMarkdown] = useRemark({
remarkPlugins: [
remarkPreventBoldFilenames,
@@ -223,7 +276,7 @@ const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => {
],
rehypeReactOptions: {
components: {
pre: ({ node, children, ...preProps }: any) => {
pre: ({ children, ...preProps }: React.HTMLAttributes<HTMLPreElement>) => {
if (Array.isArray(children) && children.length === 1 && React.isValidElement(children[0])) {
const child = children[0] as React.ReactElement<{ className?: string }>
if (child.props?.className?.includes("language-mermaid")) {
@@ -231,9 +284,9 @@ const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => {
}
}
return (
<StyledPre {...preProps} theme={theme}>
<PreWithCopyButton {...preProps} theme={theme || {}}>
{children}
</StyledPre>
</PreWithCopyButton>
)
},
code: (props: ComponentProps<"code">) => {
@@ -253,7 +306,7 @@ const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => {
}, [markdown, setMarkdown, theme])
return (
<div style={{}}>
<div>
<StyledMarkdown>{reactContent}</StyledMarkdown>
</div>
)
@@ -1,4 +1,4 @@
import { vscode } from "@/utils/vscode"
import { WebContentServiceClient } from "@/services/grpc-client"
// Safely create a URL object with error handling and ensure HTTPS
export const safeCreateUrl = (url: string): URL | null => {
@@ -133,44 +133,30 @@ export const checkIfImageUrl = async (url: string): Promise<boolean> => {
return false
}
// For https URLs, we need to send a message to the extension
// For https URLs, we need to use the gRPC FileService
if (url.startsWith("https")) {
try {
// Create a promise that will resolve when we get a response
return new Promise((resolve) => {
let timeoutId: ReturnType<typeof setTimeout> | undefined = undefined
// Set up a one-time listener for the response
const messageListener = (event: MessageEvent) => {
const message = event.data
if (message.type === "isImageUrlResult" && message.url === url) {
window.removeEventListener("message", messageListener)
resolve(message.isImage)
if (timeoutId) {
clearTimeout(timeoutId)
}
}
}
window.addEventListener("message", messageListener)
// Send the request to the extension
vscode.postMessage({
type: "checkIsImageUrl",
text: url,
})
// Set a timeout to avoid hanging indefinitely
timeoutId = setTimeout(() => {
window.removeEventListener("message", messageListener)
// Use the gRPC client with timeout
const timeoutPromise = new Promise<boolean>((resolve) => {
setTimeout(() => {
console.log("Hit timeout waiting for checkIsImageUrl")
resolve(false)
}, 3000)
})
// Create the actual service call
const servicePromise = WebContentServiceClient.checkIsImageUrl({ value: url })
.then((result) => result.isImage)
.catch((error) => {
console.error("Error checking if URL is an image via gRPC:", error)
return false
})
// Race between the service call and the timeout
return Promise.race([servicePromise, timeoutPromise])
} catch (error) {
console.log("Error checking if URL is an image:", url)
// Don't fall back to extension check on error
// Instead, return false to indicate it's not an image
// Return false to indicate it's not an image
return false
}
}
@@ -769,6 +769,101 @@ const ApiOptions = ({
</>
)}
</p>
<label htmlFor="bedrock-model-dropdown">
<span style={{ fontWeight: 500 }}>Model</span>
</label>
<DropdownContainer zIndex={DROPDOWN_Z_INDEX - 2} className="dropdown-container">
<VSCodeDropdown
id="bedrock-model-dropdown"
value={apiConfiguration?.awsBedrockCustomSelected ? "custom" : selectedModelId}
onChange={(e: any) => {
const isCustom = e.target.value === "custom"
setApiConfiguration({
...apiConfiguration,
apiModelId: isCustom ? "" : e.target.value,
awsBedrockCustomSelected: isCustom,
awsBedrockCustomModelBaseId: bedrockDefaultModelId,
})
}}
style={{ width: "100%" }}>
<VSCodeOption value="">Select a model...</VSCodeOption>
{Object.keys(bedrockModels).map((modelId) => (
<VSCodeOption
key={modelId}
value={modelId}
style={{
whiteSpace: "normal",
wordWrap: "break-word",
maxWidth: "100%",
}}>
{modelId}
</VSCodeOption>
))}
<VSCodeOption value="custom">Custom</VSCodeOption>
</VSCodeDropdown>
</DropdownContainer>
{apiConfiguration?.awsBedrockCustomSelected && (
<div>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
Select "Custom" when using the Application Inference Profile in Bedrock. Enter the Application
Inference Profile ID in the Model ID field. However, be sure to encode the / in the ARN as %2F.
<br />
Example: arn:aws:bedrock:us-west-2:&lt;AWS Account
ID&gt;:application-inference-profile%2Fxxxxxxxxxxxx
</p>
<label htmlFor="bedrock-model-input">
<span style={{ fontWeight: 500 }}>Model ID</span>
</label>
<VSCodeTextField
id="bedrock-model-input"
value={apiConfiguration?.apiModelId || ""}
style={{ width: "100%", marginTop: 3 }}
onInput={handleInputChange("apiModelId")}
placeholder="Enter custom model ID..."
/>
<label htmlFor="bedrock-base-model-dropdown">
<span style={{ fontWeight: 500 }}>Base Inference Model</span>
</label>
<DropdownContainer zIndex={DROPDOWN_Z_INDEX - 3} className="dropdown-container">
<VSCodeDropdown
id="bedrock-base-model-dropdown"
value={apiConfiguration?.awsBedrockCustomModelBaseId || bedrockDefaultModelId}
onChange={handleInputChange("awsBedrockCustomModelBaseId")}
style={{ width: "100%" }}>
<VSCodeOption value="">Select a model...</VSCodeOption>
{Object.keys(bedrockModels).map((modelId) => (
<VSCodeOption
key={modelId}
value={modelId}
style={{
whiteSpace: "normal",
wordWrap: "break-word",
maxWidth: "100%",
}}>
{modelId}
</VSCodeOption>
))}
</VSCodeDropdown>
</DropdownContainer>
</div>
)}
{(selectedModelId === "anthropic.claude-3-7-sonnet-20250219-v1:0" ||
(apiConfiguration?.awsBedrockCustomSelected &&
apiConfiguration?.awsBedrockCustomModelBaseId === "anthropic.claude-3-7-sonnet-20250219-v1:0")) && (
<ThinkingBudgetSlider apiConfiguration={apiConfiguration} setApiConfiguration={setApiConfiguration} />
)}
<ModelInfoView
selectedModelId={selectedModelId}
modelInfo={selectedModelInfo}
isDescriptionExpanded={isDescriptionExpanded}
setIsDescriptionExpanded={setIsDescriptionExpanded}
isPopup={isPopup}
/>
</div>
)}
@@ -1702,6 +1797,7 @@ const ApiOptions = ({
selectedProvider !== "vscode-lm" &&
selectedProvider !== "litellm" &&
selectedProvider !== "requesty" &&
selectedProvider !== "bedrock" &&
showModelOptions && (
<>
<DropdownContainer zIndex={DROPDOWN_Z_INDEX - 2} className="dropdown-container">
@@ -1709,7 +1805,6 @@ const ApiOptions = ({
<span style={{ fontWeight: 500 }}>Model</span>
</label>
{selectedProvider === "anthropic" && createDropdown(anthropicModels)}
{selectedProvider === "bedrock" && createDropdown(bedrockModels)}
{selectedProvider === "vertex" && createDropdown(vertexModels)}
{selectedProvider === "gemini" && createDropdown(geminiModels)}
{selectedProvider === "openai-native" && createDropdown(openAiNativeModels)}
@@ -1726,7 +1821,6 @@ const ApiOptions = ({
</DropdownContainer>
{((selectedProvider === "anthropic" && selectedModelId === "claude-3-7-sonnet-20250219") ||
(selectedProvider === "bedrock" && selectedModelId === "anthropic.claude-3-7-sonnet-20250219-v1:0") ||
(selectedProvider === "vertex" && selectedModelId === "claude-3-7-sonnet@20250219")) && (
<ThinkingBudgetSlider apiConfiguration={apiConfiguration} setApiConfiguration={setApiConfiguration} />
)}
@@ -2048,6 +2142,14 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration):
case "anthropic":
return getProviderData(anthropicModels, anthropicDefaultModelId)
case "bedrock":
if (apiConfiguration?.awsBedrockCustomSelected) {
const baseModelId = apiConfiguration.awsBedrockCustomModelBaseId
return {
selectedProvider: provider,
selectedModelId: modelId || bedrockDefaultModelId,
selectedModelInfo: (baseModelId && bedrockModels[baseModelId]) || bedrockModels[bedrockDefaultModelId],
}
}
return getProviderData(bedrockModels, bedrockDefaultModelId)
case "vertex":
return getProviderData(vertexModels, vertexDefaultModelId)
@@ -9,6 +9,7 @@ import { TabButton } from "../mcp/configuration/McpConfigurationView"
import { useEvent } from "react-use"
import { ExtensionMessage } from "@shared/ExtensionMessage"
import BrowserSettingsSection from "./BrowserSettingsSection"
import TerminalSettingsSection from "./TerminalSettingsSection"
const { IS_DEV } = process.env
@@ -240,6 +241,9 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
{/* Browser Settings Section */}
<BrowserSettingsSection />
{/* Terminal Settings Section */}
<TerminalSettingsSection />
<div className="mt-auto pr-2 flex justify-center">
<SettingsButton
onClick={() => vscode.postMessage({ type: "openExtensionSettings" })}
@@ -0,0 +1,76 @@
import React, { useState } from "react"
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { vscode } from "@/utils/vscode"
export const TerminalSettingsSection: React.FC = () => {
const { shellIntegrationTimeout, setShellIntegrationTimeout } = useExtensionState()
const [inputValue, setInputValue] = useState((shellIntegrationTimeout / 1000).toString())
const [inputError, setInputError] = useState<string | null>(null)
const handleTimeoutChange = (event: Event) => {
const target = event.target as HTMLInputElement
const value = target.value
setInputValue(value)
const seconds = parseFloat(value)
if (isNaN(seconds) || seconds <= 0) {
setInputError("Please enter a positive number")
return
}
setInputError(null)
const timeout = Math.round(seconds * 1000) // Convert to milliseconds
// Update local state
setShellIntegrationTimeout(timeout)
// Send to extension
vscode.postMessage({
type: "updateTerminalConnectionTimeout",
shellIntegrationTimeout: timeout,
})
}
const handleInputBlur = () => {
// If there was an error, reset the input to the current valid value
if (inputError) {
setInputValue((shellIntegrationTimeout / 1000).toString())
setInputError(null)
}
}
return (
<div
id="terminal-settings-section"
style={{ marginBottom: 20, borderTop: "1px solid var(--vscode-panel-border)", paddingTop: 15 }}>
<h3 style={{ color: "var(--vscode-foreground)", margin: "0 0 10px 0", fontSize: "14px" }}>Terminal Settings</h3>
<div style={{ marginBottom: 15 }}>
<div style={{ marginBottom: 8 }}>
<label style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>
Shell integration timeout (seconds)
</label>
<div style={{ display: "flex", alignItems: "center" }}>
<VSCodeTextField
style={{ width: "100%" }}
value={inputValue}
placeholder="Enter timeout in seconds"
onChange={(event) => handleTimeoutChange(event as Event)}
onBlur={handleInputBlur}
/>
</div>
{inputError && (
<div style={{ color: "var(--vscode-errorForeground)", fontSize: "12px", marginTop: 5 }}>{inputError}</div>
)}
</div>
<p style={{ fontSize: "12px", color: "var(--vscode-descriptionForeground)", margin: 0 }}>
Set how long Cline waits for shell integration to activate before executing commands. Increase this value if
you experience terminal connection timeouts.
</p>
</div>
</div>
)
}
export default TerminalSettingsSection
@@ -39,6 +39,7 @@ interface ExtensionStateContextType extends ExtensionState {
setTelemetrySetting: (value: TelemetrySetting) => void
setShowAnnouncement: (value: boolean) => void
setPlanActSeparateModelsSetting: (value: boolean) => void
setShellIntegrationTimeout: (value: number) => void
setMcpServers: (value: McpServer[]) => void
// Navigation
@@ -69,6 +70,7 @@ export const ExtensionStateContextProvider: React.FC<{
planActSeparateModelsSetting: true,
globalClineRulesToggles: {},
localClineRulesToggles: {},
shellIntegrationTimeout: 4000, // default timeout for shell integration
})
const [didHydrateState, setDidHydrateState] = useState(false)
const [showWelcome, setShowWelcome] = useState(false)
@@ -242,6 +244,11 @@ export const ExtensionStateContextProvider: React.FC<{
...prevState,
shouldShowAnnouncement: value,
})),
setShellIntegrationTimeout: (value) =>
setState((prevState) => ({
...prevState,
shellIntegrationTimeout: value,
})),
setMcpServers: (mcpServers: McpServer[]) => setMcpServers(mcpServers),
setShowMcp,
setMcpTab,
+2
View File
@@ -4,6 +4,8 @@
/* @import "tailwindcss/preflight.css" layer(base); */
@import "tailwindcss/utilities.css" layer(utilities);
@config "../tailwind.config.js";
textarea:focus {
outline: 1.5px solid var(--vscode-focusBorder, #007fd4);
}
+3
View File
@@ -7,6 +7,7 @@ import { EmptyRequest } from "@shared/proto/common"
import { FileServiceDefinition } from "@shared/proto/file"
import { McpServiceDefinition } from "@shared/proto/mcp"
import { TaskServiceDefinition } from "@shared/proto/task"
import { WebContentServiceDefinition } from "@shared/proto/web_content"
// Generic type for any protobuf service definition
type ProtoService = {
name: string
@@ -102,6 +103,7 @@ const CheckpointsServiceClient = createGrpcClient(CheckpointsServiceDefinition)
const FileServiceClient = createGrpcClient(FileServiceDefinition)
const McpServiceClient = createGrpcClient(McpServiceDefinition)
const TaskServiceClient = createGrpcClient(TaskServiceDefinition)
const WebContentServiceClient = createGrpcClient(WebContentServiceDefinition)
export {
AccountServiceClient,
@@ -110,4 +112,5 @@ export {
FileServiceClient,
TaskServiceClient,
McpServiceClient,
WebContentServiceClient,
}
+22
View File
@@ -0,0 +1,22 @@
const { heroui } = require("@heroui/react")
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{js,ts,jsx,tsx,mdx}", "./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {},
},
darkMode: "class",
plugins: [
heroui({
defaultTheme: "vscode",
themes: {
vscode: {
colors: {
background: "",
},
},
},
}),
],
}