mirror of
https://github.com/cline/cline.git
synced 2026-09-07 22:07:51 +08:00
Compare commits
30
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d9b9445a0 | ||
|
|
21c3c33fed | ||
|
|
fb43add6df | ||
|
|
362b05b25b | ||
|
|
59894c40e9 | ||
|
|
88980a8e1b | ||
|
|
bd52febe90 | ||
|
|
9395d25f7a | ||
|
|
5802b6847e | ||
|
|
4a768702aa | ||
|
|
4565e067af | ||
|
|
4650ffa86b | ||
|
|
f6d50ead3f | ||
|
|
70cc437d71 | ||
|
|
bdfda6f908 | ||
|
|
c5de50fdd2 | ||
|
|
77c9863b50 | ||
|
|
03d44105cc | ||
|
|
79b76fd783 | ||
|
|
19cc8bc9f8 | ||
|
|
08c04a3c67 | ||
|
|
41ae7326c0 | ||
|
|
b0961f4538 | ||
|
|
26242f6378 | ||
|
|
13228ed46f | ||
|
|
d162a4b420 | ||
|
|
1704684af8 | ||
|
|
c63d9a13a5 | ||
|
|
65243adb24 | ||
|
|
e35f7b4e21 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
create clinerules folder if its currently a file and creating new rule
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
updated drag and drop text to say "drop" instead of "drag"
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Allow option to collect events to send them in a bundle to avoid sending too many events
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Add support for custom model ID in AWS Bedrock provider, enabling use of Application Inference Profile.
|
||||
@@ -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.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Remove linear pull request action
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
createRuleFile protobus migration
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Fix Handle @withRetry() SyntaxError when running extension locally issue
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix for git commit mentions in repos with no git commits
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
feat(bedrock): Introduce Amazon Nova Premier
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Added copy button to code blocks.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Introduce UI library for future UI development
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
checkIsImageURL migrated to protobus
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Fix Windows path issue: Correct handling of import.meta.url to avoid leading slash in pathname
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
deleteRuleFile protobus migration
|
||||
@@ -13,6 +13,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
codespell:
|
||||
if: false
|
||||
name: Check for spelling errors
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
@@ -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"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Hello World"
|
||||
description: "This is the introduction to the documentation"
|
||||
---
|
||||
@@ -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,
|
||||
|
||||
Generated
+7559
-2
File diff suppressed because it is too large
Load Diff
+4
-2
@@ -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",
|
||||
|
||||
@@ -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
|
||||
]
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "common.proto";
|
||||
|
||||
service WebContentService {
|
||||
rpc checkIsImageUrl(StringRequest) returns (IsImageUrl);
|
||||
}
|
||||
|
||||
message IsImageUrl {
|
||||
bool is_image = 1;
|
||||
string url = 2;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
const { execSync } = require("child_process")
|
||||
const esbuild = require("esbuild")
|
||||
|
||||
const watch = process.argv.includes("--watch")
|
||||
|
||||
/**
|
||||
* @type {import('esbuild').Plugin}
|
||||
*/
|
||||
const esbuildProblemMatcherPlugin = {
|
||||
name: "esbuild-problem-matcher",
|
||||
|
||||
setup(build) {
|
||||
build.onStart(() => {
|
||||
console.log("[watch] build started")
|
||||
})
|
||||
build.onEnd((result) => {
|
||||
result.errors.forEach(({ text, location }) => {
|
||||
console.error(`✘ [ERROR] ${text}`)
|
||||
console.error(` ${location.file}:${location.line}:${location.column}:`)
|
||||
})
|
||||
console.log("[watch] build finished")
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
const srcConfig = {
|
||||
bundle: true,
|
||||
minify: false,
|
||||
sourcemap: true,
|
||||
sourcesContent: true,
|
||||
logLevel: "silent",
|
||||
entryPoints: ["src/packages/**/*.ts"],
|
||||
outdir: "out/packages",
|
||||
format: "cjs",
|
||||
platform: "node",
|
||||
define: {
|
||||
"process.env.IS_DEV": "true",
|
||||
"process.env.IS_TEST": "true",
|
||||
},
|
||||
external: ["vscode"],
|
||||
plugins: [esbuildProblemMatcherPlugin],
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const srcCtx = await esbuild.context(srcConfig)
|
||||
|
||||
if (watch) {
|
||||
await srcCtx.watch()
|
||||
} else {
|
||||
await srcCtx.rebuild()
|
||||
|
||||
await srcCtx.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
execSync("tsc -p ./tsconfig.test.json --outDir out", { encoding: "utf-8" })
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -28,20 +28,26 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
const modelId = await this.getModelId()
|
||||
const model = this.getModel()
|
||||
|
||||
// This baseModelId is used to indicate the capabilities of the model.
|
||||
// If the user selects a custom model, baseModelId will be set to the base model ID of the custom model.
|
||||
// Otherwise, baseModelId will be the same as modelId.
|
||||
const baseModelId =
|
||||
(this.options.awsBedrockCustomSelected ? this.options.awsBedrockCustomModelBaseId : modelId) || modelId
|
||||
|
||||
// Check if this is an Amazon Nova model
|
||||
if (modelId.includes("amazon.nova")) {
|
||||
if (baseModelId.includes("amazon.nova")) {
|
||||
yield* this.createNovaMessage(systemPrompt, messages, modelId, model)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if this is a Deepseek model
|
||||
if (modelId.includes("deepseek")) {
|
||||
if (baseModelId.includes("deepseek")) {
|
||||
yield* this.createDeepseekMessage(systemPrompt, messages, modelId, model)
|
||||
return
|
||||
}
|
||||
|
||||
const budget_tokens = this.options.thinkingBudgetTokens || 0
|
||||
const reasoningOn = modelId.includes("3-7") && budget_tokens !== 0 ? true : false
|
||||
const reasoningOn = baseModelId.includes("3-7") && budget_tokens !== 0 ? true : false
|
||||
|
||||
// Get model info and message indices for caching
|
||||
const userMsgIndices = messages.reduce((acc, msg, index) => (msg.role === "user" ? [...acc, index] : acc), [] as number[])
|
||||
@@ -167,12 +173,23 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
getModel(): { id: BedrockModelId; info: ModelInfo } {
|
||||
getModel(): { id: string; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
if (modelId && modelId in bedrockModels) {
|
||||
const id = modelId as BedrockModelId
|
||||
return { id, info: bedrockModels[id] }
|
||||
}
|
||||
|
||||
const customSelected = this.options.awsBedrockCustomSelected
|
||||
const baseModel = this.options.awsBedrockCustomModelBaseId
|
||||
if (customSelected && modelId && baseModel && baseModel in bedrockModels) {
|
||||
// Use the user-input model ID but inherit capabilities from the base model
|
||||
return {
|
||||
id: modelId,
|
||||
info: bedrockModels[baseModel],
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: bedrockDefaultModelId,
|
||||
info: bedrockModels[bedrockDefaultModelId],
|
||||
@@ -290,7 +307,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
modelId: string,
|
||||
model: { id: BedrockModelId; info: ModelInfo },
|
||||
model: { id: string; info: ModelInfo },
|
||||
): ApiStream {
|
||||
// Get Bedrock client with proper credentials
|
||||
const client = await this.getBedrockClient()
|
||||
@@ -476,13 +493,13 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
|
||||
/**
|
||||
* Creates a message using Amazon Nova models through AWS Bedrock
|
||||
* Implements support for Nova Micro, Nova Lite, and Nova Pro models
|
||||
* Implements support for Amazon Nova models
|
||||
*/
|
||||
private async *createNovaMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
modelId: string,
|
||||
model: { id: BedrockModelId; info: ModelInfo },
|
||||
model: { id: string; info: ModelInfo },
|
||||
): ApiStream {
|
||||
// Get Bedrock client with proper credentials
|
||||
const client = await this.getBedrockClient()
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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}`)
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 || "",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { execa } from "execa"
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./file"
|
||||
+272
-1
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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/**/*"],
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
25463
|
||||
Generated
+4295
-162
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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:<AWS Account
|
||||
ID>: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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
}
|
||||
Reference in New Issue
Block a user