mirror of
https://github.com/cline/cline.git
synced 2026-09-14 11:29:25 +08:00
Compare commits
26
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0b2c84606 | ||
|
|
345b6c03a0 | ||
|
|
53e58ce49f | ||
|
|
08bd431b0e | ||
|
|
5e6716645e | ||
|
|
8b8a422dc8 | ||
|
|
be1add1baa | ||
|
|
5147e28aaf | ||
|
|
c6e8b04b86 | ||
|
|
c0b3c69a8f | ||
|
|
080ed7c1c6 | ||
|
|
570ece3284 | ||
|
|
8f6f6464a0 | ||
|
|
8c565b5a7c | ||
|
|
cd1ff2ad25 | ||
|
|
d2979631d8 | ||
|
|
4dfc1358c5 | ||
|
|
6a96c183a3 | ||
|
|
9df023b9d0 | ||
|
|
19e4387b86 | ||
|
|
ab01a518d1 | ||
|
|
a66724e312 | ||
|
|
cc56486814 | ||
|
|
277b20a1b2 | ||
|
|
55d12d7556 | ||
|
|
a527acc56c |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix eternal loading states when the last message is a checkpoint
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Recent task list is now collapsible, allowing users to hide their recent tasks (e.g. when sharing their screen).
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Add detection for new users to display special components
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
selectImages protos migration
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Finishing the migration of Vscode Advanced settings to Settings Webview
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Add Tailwind CSS IntelliSense to the the recommended extensions list
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
new workflow feature
|
||||
@@ -1,4 +1,4 @@
|
||||
blank_issues_enabled: false
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: ✨ Feature Request
|
||||
url: https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop
|
||||
|
||||
+1
-1
@@ -19,4 +19,4 @@ coverage
|
||||
# But don't ignore the coverage scripts in .github/scripts/
|
||||
!.github/scripts/coverage/
|
||||
|
||||
*evals.env
|
||||
*evals.env
|
||||
|
||||
Vendored
+6
-1
@@ -1,5 +1,10 @@
|
||||
{
|
||||
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": ["dbaeumer.vscode-eslint", "connor4312.esbuild-problem-matchers", "ms-vscode.extension-test-runner"]
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"connor4312.esbuild-problem-matchers",
|
||||
"ms-vscode.extension-test-runner",
|
||||
"bradlc.vscode-tailwindcss"
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+17
@@ -38,6 +38,23 @@
|
||||
"IS_DEV": "true",
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Run Standalone Extension",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"sourceMaps": true,
|
||||
"resolveSourceMapLocations": ["${workspaceFolder}/**", "!**/node_modules/**"],
|
||||
"cwd": "${workspaceFolder}/dist-standalone",
|
||||
"outFiles": ["${workspaceFolder}/dist-standalone/**/*.js"],
|
||||
"preLaunchTask": "compile-standalone",
|
||||
"env": {
|
||||
"GRPC_TRACE": "all",
|
||||
"GRPC_VERBOSITY": "DEBUG",
|
||||
"NODE_PATH": "${workspaceFolder}/dist-standalone/node_modules"
|
||||
},
|
||||
"program": "standalone.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+10
@@ -3,6 +3,16 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "compile-standalone",
|
||||
"type": "npm",
|
||||
"script": "compile-standalone",
|
||||
"group": "build",
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
"reveal": "always"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "npm: protos",
|
||||
"type": "npm",
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
# Changelog
|
||||
|
||||
## [3.15.5]
|
||||
|
||||
- Fix inefficient memory management in the task timeline
|
||||
- Fix Gemini rate limitation response not being handled properly (Thanks @BarreiroT!)
|
||||
|
||||
## [3.15.4]
|
||||
|
||||
- Add gemini model back to vertex provider
|
||||
- Add gemini telemetry
|
||||
- Add filtering for tasks tied to the current workspace
|
||||
|
||||
## [3.15.3]
|
||||
|
||||
- Add Fireworks API Provider
|
||||
|
||||
+24
-5
@@ -4,6 +4,8 @@ const path = require("path")
|
||||
|
||||
const production = process.argv.includes("--production")
|
||||
const watch = process.argv.includes("--watch")
|
||||
const standalone = process.argv.includes("--standalone")
|
||||
const destDir = standalone ? "dist-standalone" : "dist"
|
||||
|
||||
/**
|
||||
* @type {import('esbuild').Plugin}
|
||||
@@ -85,7 +87,7 @@ const copyWasmFiles = {
|
||||
build.onEnd(() => {
|
||||
// tree sitter
|
||||
const sourceDir = path.join(__dirname, "node_modules", "web-tree-sitter")
|
||||
const targetDir = path.join(__dirname, "dist")
|
||||
const targetDir = path.join(__dirname, destDir)
|
||||
|
||||
// Copy tree-sitter.wasm
|
||||
fs.copyFileSync(path.join(sourceDir, "tree-sitter.wasm"), path.join(targetDir, "tree-sitter.wasm"))
|
||||
@@ -117,7 +119,8 @@ const copyWasmFiles = {
|
||||
},
|
||||
}
|
||||
|
||||
const extensionConfig = {
|
||||
// Base configuration shared between extension and standalone builds
|
||||
const baseConfig = {
|
||||
bundle: true,
|
||||
minify: production,
|
||||
sourcemap: !production,
|
||||
@@ -140,16 +143,32 @@ const extensionConfig = {
|
||||
},
|
||||
},
|
||||
],
|
||||
entryPoints: ["src/extension.ts"],
|
||||
format: "cjs",
|
||||
sourcesContent: false,
|
||||
platform: "node",
|
||||
outfile: "dist/extension.js",
|
||||
}
|
||||
|
||||
// Extension-specific configuration
|
||||
const extensionConfig = {
|
||||
...baseConfig,
|
||||
entryPoints: ["src/extension.ts"],
|
||||
outfile: `${destDir}/extension.js`,
|
||||
external: ["vscode"],
|
||||
}
|
||||
|
||||
// Standalone-specific configuration
|
||||
const standaloneConfig = {
|
||||
...baseConfig,
|
||||
entryPoints: ["src/standalone/standalone.ts"],
|
||||
outfile: `${destDir}/standalone.js`,
|
||||
// These gRPC protos need to load files from the module directory at runtime,
|
||||
// so they cannot be bundled.
|
||||
external: ["vscode", "@grpc/reflection", "grpc-health-check"],
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const extensionCtx = await esbuild.context(extensionConfig)
|
||||
const config = standalone ? standaloneConfig : extensionConfig
|
||||
const extensionCtx = await esbuild.context(config)
|
||||
if (watch) {
|
||||
await extensionCtx.watch()
|
||||
} else {
|
||||
|
||||
Generated
+1032
-94
File diff suppressed because it is too large
Load Diff
+11
-39
@@ -2,7 +2,7 @@
|
||||
"name": "claude-dev",
|
||||
"displayName": "Cline",
|
||||
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
|
||||
"version": "3.15.3",
|
||||
"version": "3.15.5",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
@@ -236,53 +236,20 @@
|
||||
},
|
||||
"configuration": {
|
||||
"title": "Cline",
|
||||
"properties": {
|
||||
"cline.enableCheckpoints": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Enables extension to save checkpoints of workspace throughout the task. Uses git under the hood which may not work well with large workspaces."
|
||||
},
|
||||
"cline.preferredLanguage": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"English",
|
||||
"Arabic - العربية",
|
||||
"Portuguese - Português (Brasil)",
|
||||
"Czech - Čeština",
|
||||
"French - Français",
|
||||
"German - Deutsch",
|
||||
"Hindi - हिन्दी",
|
||||
"Hungarian - Magyar",
|
||||
"Italian - Italiano",
|
||||
"Japanese - 日本語",
|
||||
"Korean - 한국어",
|
||||
"Polish - Polski",
|
||||
"Portuguese - Português (Portugal)",
|
||||
"Russian - Русский",
|
||||
"Simplified Chinese - 简体中文",
|
||||
"Spanish - Español",
|
||||
"Traditional Chinese - 繁體中文",
|
||||
"Turkish - Türkçe"
|
||||
],
|
||||
"default": "English",
|
||||
"description": "The language that Cline should use for communication."
|
||||
},
|
||||
"cline.mcpMarketplace.enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Controls whether the MCP Marketplace is enabled."
|
||||
}
|
||||
}
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"vscode:prepublish": "npm run package",
|
||||
"compile": "npm run check-types && npm run lint && node esbuild.js",
|
||||
"compile-standalone": "npm run protos && npm run check-types && npm run lint && node esbuild.js --standalone",
|
||||
"postcompile-standalone": "node scripts/package-standalone.mjs",
|
||||
"watch": "npm-run-all -p watch:*",
|
||||
"watch:esbuild": "node esbuild.js --watch",
|
||||
"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 src/core/controller webview-ui/src/services --write",
|
||||
"protos": "node proto/build-proto.js && node scripts/generate-server-setup.mjs",
|
||||
"postprotos": "prettier src/shared/proto src/core/controller webview-ui/src/services src/standalone/server-setup.ts --write --log-level silent",
|
||||
"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",
|
||||
@@ -354,6 +321,7 @@
|
||||
"@google-cloud/vertexai": "^1.9.3",
|
||||
"@google/genai": "^0.9.0",
|
||||
"@grpc/grpc-js": "^1.9.15",
|
||||
"@grpc/reflection": "^1.0.4",
|
||||
"@mistralai/mistralai": "^1.5.0",
|
||||
"@modelcontextprotocol/sdk": "^1.7.0",
|
||||
"@opentelemetry/api": "^1.4.1",
|
||||
@@ -364,6 +332,7 @@
|
||||
"@opentelemetry/semantic-conventions": "^1.30.0",
|
||||
"@sentry/browser": "^9.12.0",
|
||||
"@vscode/codicons": "^0.0.36",
|
||||
"archiver": "^7.0.1",
|
||||
"axios": "^1.8.2",
|
||||
"cheerio": "^1.0.0",
|
||||
"chokidar": "^4.0.1",
|
||||
@@ -377,6 +346,7 @@
|
||||
"fzf": "^0.5.2",
|
||||
"get-folder-size": "^5.0.0",
|
||||
"globby": "^14.0.2",
|
||||
"grpc-health-check": "^2.0.2",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"ignore": "^7.0.3",
|
||||
"image-size": "^2.0.2",
|
||||
@@ -385,6 +355,7 @@
|
||||
"mammoth": "^1.8.0",
|
||||
"monaco-vscode-textmate-theme-converter": "^0.1.7",
|
||||
"ollama": "^0.5.13",
|
||||
"open": "^10.1.2",
|
||||
"open-graph-scraper": "^6.9.0",
|
||||
"openai": "^4.83.0",
|
||||
"os-name": "^6.0.0",
|
||||
@@ -398,6 +369,7 @@
|
||||
"simple-git": "^3.27.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"tree-sitter-wasms": "^0.1.11",
|
||||
"ts-morph": "^25.0.1",
|
||||
"turndown": "^7.2.0",
|
||||
"web-tree-sitter": "^0.22.6",
|
||||
"zod": "^3.24.2"
|
||||
|
||||
@@ -22,6 +22,9 @@ service FileService {
|
||||
|
||||
// Search git commits in the workspace
|
||||
rpc searchCommits(StringRequest) returns (GitCommits);
|
||||
|
||||
// Select images from the file system and return as data URLs
|
||||
rpc selectImages(EmptyRequest) returns (StringArray);
|
||||
|
||||
// Convert URIs to workspace-relative paths
|
||||
rpc getRelativePaths(RelativePathsRequest) returns (RelativePaths);
|
||||
@@ -82,6 +85,7 @@ message RuleFileRequest {
|
||||
bool is_global = 2; // Common field for all operations
|
||||
optional string rule_path = 3; // Path field for deleteRuleFile (optional)
|
||||
optional string filename = 4; // Filename field for createRuleFile (optional)
|
||||
optional string type = 5; // Type of the file to create (optional)
|
||||
}
|
||||
|
||||
// Result for rule file operations with meaningful data only
|
||||
|
||||
@@ -67,6 +67,7 @@ message GetTaskHistoryRequest {
|
||||
bool favorites_only = 2;
|
||||
string search_query = 3;
|
||||
string sort_by = 4;
|
||||
bool current_workspace_only = 5;
|
||||
}
|
||||
|
||||
// Response for task history
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import * as fs from "fs"
|
||||
import * as grpc from "@grpc/grpc-js"
|
||||
import * as protoLoader from "@grpc/proto-loader"
|
||||
import * as health from "grpc-health-check"
|
||||
import { fileURLToPath } from "url"
|
||||
import path from "path"
|
||||
|
||||
const OUT_FILE = path.resolve("src/standalone/server-setup.ts")
|
||||
const DESCRIPTOR_SET = path.resolve("dist-standalone/proto/descriptor_set.pb")
|
||||
|
||||
// Load service definitions.
|
||||
const clineDef = protoLoader.loadFileDescriptorSetFromBuffer(fs.readFileSync(DESCRIPTOR_SET))
|
||||
const healthDef = protoLoader.loadSync(health.protoPath)
|
||||
const packageDefinition = { ...clineDef, ...healthDef }
|
||||
const proto = grpc.loadPackageDefinition(packageDefinition)
|
||||
|
||||
/**
|
||||
* Generate imports and function to add all the handlers to the server for all services defined in the proto files.
|
||||
*/
|
||||
function generateHandlersAndExports() {
|
||||
let imports = []
|
||||
let handlerSetup = []
|
||||
|
||||
for (const [name, def] of Object.entries(proto.cline)) {
|
||||
if (!def || !("service" in def)) {
|
||||
continue
|
||||
}
|
||||
const domain = name.replace(/Service$/, "")
|
||||
const dir = domain.charAt(0).toLowerCase() + domain.slice(1)
|
||||
imports.push(`// ${domain} Service`)
|
||||
handlerSetup.push(` // ${domain} Service`)
|
||||
handlerSetup.push(` server.addService(proto.cline.${name}.service, {`)
|
||||
for (const [rpcName, rpc] of Object.entries(def.service)) {
|
||||
imports.push(`import { ${rpcName} } from "../core/controller/${dir}/${rpcName}"`)
|
||||
if (rpc.requestStream) {
|
||||
throw new Error("Request streaming is not supported")
|
||||
}
|
||||
if (rpc.responseStream) {
|
||||
handlerSetup.push(` ${rpcName}: wrapStreamingResponse(${rpcName}, controller),`)
|
||||
} else {
|
||||
handlerSetup.push(` ${rpcName}: wrapper(${rpcName}, controller),`)
|
||||
}
|
||||
}
|
||||
handlerSetup.push(` });`)
|
||||
imports.push("")
|
||||
handlerSetup.push("")
|
||||
}
|
||||
return {
|
||||
imports: imports.join("\n"),
|
||||
handlerSetup: handlerSetup.join("\n"),
|
||||
}
|
||||
}
|
||||
|
||||
const { imports, handlerSetup } = generateHandlersAndExports()
|
||||
const scriptName = path.basename(fileURLToPath(import.meta.url))
|
||||
|
||||
// Create output file
|
||||
let output = `// GENERATED CODE -- DO NOT EDIT!
|
||||
// Generated by ${scriptName}
|
||||
import * as grpc from "@grpc/grpc-js"
|
||||
import { Controller } from "../core/controller"
|
||||
import { GrpcHandlerWrapper, GrpcStreamingResponseHandlerWrapper } from "./grpc-types"
|
||||
|
||||
${imports}
|
||||
|
||||
export function addServices(
|
||||
server: grpc.Server,
|
||||
proto: any,
|
||||
controller: Controller,
|
||||
wrapper: GrpcHandlerWrapper,
|
||||
wrapStreamingResponse: GrpcStreamingResponseHandlerWrapper,
|
||||
): void {
|
||||
${handlerSetup}
|
||||
}
|
||||
`
|
||||
// Write output file
|
||||
fs.writeFileSync(OUT_FILE, output)
|
||||
|
||||
console.log(`Generated service handlers in ${OUT_FILE}.`)
|
||||
@@ -0,0 +1,96 @@
|
||||
const fs = require("fs")
|
||||
const path = require("path")
|
||||
const { Project, SyntaxKind } = require("ts-morph")
|
||||
|
||||
function traverse(container, output, prefix = "") {
|
||||
for (const node of container.getStatements()) {
|
||||
const kind = node.getKind()
|
||||
|
||||
if (kind === SyntaxKind.ModuleDeclaration) {
|
||||
const name = node.getName().replace(/^['"]|['"]$/g, "")
|
||||
var fullPrefix
|
||||
if (prefix) {
|
||||
fullPrefix = `${prefix}.${name}`
|
||||
} else {
|
||||
fullPrefix = name
|
||||
}
|
||||
output.push(`${fullPrefix} = {};`)
|
||||
const body = node.getBody()
|
||||
if (body && body.getKind() === SyntaxKind.ModuleBlock) {
|
||||
traverse(body, output, fullPrefix)
|
||||
}
|
||||
} else if (kind === SyntaxKind.FunctionDeclaration) {
|
||||
const name = node.getName()
|
||||
const params = node.getParameters().map((p, i) => sanitizeParam(p.getName(), i))
|
||||
const typeNode = node.getReturnTypeNode()
|
||||
const returnType = typeNode ? typeNode.getText() : ""
|
||||
const ret = mapReturn(returnType)
|
||||
output.push(
|
||||
`${prefix}.${name} = function(${params.join(", ")}) { console.log('Called stubbed function: ${prefix}.${name}'); ${ret} };`,
|
||||
)
|
||||
} else if (kind === SyntaxKind.EnumDeclaration) {
|
||||
const name = node.getName()
|
||||
const members = node.getMembers().map((m) => m.getName())
|
||||
output.push(`${prefix}.${name} = { ${members.map((m) => `${m}: 0`).join(", ")} };`)
|
||||
} else if (kind === SyntaxKind.VariableStatement) {
|
||||
for (const decl of node.getDeclarations()) {
|
||||
const name = decl.getName()
|
||||
output.push(`${prefix}.${name} = createStub("${prefix}.${name}");`)
|
||||
}
|
||||
} else if (kind == SyntaxKind.ClassDeclaration) {
|
||||
const name = node.getName()
|
||||
output.push(
|
||||
`${prefix}.${name} = class { constructor(...args) {
|
||||
console.log('Constructed stubbed class: new ${prefix}.${name}(', args, ')');
|
||||
return createStub(${prefix}.${name});
|
||||
}};`,
|
||||
)
|
||||
} else if (kind === SyntaxKind.TypeAliasDeclaration || kind === SyntaxKind.InterfaceDeclaration) {
|
||||
//console.log("Skipping", SyntaxKind[kind], node.getName())
|
||||
// Skip interfaces and type aliases because they are only used at compile time by typescript.
|
||||
} else {
|
||||
console.log("Can't handle: ", SyntaxKind[kind])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mapReturn(typeStr) {
|
||||
if (!typeStr) return ""
|
||||
if (typeStr.includes("void")) return ""
|
||||
if (typeStr.includes("string")) return `return '';`
|
||||
if (typeStr.includes("number")) return `return 0;`
|
||||
if (typeStr.includes("boolean")) return `return false;`
|
||||
if (typeStr.includes("[]")) return `return [];`
|
||||
if (typeStr.includes("Thenable")) return `return Promise.resolve(null);`
|
||||
return `return createStub("unknown");`
|
||||
}
|
||||
|
||||
function sanitizeParam(name, index) {
|
||||
return name || `arg${index}`
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const inputPath = "node_modules/@types/vscode/index.d.ts"
|
||||
const outputPath = "standalone/runtime-files/vscode/vscode-stubs.js"
|
||||
|
||||
const project = new Project()
|
||||
const sourceFile = project.addSourceFileAtPath(inputPath)
|
||||
|
||||
const output = []
|
||||
output.push("// GENERATED CODE -- DO NOT EDIT!")
|
||||
output.push('console.log("Loading stubs...");')
|
||||
output.push('const { createStub } = require("./stub-utils")')
|
||||
traverse(sourceFile, output)
|
||||
output.push("module.exports = vscode;")
|
||||
output.push('console.log("Finished loading stubs");')
|
||||
|
||||
fs.mkdirSync(path.dirname(outputPath), { recursive: true })
|
||||
fs.writeFileSync(outputPath, output.join("\n"))
|
||||
|
||||
console.log(`Wrote vscode SDK stubs to ${outputPath}`)
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eu
|
||||
|
||||
DIR=${1:-src/}
|
||||
DEST_DIR=dist-standalone
|
||||
DEST=dist-standalone/vscode-uses.txt
|
||||
mkdir -p $DEST_DIR
|
||||
|
||||
{
|
||||
git grep -h 'vscode\.' $DIR |
|
||||
grep -Ev '//.*vscode' | # remove commented out code
|
||||
sed 's|.*vscode\.|vscode.|'| # remove everything before vscode.
|
||||
sed 's/[^a-zA-Z0-9_.].*$//' | # remove everything after last identifier
|
||||
sort | uniq > $DEST
|
||||
}
|
||||
|
||||
echo Done, wrote uses of the vscode SDK to $(realpath $DEST)
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
import { glob } from "glob"
|
||||
import archiver from "archiver"
|
||||
import { cp } from "fs/promises"
|
||||
import { execSync } from "child_process"
|
||||
|
||||
const BUILD_DIR = "dist-standalone"
|
||||
const SOURCE_DIR = "standalone/runtime-files"
|
||||
|
||||
await cp(SOURCE_DIR, BUILD_DIR, { recursive: true })
|
||||
|
||||
// Run npm install in the distribution directory
|
||||
console.log("Running npm install in distribution directory...")
|
||||
const cwd = process.cwd()
|
||||
process.chdir(BUILD_DIR)
|
||||
try {
|
||||
execSync("npm install", { stdio: "inherit" })
|
||||
} catch (error) {
|
||||
console.error("Error running npm install:", error)
|
||||
process.exit(1)
|
||||
} finally {
|
||||
process.chdir(cwd)
|
||||
}
|
||||
|
||||
// Check for native .node modules.
|
||||
const nativeModules = await glob("**/*.node", { cwd: BUILD_DIR, nodir: true })
|
||||
if (nativeModules.length > 0) {
|
||||
console.error("Native node modules cannot be included in the standalone distribution:\n", nativeModules.join("\n"))
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Zip the build directory (excluding any pre-existing output zip).
|
||||
const zipPath = path.join(BUILD_DIR, "standalone.zip")
|
||||
const output = fs.createWriteStream(zipPath)
|
||||
const archive = archiver("zip", { zlib: { level: 9 } })
|
||||
|
||||
output.on("close", () => {
|
||||
console.log(`Created ${zipPath} (${archive.pointer()} bytes)`)
|
||||
})
|
||||
|
||||
archive.on("error", (err) => {
|
||||
throw err
|
||||
})
|
||||
|
||||
archive.pipe(output)
|
||||
archive.glob("**/*", {
|
||||
cwd: BUILD_DIR,
|
||||
ignore: ["standalone.zip"],
|
||||
})
|
||||
await archive.finalize()
|
||||
@@ -33,7 +33,7 @@ export class ClineHandler implements ApiHandler {
|
||||
systemPrompt,
|
||||
messages,
|
||||
this.getModel(),
|
||||
this.options.o3MiniReasoningEffort,
|
||||
this.options.reasoningEffort,
|
||||
this.options.thinkingBudgetTokens,
|
||||
this.options.openRouterProviderSorting,
|
||||
)
|
||||
|
||||
+103
-44
@@ -1,11 +1,12 @@
|
||||
import type { Anthropic } from "@anthropic-ai/sdk"
|
||||
// Restore GenerateContentConfig import and add GenerateContentResponseUsageMetadata
|
||||
import { GoogleGenAI, type Content, type GenerateContentConfig, type GenerateContentResponseUsageMetadata } from "@google/genai"
|
||||
import { GoogleGenAI, type GenerateContentConfig, type GenerateContentResponseUsageMetadata } from "@google/genai"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiHandler } from "../"
|
||||
import { ApiHandlerOptions, geminiDefaultModelId, GeminiModelId, geminiModels, ModelInfo } from "@shared/api"
|
||||
import { convertAnthropicMessageToGemini } from "../transform/gemini-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { telemetryService } from "@services/posthog/telemetry/TelemetryService"
|
||||
|
||||
// Define a default TTL for the cache (e.g., 15 minutes in seconds)
|
||||
const DEFAULT_CACHE_TTL_SECONDS = 900
|
||||
@@ -74,7 +75,7 @@ export class GeminiHandler implements ApiHandler {
|
||||
*/
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const { id: model, info } = this.getModel()
|
||||
const { id: modelId, info } = this.getModel()
|
||||
const contents = messages.map(convertAnthropicMessageToGemini)
|
||||
|
||||
// Configure thinking budget if supported
|
||||
@@ -98,52 +99,110 @@ export class GeminiHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
// Generate content using the configured parameters
|
||||
const result = await this.client.models.generateContentStream({
|
||||
model,
|
||||
contents: contents,
|
||||
config: {
|
||||
...requestConfig,
|
||||
},
|
||||
})
|
||||
|
||||
// Track usage metadata
|
||||
const sdkCallStartTime = Date.now()
|
||||
let sdkFirstChunkTime: number | undefined
|
||||
let ttftSdkMs: number | undefined
|
||||
let apiSuccess = false
|
||||
let apiError: string | undefined
|
||||
let promptTokens = 0
|
||||
let outputTokens = 0
|
||||
let cacheReadTokens = 0
|
||||
let lastUsageMetadata: GenerateContentResponseUsageMetadata | undefined
|
||||
|
||||
// Process the stream
|
||||
for await (const chunk of result) {
|
||||
if (chunk.text) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: chunk.text,
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.usageMetadata) {
|
||||
lastUsageMetadata = chunk.usageMetadata
|
||||
}
|
||||
}
|
||||
|
||||
// Yield usage information at the end
|
||||
if (lastUsageMetadata) {
|
||||
const inputTokens = lastUsageMetadata.promptTokenCount ?? 0
|
||||
const outputTokens = lastUsageMetadata.candidatesTokenCount ?? 0
|
||||
const cacheReadTokens = lastUsageMetadata.cachedContentTokenCount
|
||||
|
||||
// Calculate immediate costs
|
||||
const totalCost = this.calculateCost({
|
||||
info,
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
cacheReadTokens,
|
||||
try {
|
||||
const result = await this.client.models.generateContentStream({
|
||||
model: modelId,
|
||||
contents: contents,
|
||||
config: {
|
||||
...requestConfig,
|
||||
},
|
||||
})
|
||||
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
cacheReadTokens,
|
||||
cacheWriteTokens: 0,
|
||||
totalCost,
|
||||
let isFirstSdkChunk = true
|
||||
for await (const chunk of result) {
|
||||
if (isFirstSdkChunk) {
|
||||
sdkFirstChunkTime = Date.now()
|
||||
ttftSdkMs = sdkFirstChunkTime - sdkCallStartTime
|
||||
isFirstSdkChunk = false
|
||||
}
|
||||
|
||||
if (chunk.text) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: chunk.text,
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.usageMetadata) {
|
||||
lastUsageMetadata = chunk.usageMetadata
|
||||
promptTokens = lastUsageMetadata.promptTokenCount ?? promptTokens
|
||||
outputTokens = lastUsageMetadata.candidatesTokenCount ?? outputTokens
|
||||
cacheReadTokens = lastUsageMetadata.cachedContentTokenCount ?? cacheReadTokens
|
||||
}
|
||||
}
|
||||
apiSuccess = true
|
||||
|
||||
if (lastUsageMetadata) {
|
||||
const totalCost = this.calculateCost({
|
||||
info,
|
||||
inputTokens: promptTokens,
|
||||
outputTokens,
|
||||
cacheReadTokens,
|
||||
})
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: promptTokens,
|
||||
outputTokens,
|
||||
cacheReadTokens,
|
||||
cacheWriteTokens: 0,
|
||||
totalCost,
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
apiSuccess = false
|
||||
// Let the error propagate to be handled by withRetry or Task.ts
|
||||
// Telemetry will be sent in the finally block.
|
||||
if (error instanceof Error) {
|
||||
apiError = error.message
|
||||
|
||||
// Gemini doesn't include status codes in their errors
|
||||
// https://github.com/googleapis/js-genai/blob/61f7f27b866c74333ca6331883882489bcb708b9/src/_api_client.ts#L569
|
||||
if (error.name === "ClientError" && error.message.includes("got status: 429 Too Many Requests.")) {
|
||||
;(error as any).status = 429
|
||||
}
|
||||
} else {
|
||||
apiError = String(error)
|
||||
}
|
||||
|
||||
throw error
|
||||
} finally {
|
||||
const sdkCallEndTime = Date.now()
|
||||
const totalDurationSdkMs = sdkCallEndTime - sdkCallStartTime
|
||||
const cacheHit = cacheReadTokens > 0
|
||||
const cacheHitPercentage = promptTokens > 0 ? (cacheReadTokens / promptTokens) * 100 : undefined
|
||||
const throughputTokensPerSecSdk =
|
||||
totalDurationSdkMs > 0 && outputTokens > 0 ? outputTokens / (totalDurationSdkMs / 1000) : undefined
|
||||
|
||||
if (this.options.taskId) {
|
||||
telemetryService.captureGeminiApiPerformance(
|
||||
this.options.taskId,
|
||||
modelId,
|
||||
{
|
||||
ttftSec: ttftSdkMs !== undefined ? ttftSdkMs / 1000 : undefined,
|
||||
totalDurationSec: totalDurationSdkMs / 1000,
|
||||
promptTokens,
|
||||
outputTokens,
|
||||
cacheReadTokens,
|
||||
cacheHit,
|
||||
cacheHitPercentage,
|
||||
apiSuccess,
|
||||
apiError,
|
||||
throughputTokensPerSec: throughputTokensPerSecSdk,
|
||||
},
|
||||
true,
|
||||
)
|
||||
} else {
|
||||
console.warn("GeminiHandler: taskId not available for telemetry in createMessage.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,8 +66,9 @@ export class OpenAiNativeHandler implements ApiHandler {
|
||||
messages: [{ role: "developer", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
reasoning_effort: (this.options.o3MiniReasoningEffort as ChatCompletionReasoningEffort) || "medium",
|
||||
reasoning_effort: (this.options.reasoningEffort as ChatCompletionReasoningEffort) || "medium",
|
||||
})
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
if (delta?.content) {
|
||||
|
||||
@@ -65,7 +65,7 @@ export class OpenAiHandler implements ApiHandler {
|
||||
if (isReasoningModelFamily) {
|
||||
openAiMessages = [{ role: "developer", content: systemPrompt }, ...convertToOpenAiMessages(messages)]
|
||||
temperature = undefined // does not support temperature
|
||||
reasoningEffort = (this.options.o3MiniReasoningEffort as ChatCompletionReasoningEffort) || "medium"
|
||||
reasoningEffort = (this.options.reasoningEffort as ChatCompletionReasoningEffort) || "medium"
|
||||
}
|
||||
|
||||
const stream = await this.client.chat.completions.create({
|
||||
|
||||
@@ -35,7 +35,7 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
systemPrompt,
|
||||
messages,
|
||||
this.getModel(),
|
||||
this.options.o3MiniReasoningEffort,
|
||||
this.options.reasoningEffort,
|
||||
this.options.thinkingBudgetTokens,
|
||||
this.options.openRouterProviderSorting,
|
||||
)
|
||||
|
||||
@@ -32,7 +32,7 @@ export class RequestyHandler implements ApiHandler {
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
const reasoningEffort = this.options.o3MiniReasoningEffort || "medium"
|
||||
const reasoningEffort = this.options.reasoningEffort || "medium"
|
||||
const reasoning = { reasoning_effort: reasoningEffort }
|
||||
const reasoningArgs = model.id.startsWith("openai/o") ? reasoning : {}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ export async function createOpenRouterStream(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
model: { id: string; info: ModelInfo },
|
||||
o3MiniReasoningEffort?: string,
|
||||
reasoningEffort?: string,
|
||||
thinkingBudgetTokens?: number,
|
||||
openRouterProviderSorting?: string,
|
||||
) {
|
||||
@@ -144,7 +144,7 @@ export async function createOpenRouterStream(
|
||||
stream_options: { include_usage: true },
|
||||
transforms: shouldApplyMiddleOutTransform ? ["middle-out"] : undefined,
|
||||
include_reasoning: true,
|
||||
...(model.id.startsWith("openai/o") ? { reasoning_effort: o3MiniReasoningEffort || "medium" } : {}),
|
||||
...(model.id.startsWith("openai/o") ? { reasoning_effort: reasoningEffort || "medium" } : {}),
|
||||
...(reasoning ? { reasoning } : {}),
|
||||
...(openRouterProviderSorting ? { provider: { sort: openRouterProviderSorting } } : {}),
|
||||
})
|
||||
|
||||
@@ -8,45 +8,6 @@ import { getGlobalState, getWorkspaceState, updateGlobalState, updateWorkspaceSt
|
||||
import * as vscode from "vscode"
|
||||
import { synchronizeRuleToggles, getRuleFilesTotalContent } from "@core/context/instructions/user-instructions/rule-helpers"
|
||||
|
||||
/**
|
||||
* Converts .clinerules file to directory and places old .clinerule file inside directory, renaming it
|
||||
* Doesn't do anything if .clinerules dir already exists or doesn't exist
|
||||
* Returns whether there are any uncaught errors
|
||||
*/
|
||||
export async function ensureLocalClinerulesDirExists(cwd: string): Promise<boolean> {
|
||||
const clinerulePath = path.resolve(cwd, GlobalFileNames.clineRules)
|
||||
const defaultRuleFilename = "default-rules.md"
|
||||
|
||||
try {
|
||||
const exists = await fileExistsAtPath(clinerulePath)
|
||||
|
||||
if (exists && !(await isDirectory(clinerulePath))) {
|
||||
// logic to convert .clinerules file into directory, and rename the rules file to {defaultRuleFilename}
|
||||
const content = await fs.readFile(clinerulePath, "utf8")
|
||||
const tempPath = clinerulePath + ".bak"
|
||||
await fs.rename(clinerulePath, tempPath) // create backup
|
||||
try {
|
||||
await fs.mkdir(clinerulePath, { recursive: true })
|
||||
await fs.writeFile(path.join(clinerulePath, defaultRuleFilename), content, "utf8")
|
||||
await fs.unlink(tempPath).catch(() => {}) // delete backup
|
||||
|
||||
return false // conversion successful with no errors
|
||||
} catch (conversionError) {
|
||||
// attempt to restore backup on conversion failure
|
||||
try {
|
||||
await fs.rm(clinerulePath, { recursive: true, force: true }).catch(() => {})
|
||||
await fs.rename(tempPath, clinerulePath) // restore backup
|
||||
} catch (restoreError) {}
|
||||
return true // in either case here we consider this an error
|
||||
}
|
||||
}
|
||||
// exists and is a dir or doesn't exist, either of these cases we dont need to handle here
|
||||
return false
|
||||
} catch (error) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export const getGlobalClineRules = async (globalClineRulesFilePath: string, toggles: ClineRulesToggles) => {
|
||||
if (await fileExistsAtPath(globalClineRulesFilePath)) {
|
||||
if (await isDirectory(globalClineRulesFilePath)) {
|
||||
@@ -80,7 +41,8 @@ export const getLocalClineRules = async (cwd: string, toggles: ClineRulesToggles
|
||||
if (await fileExistsAtPath(clineRulesFilePath)) {
|
||||
if (await isDirectory(clineRulesFilePath)) {
|
||||
try {
|
||||
const rulesFilePaths = await readDirectory(clineRulesFilePath)
|
||||
const rulesFilePaths = await readDirectory(clineRulesFilePath, [[".clinerules", "workflows"]])
|
||||
|
||||
const rulesFilesTotalContent = await getRuleFilesTotalContent(rulesFilePaths, cwd, toggles)
|
||||
if (rulesFilesTotalContent) {
|
||||
clineRulesFileInstructions = formatResponse.clineRulesLocalDirectoryInstructions(cwd, rulesFilesTotalContent)
|
||||
@@ -121,7 +83,9 @@ export async function refreshClineRulesToggles(
|
||||
// Local toggles
|
||||
const localClineRulesToggles = ((await getWorkspaceState(context, "localClineRulesToggles")) as ClineRulesToggles) || {}
|
||||
const localClineRulesFilePath = path.resolve(workingDirectory, GlobalFileNames.clineRules)
|
||||
const updatedLocalToggles = await synchronizeRuleToggles(localClineRulesFilePath, localClineRulesToggles)
|
||||
const updatedLocalToggles = await synchronizeRuleToggles(localClineRulesFilePath, localClineRulesToggles, "", [
|
||||
[".clinerules", "workflows"],
|
||||
])
|
||||
await updateWorkspaceState(context, "localClineRulesToggles", updatedLocalToggles)
|
||||
|
||||
return {
|
||||
@@ -129,82 +93,3 @@ export async function refreshClineRulesToggles(
|
||||
localToggles: updatedLocalToggles,
|
||||
}
|
||||
}
|
||||
|
||||
export const createRuleFile = async (isGlobal: boolean, filename: string, cwd: string) => {
|
||||
try {
|
||||
let filePath: string
|
||||
if (isGlobal) {
|
||||
const globalClineRulesFilePath = await ensureRulesDirectoryExists()
|
||||
filePath = path.join(globalClineRulesFilePath, filename)
|
||||
} else {
|
||||
const localClineRulesFilePath = path.resolve(cwd, GlobalFileNames.clineRules)
|
||||
|
||||
const hasError = await ensureLocalClinerulesDirExists(cwd)
|
||||
if (hasError === true) {
|
||||
return { filePath: null, fileExists: false }
|
||||
}
|
||||
|
||||
await fs.mkdir(localClineRulesFilePath, { recursive: true })
|
||||
|
||||
filePath = path.join(localClineRulesFilePath, filename)
|
||||
}
|
||||
|
||||
const fileExists = await fileExistsAtPath(filePath)
|
||||
|
||||
if (fileExists) {
|
||||
return { filePath, fileExists }
|
||||
}
|
||||
|
||||
await fs.writeFile(filePath, "", "utf8")
|
||||
|
||||
return { filePath, fileExists: false }
|
||||
} catch (error) {
|
||||
return { filePath: null, fileExists: false }
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteRuleFile(
|
||||
context: vscode.ExtensionContext,
|
||||
rulePath: string,
|
||||
isGlobal: boolean,
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
// Check if file exists
|
||||
const fileExists = await fileExistsAtPath(rulePath)
|
||||
if (!fileExists) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Rule file does not exist: ${rulePath}`,
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the file from disk
|
||||
await fs.unlink(rulePath)
|
||||
|
||||
// Get the filename for messages
|
||||
const fileName = path.basename(rulePath)
|
||||
|
||||
// Update the appropriate toggles
|
||||
if (isGlobal) {
|
||||
const toggles = ((await getGlobalState(context, "globalClineRulesToggles")) as ClineRulesToggles) || {}
|
||||
delete toggles[rulePath]
|
||||
await updateGlobalState(context, "globalClineRulesToggles", toggles)
|
||||
} else {
|
||||
const toggles = ((await getWorkspaceState(context, "localClineRulesToggles")) as ClineRulesToggles) || {}
|
||||
delete toggles[rulePath]
|
||||
await updateWorkspaceState(context, "localClineRulesToggles", toggles)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Rule file "${fileName}" deleted successfully`,
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
console.error(`Error deleting rule file: ${errorMessage}`, error)
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed to delete rule file.`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import { fileExistsAtPath, isDirectory, readDirectory } from "@utils/fs"
|
||||
import { ensureRulesDirectoryExists, GlobalFileNames } from "@core/storage/disk"
|
||||
import { getGlobalState, getWorkspaceState, updateGlobalState, updateWorkspaceState } from "@core/storage/state"
|
||||
import * as path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { ClineRulesToggles } from "@shared/cline-rules"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
/**
|
||||
* Recursively traverses directory and finds all files, including checking for optional whitelisted file extension
|
||||
*/
|
||||
export async function readDirectoryRecursive(directoryPath: string, allowedFileExtension: string): Promise<string[]> {
|
||||
export async function readDirectoryRecursive(
|
||||
directoryPath: string,
|
||||
allowedFileExtension: string,
|
||||
excludedPaths: string[][] = [],
|
||||
): Promise<string[]> {
|
||||
try {
|
||||
const entries = await readDirectory(directoryPath)
|
||||
const entries = await readDirectory(directoryPath, excludedPaths)
|
||||
let results: string[] = []
|
||||
for (const entry of entries) {
|
||||
if (allowedFileExtension !== "") {
|
||||
@@ -33,6 +40,7 @@ export async function synchronizeRuleToggles(
|
||||
rulesDirectoryPath: string,
|
||||
currentToggles: ClineRulesToggles,
|
||||
allowedFileExtension: string = "",
|
||||
excludedPaths: string[][] = [],
|
||||
): Promise<ClineRulesToggles> {
|
||||
// Create a copy of toggles to modify
|
||||
const updatedToggles = { ...currentToggles }
|
||||
@@ -45,7 +53,7 @@ export async function synchronizeRuleToggles(
|
||||
|
||||
if (isDir) {
|
||||
// DIRECTORY CASE
|
||||
const filePaths = await readDirectoryRecursive(rulesDirectoryPath, allowedFileExtension)
|
||||
const filePaths = await readDirectoryRecursive(rulesDirectoryPath, allowedFileExtension, excludedPaths)
|
||||
const existingRulePaths = new Set<string>()
|
||||
|
||||
for (const filePath of filePaths) {
|
||||
@@ -119,3 +127,155 @@ export const getRuleFilesTotalContent = async (rulesFilePaths: string[], basePat
|
||||
).then((contents) => contents.filter(Boolean).join("\n\n"))
|
||||
return ruleFilesTotalContent
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles converting any directory into a file (specifically used for .clinerules and .clinerules/workflows)
|
||||
* The old .clinerules file or .clinerules/workflows file will be renamed to a default filename
|
||||
* Doesn't do anything if the dir already exists or doesn't exist
|
||||
* Returns whether there are any uncaught errors
|
||||
*/
|
||||
export async function ensureLocalClineDirExists(clinerulePath: string, defaultRuleFilename: string): Promise<boolean> {
|
||||
try {
|
||||
const exists = await fileExistsAtPath(clinerulePath)
|
||||
|
||||
if (exists && !(await isDirectory(clinerulePath))) {
|
||||
// logic to convert .clinerules file into directory, and rename the rules file to {defaultRuleFilename}
|
||||
const content = await fs.readFile(clinerulePath, "utf8")
|
||||
const tempPath = clinerulePath + ".bak"
|
||||
await fs.rename(clinerulePath, tempPath) // create backup
|
||||
try {
|
||||
await fs.mkdir(clinerulePath, { recursive: true })
|
||||
await fs.writeFile(path.join(clinerulePath, defaultRuleFilename), content, "utf8")
|
||||
await fs.unlink(tempPath).catch(() => {}) // delete backup
|
||||
|
||||
return false // conversion successful with no errors
|
||||
} catch (conversionError) {
|
||||
// attempt to restore backup on conversion failure
|
||||
try {
|
||||
await fs.rm(clinerulePath, { recursive: true, force: true }).catch(() => {})
|
||||
await fs.rename(tempPath, clinerulePath) // restore backup
|
||||
} catch (restoreError) {}
|
||||
return true // in either case here we consider this an error
|
||||
}
|
||||
}
|
||||
// exists and is a dir or doesn't exist, either of these cases we dont need to handle here
|
||||
return false
|
||||
} catch (error) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a rule file or workflow file
|
||||
*/
|
||||
export const createRuleFile = async (isGlobal: boolean, filename: string, cwd: string, type: string) => {
|
||||
try {
|
||||
let filePath: string
|
||||
if (isGlobal) {
|
||||
// global means its implicitly clinerules
|
||||
const globalClineRulesFilePath = await ensureRulesDirectoryExists()
|
||||
filePath = path.join(globalClineRulesFilePath, filename)
|
||||
} else {
|
||||
const localClineRulesFilePath = path.resolve(cwd, GlobalFileNames.clineRules)
|
||||
|
||||
const hasError = await ensureLocalClineDirExists(localClineRulesFilePath, "default-rules.md")
|
||||
if (hasError === true) {
|
||||
return { filePath: null, fileExists: false }
|
||||
}
|
||||
|
||||
await fs.mkdir(localClineRulesFilePath, { recursive: true })
|
||||
|
||||
if (type === "workflow") {
|
||||
const localWorkflowsFilePath = path.resolve(cwd, GlobalFileNames.workflows)
|
||||
|
||||
const hasError = await ensureLocalClineDirExists(localWorkflowsFilePath, "default-workflows.md")
|
||||
if (hasError === true) {
|
||||
return { filePath: null, fileExists: false }
|
||||
}
|
||||
|
||||
await fs.mkdir(localWorkflowsFilePath, { recursive: true })
|
||||
|
||||
filePath = path.join(localWorkflowsFilePath, filename)
|
||||
} else {
|
||||
// clinerules file creation
|
||||
filePath = path.join(localClineRulesFilePath, filename)
|
||||
}
|
||||
}
|
||||
|
||||
const fileExists = await fileExistsAtPath(filePath)
|
||||
|
||||
if (fileExists) {
|
||||
return { filePath, fileExists }
|
||||
}
|
||||
|
||||
await fs.writeFile(filePath, "", "utf8")
|
||||
|
||||
return { filePath, fileExists: false }
|
||||
} catch (error) {
|
||||
return { filePath: null, fileExists: false }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a rule file or workflow file
|
||||
*/
|
||||
export async function deleteRuleFile(
|
||||
context: vscode.ExtensionContext,
|
||||
rulePath: string,
|
||||
isGlobal: boolean,
|
||||
type: string,
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
// Check if file exists
|
||||
const fileExists = await fileExistsAtPath(rulePath)
|
||||
if (!fileExists) {
|
||||
return {
|
||||
success: false,
|
||||
message: `File does not exist: ${rulePath}`,
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the file from disk
|
||||
await fs.unlink(rulePath)
|
||||
|
||||
// Get the filename for messages
|
||||
const fileName = path.basename(rulePath)
|
||||
|
||||
// Update the appropriate toggles
|
||||
if (isGlobal) {
|
||||
const toggles = ((await getGlobalState(context, "globalClineRulesToggles")) as ClineRulesToggles) || {}
|
||||
delete toggles[rulePath]
|
||||
await updateGlobalState(context, "globalClineRulesToggles", toggles)
|
||||
} else {
|
||||
if (type === "workflow") {
|
||||
const toggles = ((await getWorkspaceState(context, "workflowToggles")) as ClineRulesToggles) || {}
|
||||
delete toggles[rulePath]
|
||||
await updateWorkspaceState(context, "workflowToggles", toggles)
|
||||
} else if (type === "cursor") {
|
||||
const toggles = ((await getWorkspaceState(context, "localCursorRulesToggles")) as ClineRulesToggles) || {}
|
||||
delete toggles[rulePath]
|
||||
await updateWorkspaceState(context, "localCursorRulesToggles", toggles)
|
||||
} else if (type === "windsurf") {
|
||||
const toggles = ((await getWorkspaceState(context, "localWindsurfRulesToggles")) as ClineRulesToggles) || {}
|
||||
delete toggles[rulePath]
|
||||
await updateWorkspaceState(context, "localWindsurfRulesToggles", toggles)
|
||||
} else {
|
||||
const toggles = ((await getWorkspaceState(context, "localClineRulesToggles")) as ClineRulesToggles) || {}
|
||||
delete toggles[rulePath]
|
||||
await updateWorkspaceState(context, "localClineRulesToggles", toggles)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `File "${fileName}" deleted successfully`,
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
console.error(`Error deleting file: ${errorMessage}`, error)
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed to delete file.`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import path from "path"
|
||||
import { GlobalFileNames } from "@core/storage/disk"
|
||||
import { ClineRulesToggles } from "@shared/cline-rules"
|
||||
import { getWorkspaceState, updateWorkspaceState } from "@core/storage/state"
|
||||
import * as vscode from "vscode"
|
||||
import { synchronizeRuleToggles } from "@core/context/instructions/user-instructions/rule-helpers"
|
||||
|
||||
/**
|
||||
* Refresh the workflow toggles
|
||||
*/
|
||||
export async function refreshWorkflowToggles(
|
||||
context: vscode.ExtensionContext,
|
||||
workingDirectory: string,
|
||||
): Promise<ClineRulesToggles> {
|
||||
const workflowRulesToggles = ((await getWorkspaceState(context, "workflowToggles")) as ClineRulesToggles) || {}
|
||||
const workflowsDirPath = path.resolve(workingDirectory, GlobalFileNames.workflows)
|
||||
const updatedWorkflowToggles = await synchronizeRuleToggles(workflowsDirPath, workflowRulesToggles)
|
||||
await updateWorkspaceState(context, "workflowToggles", updatedWorkflowToggles)
|
||||
return updatedWorkflowToggles
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
import { Controller } from ".."
|
||||
import { RuleFileRequest, RuleFile } from "@shared/proto/file"
|
||||
import { FileMethodHandler } from "./index"
|
||||
import {
|
||||
createRuleFile as createRuleFileImpl,
|
||||
refreshClineRulesToggles,
|
||||
} from "@core/context/instructions/user-instructions/cline-rules"
|
||||
import { refreshClineRulesToggles } from "@core/context/instructions/user-instructions/cline-rules"
|
||||
import { createRuleFile as createRuleFileImpl } from "@core/context/instructions/user-instructions/rule-helpers"
|
||||
import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
import { handleFileServiceRequest } from "./index"
|
||||
import { cwd } from "@core/task"
|
||||
import { refreshWorkflowToggles } from "@/core/context/instructions/user-instructions/workflows"
|
||||
|
||||
/**
|
||||
* Creates a rule file in either global or workspace rules directory
|
||||
@@ -18,32 +17,45 @@ import { cwd } from "@core/task"
|
||||
* @throws Error if operation fails
|
||||
*/
|
||||
export const createRuleFile: FileMethodHandler = async (controller: Controller, request: RuleFileRequest): Promise<RuleFile> => {
|
||||
if (typeof request.isGlobal !== "boolean" || typeof request.filename !== "string" || !request.filename) {
|
||||
if (
|
||||
typeof request.isGlobal !== "boolean" ||
|
||||
!request.filename ||
|
||||
typeof request.filename !== "string" ||
|
||||
!request.type ||
|
||||
typeof request.type !== "string"
|
||||
) {
|
||||
console.error("createRuleFile: Missing or invalid parameters", {
|
||||
isGlobal: typeof request.isGlobal === "boolean" ? request.isGlobal : `Invalid: ${typeof request.isGlobal}`,
|
||||
filename: typeof request.filename === "string" ? request.filename : `Invalid: ${typeof request.filename}`,
|
||||
type: typeof request.type === "string" ? request.type : `Invalid: ${typeof request.type}`,
|
||||
})
|
||||
throw new Error("Missing or invalid parameters")
|
||||
}
|
||||
|
||||
const { filePath, fileExists } = await createRuleFileImpl(request.isGlobal, request.filename, cwd)
|
||||
const { filePath, fileExists } = await createRuleFileImpl(request.isGlobal, request.filename, cwd, request.type)
|
||||
|
||||
if (!filePath) {
|
||||
throw new Error("Failed to create rule file.")
|
||||
throw new Error("Failed to create file.")
|
||||
}
|
||||
|
||||
const fileTypeName = request.type === "workflow" ? "workflow" : "rule"
|
||||
|
||||
if (fileExists) {
|
||||
vscode.window.showWarningMessage(`Rule file "${request.filename}" already exists.`)
|
||||
vscode.window.showWarningMessage(`${fileTypeName} file "${request.filename}" already exists.`)
|
||||
// Still open it for editing
|
||||
await handleFileServiceRequest(controller, "openFile", { value: filePath })
|
||||
} else {
|
||||
await refreshClineRulesToggles(controller.context, cwd)
|
||||
if (request.type === "workflow") {
|
||||
await refreshWorkflowToggles(controller.context, cwd)
|
||||
} else {
|
||||
await refreshClineRulesToggles(controller.context, cwd)
|
||||
}
|
||||
await controller.postStateToWebview()
|
||||
|
||||
await handleFileServiceRequest(controller, "openFile", { value: filePath })
|
||||
|
||||
vscode.window.showInformationMessage(
|
||||
`Created new ${request.isGlobal ? "global" : "workspace"} rule file: ${request.filename}`,
|
||||
`Created new ${request.isGlobal ? "global" : "workspace"} ${fileTypeName} file: ${request.filename}`,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Controller } from ".."
|
||||
import { RuleFileRequest, RuleFile } from "@shared/proto/file"
|
||||
import { FileMethodHandler } from "./index"
|
||||
import {
|
||||
deleteRuleFile as deleteRuleFileImpl,
|
||||
refreshClineRulesToggles,
|
||||
} from "@core/context/instructions/user-instructions/cline-rules"
|
||||
import { refreshClineRulesToggles } from "@core/context/instructions/user-instructions/cline-rules"
|
||||
import { deleteRuleFile as deleteRuleFileImpl } from "@core/context/instructions/user-instructions/rule-helpers"
|
||||
import { refreshExternalRulesToggles } from "@core/context/instructions/user-instructions/external-rules"
|
||||
import { refreshWorkflowToggles } from "@core/context/instructions/user-instructions/workflows"
|
||||
import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
import { cwd } from "@core/task"
|
||||
@@ -18,26 +17,38 @@ import { cwd } from "@core/task"
|
||||
* @throws Error if operation fails
|
||||
*/
|
||||
export const deleteRuleFile: FileMethodHandler = async (controller: Controller, request: RuleFileRequest): Promise<RuleFile> => {
|
||||
if (typeof request.isGlobal !== "boolean" || typeof request.rulePath !== "string" || !request.rulePath) {
|
||||
if (
|
||||
typeof request.isGlobal !== "boolean" ||
|
||||
typeof request.rulePath !== "string" ||
|
||||
!request.rulePath ||
|
||||
!request.type ||
|
||||
typeof request.type !== "string"
|
||||
) {
|
||||
console.error("deleteRuleFile: Missing or invalid parameters", {
|
||||
isGlobal: typeof request.isGlobal === "boolean" ? request.isGlobal : `Invalid: ${typeof request.isGlobal}`,
|
||||
rulePath: typeof request.rulePath === "string" ? request.rulePath : `Invalid: ${typeof request.rulePath}`,
|
||||
type: typeof request.type === "string" ? request.type : `Invalid: ${typeof request.type}`,
|
||||
})
|
||||
throw new Error("Missing or invalid parameters")
|
||||
}
|
||||
|
||||
const result = await deleteRuleFileImpl(controller.context, request.rulePath, request.isGlobal)
|
||||
const result = await deleteRuleFileImpl(controller.context, request.rulePath, request.isGlobal, request.type)
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.message || "Failed to delete rule file")
|
||||
}
|
||||
|
||||
await refreshClineRulesToggles(controller.context, cwd)
|
||||
await refreshExternalRulesToggles(controller.context, cwd)
|
||||
// we refresh inside of the deleteRuleFileImpl(..) call
|
||||
//await refreshClineRulesToggles(controller.context, cwd)
|
||||
//await refreshExternalRulesToggles(controller.context, cwd)
|
||||
//await refreshWorkflowToggles(controller.context, cwd)
|
||||
await controller.postStateToWebview()
|
||||
|
||||
const fileName = path.basename(request.rulePath)
|
||||
vscode.window.showInformationMessage(`Rule file "${fileName}" deleted successfully`)
|
||||
|
||||
const fileTypeName = request.type === "workflow" ? "workflow" : "rule"
|
||||
|
||||
vscode.window.showInformationMessage(`${fileTypeName} file "${fileName}" deleted successfully`)
|
||||
|
||||
return RuleFile.create({
|
||||
filePath: request.rulePath,
|
||||
|
||||
@@ -10,6 +10,7 @@ import { openFile } from "./openFile"
|
||||
import { openImage } from "./openImage"
|
||||
import { searchCommits } from "./searchCommits"
|
||||
import { searchFiles } from "./searchFiles"
|
||||
import { selectImages } from "./selectImages"
|
||||
|
||||
// Register all file service methods
|
||||
export function registerAllMethods(): void {
|
||||
@@ -21,4 +22,5 @@ export function registerAllMethods(): void {
|
||||
registerMethod("openImage", openImage)
|
||||
registerMethod("searchCommits", searchCommits)
|
||||
registerMethod("searchFiles", searchFiles)
|
||||
registerMethod("selectImages", selectImages)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Controller } from ".."
|
||||
import { EmptyRequest, StringArray } from "@shared/proto/common"
|
||||
import { selectImages as selectImagesIntegration } from "@integrations/misc/process-images"
|
||||
import { FileMethodHandler } from "./index"
|
||||
|
||||
/**
|
||||
* Prompts the user to select images from the file system and returns them as data URLs
|
||||
* @param controller The controller instance
|
||||
* @param request Empty request, no parameters needed
|
||||
* @returns Array of image data URLs
|
||||
*/
|
||||
export const selectImages: FileMethodHandler = async (controller: Controller, request: EmptyRequest): Promise<StringArray> => {
|
||||
try {
|
||||
const images = await selectImagesIntegration()
|
||||
return StringArray.create({ values: images })
|
||||
} catch (error) {
|
||||
console.error("Error selecting images:", error)
|
||||
// Return empty array on error
|
||||
return StringArray.create({ values: [] })
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@ import { cleanupLegacyCheckpoints } from "@integrations/checkpoints/CheckpointMi
|
||||
import { downloadTask } from "@integrations/misc/export-markdown"
|
||||
import { fetchOpenGraphData } from "@integrations/misc/link-preview"
|
||||
import { handleFileServiceRequest } from "./file"
|
||||
import { selectImages } from "@integrations/misc/process-images"
|
||||
import { getTheme } from "@integrations/theme/getTheme"
|
||||
import WorkspaceTracker from "@integrations/workspace/WorkspaceTracker"
|
||||
import { ClineAccountService } from "@services/account/ClineAccountService"
|
||||
@@ -51,6 +50,7 @@ import { ClineRulesToggles } from "@shared/cline-rules"
|
||||
import { sendStateUpdate } from "./state/subscribeToState"
|
||||
import { refreshClineRulesToggles } from "@core/context/instructions/user-instructions/cline-rules"
|
||||
import { refreshExternalRulesToggles } from "@core/context/instructions/user-instructions/external-rules"
|
||||
import { refreshWorkflowToggles } from "@core/context/instructions/user-instructions/workflows"
|
||||
|
||||
/*
|
||||
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
||||
@@ -145,8 +145,19 @@ export class Controller {
|
||||
browserSettings,
|
||||
chatSettings,
|
||||
shellIntegrationTimeout,
|
||||
enableCheckpointsSetting,
|
||||
isNewUser,
|
||||
taskHistory,
|
||||
} = await getAllExtensionState(this.context)
|
||||
|
||||
const NEW_USER_TASK_COUNT_THRESHOLD = 10
|
||||
|
||||
// Check if the user has completed enough tasks to no longer be considered a "new user"
|
||||
if (isNewUser && !historyItem && taskHistory && taskHistory.length >= NEW_USER_TASK_COUNT_THRESHOLD) {
|
||||
await updateGlobalState(this.context, "isNewUser", false)
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
|
||||
if (autoApprovalSettings) {
|
||||
const updatedAutoApprovalSettings = {
|
||||
...autoApprovalSettings,
|
||||
@@ -168,6 +179,7 @@ export class Controller {
|
||||
browserSettings,
|
||||
chatSettings,
|
||||
shellIntegrationTimeout,
|
||||
enableCheckpointsSetting ?? true,
|
||||
customInstructions,
|
||||
task,
|
||||
images,
|
||||
@@ -318,19 +330,13 @@ export class Controller {
|
||||
await updateGlobalState(this.context, "lastShownAnnouncementId", this.latestAnnouncementId)
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
case "selectImages":
|
||||
const images = await selectImages()
|
||||
await this.postMessageToWebview({
|
||||
type: "selectedImages",
|
||||
images,
|
||||
})
|
||||
break
|
||||
case "resetState":
|
||||
await this.resetState()
|
||||
break
|
||||
case "refreshClineRules":
|
||||
await refreshClineRulesToggles(this.context, cwd)
|
||||
await refreshExternalRulesToggles(this.context, cwd)
|
||||
await refreshWorkflowToggles(this.context, cwd)
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
case "openInBrowser":
|
||||
@@ -482,6 +488,16 @@ export class Controller {
|
||||
}
|
||||
break
|
||||
}
|
||||
case "toggleWorkflow": {
|
||||
const { workflowPath, enabled } = message
|
||||
if (workflowPath && typeof enabled === "boolean") {
|
||||
const toggles = ((await getWorkspaceState(this.context, "workflowToggles")) as ClineRulesToggles) || {}
|
||||
toggles[workflowPath] = enabled
|
||||
await updateWorkspaceState(this.context, "workflowToggles", toggles)
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
break
|
||||
}
|
||||
case "requestTotalTasksSize": {
|
||||
this.refreshTotalTasksSize()
|
||||
break
|
||||
@@ -563,6 +579,22 @@ export class Controller {
|
||||
// plan act setting
|
||||
await updateGlobalState(this.context, "planActSeparateModelsSetting", message.planActSeparateModelsSetting)
|
||||
|
||||
if (typeof message.enableCheckpointsSetting === "boolean") {
|
||||
await updateGlobalState(this.context, "enableCheckpointsSetting", message.enableCheckpointsSetting)
|
||||
}
|
||||
|
||||
if (typeof message.mcpMarketplaceEnabled === "boolean") {
|
||||
await updateGlobalState(this.context, "mcpMarketplaceEnabled", message.mcpMarketplaceEnabled)
|
||||
}
|
||||
|
||||
// chat settings (including preferredLanguage and openAIReasoningEffort)
|
||||
if (message.chatSettings) {
|
||||
await updateGlobalState(this.context, "chatSettings", message.chatSettings)
|
||||
if (this.task) {
|
||||
this.task.chatSettings = message.chatSettings
|
||||
}
|
||||
}
|
||||
|
||||
// after settings are updated, post state to webview
|
||||
await this.postStateToWebview()
|
||||
|
||||
@@ -1332,8 +1364,10 @@ export class Controller {
|
||||
mcpMarketplaceEnabled,
|
||||
telemetrySetting,
|
||||
planActSeparateModelsSetting,
|
||||
enableCheckpointsSetting,
|
||||
globalClineRulesToggles,
|
||||
shellIntegrationTimeout,
|
||||
isNewUser,
|
||||
} = await getAllExtensionState(this.context)
|
||||
|
||||
const localClineRulesToggles =
|
||||
@@ -1345,6 +1379,8 @@ export class Controller {
|
||||
const localCursorRulesToggles =
|
||||
((await getWorkspaceState(this.context, "localCursorRulesToggles")) as ClineRulesToggles) || {}
|
||||
|
||||
const workflowToggles = ((await getWorkspaceState(this.context, "workflowToggles")) as ClineRulesToggles) || {}
|
||||
|
||||
return {
|
||||
version: this.context.extension?.packageJSON?.version ?? "",
|
||||
apiConfiguration,
|
||||
@@ -1366,12 +1402,15 @@ export class Controller {
|
||||
mcpMarketplaceEnabled,
|
||||
telemetrySetting,
|
||||
planActSeparateModelsSetting,
|
||||
enableCheckpointsSetting: enableCheckpointsSetting ?? true,
|
||||
vscMachineId: vscode.env.machineId,
|
||||
globalClineRulesToggles: globalClineRulesToggles || {},
|
||||
localClineRulesToggles: localClineRulesToggles || {},
|
||||
localWindsurfRulesToggles: localWindsurfRulesToggles || {},
|
||||
localCursorRulesToggles: localCursorRulesToggles || {},
|
||||
workflowToggles: workflowToggles || {},
|
||||
shellIntegrationTimeout,
|
||||
isNewUser,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Controller } from ".."
|
||||
import { GetTaskHistoryRequest, TaskHistoryArray } from "../../../shared/proto/task"
|
||||
import { getGlobalState } from "../../storage/state"
|
||||
import { getWorkspacePath, arePathsEqual } from "../../../utils/path"
|
||||
|
||||
/**
|
||||
* Gets filtered task history
|
||||
@@ -10,22 +11,49 @@ import { getGlobalState } from "../../storage/state"
|
||||
*/
|
||||
export async function getTaskHistory(controller: Controller, request: GetTaskHistoryRequest): Promise<TaskHistoryArray> {
|
||||
try {
|
||||
const { favoritesOnly, searchQuery, sortBy } = request
|
||||
const { favoritesOnly, currentWorkspaceOnly, searchQuery, sortBy } = request
|
||||
|
||||
// Get task history from global state
|
||||
const taskHistory = ((await getGlobalState(controller.context, "taskHistory")) as any[]) || []
|
||||
const workspacePath = getWorkspacePath()
|
||||
|
||||
// Apply filters
|
||||
let filteredTasks = taskHistory.filter((item) => {
|
||||
// Basic filter: must have timestamp and task content
|
||||
const hasRequiredFields = item.ts && item.task
|
||||
|
||||
// Apply favorites filter if requested
|
||||
if (favoritesOnly && hasRequiredFields) {
|
||||
return item.isFavorited === true
|
||||
if (!hasRequiredFields) {
|
||||
return false
|
||||
}
|
||||
|
||||
return hasRequiredFields
|
||||
// Apply favorites filter if requested
|
||||
if (favoritesOnly && !item.isFavorited) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Apply current workspace filter if requested
|
||||
if (currentWorkspaceOnly) {
|
||||
let isInWorkspace = false
|
||||
|
||||
// First check the cwdOnTaskInitialization property - Only present on tasks from this change forward
|
||||
if (item.cwdOnTaskInitialization) {
|
||||
if (arePathsEqual(item.cwdOnTaskInitialization, workspacePath)) {
|
||||
isInWorkspace = true
|
||||
}
|
||||
}
|
||||
|
||||
// For tasks without cwdOnTaskInitialization, check the older shadowGitConfigWorkTree property
|
||||
if (!isInWorkspace && item.shadowGitConfigWorkTree) {
|
||||
if (arePathsEqual(item.shadowGitConfigWorkTree, workspacePath)) {
|
||||
isInWorkspace = true
|
||||
}
|
||||
}
|
||||
|
||||
if (!isInWorkspace) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
// Apply search if provided
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { newTaskToolResponse, condenseToolResponse, newRuleToolResponse, reportBugToolResponse } from "../prompts/commands"
|
||||
import { ClineRulesToggles } from "@shared/cline-rules"
|
||||
import fs from "fs/promises"
|
||||
|
||||
/**
|
||||
* Processes text for slash commands and transforms them with appropriate instructions
|
||||
* This is called after parseMentions() to process any slash commands in the user's message
|
||||
*/
|
||||
export function parseSlashCommands(text: string): { processedText: string; needsClinerulesFileCheck: boolean } {
|
||||
const SUPPORTED_COMMANDS = ["newtask", "smol", "compact", "newrule", "reportbug"]
|
||||
export async function parseSlashCommands(
|
||||
text: string,
|
||||
workflowToggles: ClineRulesToggles,
|
||||
): Promise<{ processedText: string; needsClinerulesFileCheck: boolean }> {
|
||||
const SUPPORTED_DEFAULT_COMMANDS = ["newtask", "smol", "compact", "newrule", "reportbug"]
|
||||
|
||||
const commandReplacements: Record<string, string> = {
|
||||
newtask: newTaskToolResponse(),
|
||||
@@ -17,10 +22,10 @@ export function parseSlashCommands(text: string): { processedText: string; needs
|
||||
|
||||
// this currently allows matching prepended whitespace prior to /slash-command
|
||||
const tagPatterns = [
|
||||
{ tag: "task", regex: /<task>(\s*\/([a-zA-Z0-9_-]+))(\s+.+?)?\s*<\/task>/is },
|
||||
{ tag: "feedback", regex: /<feedback>(\s*\/([a-zA-Z0-9_-]+))(\s+.+?)?\s*<\/feedback>/is },
|
||||
{ tag: "answer", regex: /<answer>(\s*\/([a-zA-Z0-9_-]+))(\s+.+?)?\s*<\/answer>/is },
|
||||
{ tag: "user_message", regex: /<user_message>(\s*\/([a-zA-Z0-9_-]+))(\s+.+?)?\s*<\/user_message>/is },
|
||||
{ tag: "task", regex: /<task>(\s*\/([a-zA-Z0-9_\.-]+))(\s+.+?)?\s*<\/task>/is },
|
||||
{ tag: "feedback", regex: /<feedback>(\s*\/([a-zA-Z0-9_\.-]+))(\s+.+?)?\s*<\/feedback>/is },
|
||||
{ tag: "answer", regex: /<answer>(\s*\/([a-zA-Z0-9_\.-]+))(\s+.+?)?\s*<\/answer>/is },
|
||||
{ tag: "user_message", regex: /<user_message>(\s*\/([a-zA-Z0-9_\.-]+))(\s+.+?)?\s*<\/user_message>/is },
|
||||
]
|
||||
|
||||
// if we find a valid match, we will return inside that block
|
||||
@@ -34,7 +39,8 @@ export function parseSlashCommands(text: string): { processedText: string; needs
|
||||
|
||||
const commandName = match[2] // casing matters
|
||||
|
||||
if (SUPPORTED_COMMANDS.includes(commandName)) {
|
||||
// we give preference to the default commands if the user has a file with the same name
|
||||
if (SUPPORTED_DEFAULT_COMMANDS.includes(commandName)) {
|
||||
const fullMatchStartIndex = match.index
|
||||
|
||||
// find position of slash command within the full match
|
||||
@@ -51,6 +57,48 @@ export function parseSlashCommands(text: string): { processedText: string; needs
|
||||
|
||||
return { processedText: processedText, needsClinerulesFileCheck: commandName === "newrule" ? true : false }
|
||||
}
|
||||
|
||||
// in practice we want to minimize this work, so we only do it if theres a possible match
|
||||
const enabledWorkflows = Object.entries(workflowToggles)
|
||||
.filter(([_, enabled]) => enabled)
|
||||
.map(([filePath, _]) => {
|
||||
const fileName = filePath.replace(/^.*[/\\]/, "")
|
||||
|
||||
return {
|
||||
fullPath: filePath,
|
||||
fileName: fileName,
|
||||
}
|
||||
})
|
||||
|
||||
// Then check if the command matches any enabled workflow filename
|
||||
const matchingWorkflow = enabledWorkflows.find((workflow) => workflow.fileName === commandName)
|
||||
|
||||
if (matchingWorkflow) {
|
||||
try {
|
||||
// Read workflow file content from the full path
|
||||
const workflowContent = (await fs.readFile(matchingWorkflow.fullPath, "utf8")).trim()
|
||||
|
||||
// find position of slash command within the full match
|
||||
const fullMatchStartIndex = match.index
|
||||
const fullMatch = match[0]
|
||||
const relativeStartIndex = fullMatch.indexOf(match[1])
|
||||
|
||||
// calculate absolute indices in the original string
|
||||
const slashCommandStartIndex = fullMatchStartIndex + relativeStartIndex
|
||||
const slashCommandEndIndex = slashCommandStartIndex + match[1].length
|
||||
|
||||
// remove the slash command and add custom instructions at the top of this message
|
||||
const textWithoutSlashCommand =
|
||||
text.substring(0, slashCommandStartIndex) + text.substring(slashCommandEndIndex)
|
||||
const processedText =
|
||||
`<explicit_instructions type="${matchingWorkflow.fileName}">\n${workflowContent}\n</explicit_instructions>\n` +
|
||||
textWithoutSlashCommand
|
||||
|
||||
return { processedText, needsClinerulesFileCheck: false }
|
||||
} catch (error) {
|
||||
console.error(`Error reading workflow file ${matchingWorkflow.fullPath}: ${error}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ export const GlobalFileNames = {
|
||||
openRouterModels: "openrouter_models.json",
|
||||
mcpSettings: "cline_mcp_settings.json",
|
||||
clineRules: ".clinerules",
|
||||
workflows: ".clinerules/workflows",
|
||||
cursorRulesDir: ".cursor/rules",
|
||||
cursorRulesFile: ".cursorrules",
|
||||
windsurfRules: ".windsurfrules",
|
||||
|
||||
@@ -83,8 +83,11 @@ export type GlobalStateKey =
|
||||
| "thinkingBudgetTokens"
|
||||
| "reasoningEffort"
|
||||
| "planActSeparateModelsSetting"
|
||||
| "enableCheckpointsSetting"
|
||||
| "mcpMarketplaceEnabled"
|
||||
| "favoritedModelIds"
|
||||
| "requestTimeoutMs"
|
||||
| "shellIntegrationTimeout"
|
||||
| "isNewUser"
|
||||
|
||||
export type LocalStateKey = "localClineRulesToggles"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as vscode from "vscode"
|
||||
import { DEFAULT_CHAT_SETTINGS } from "@shared/ChatSettings"
|
||||
import { DEFAULT_CHAT_SETTINGS, OpenAIReasoningEffort } from "@shared/ChatSettings"
|
||||
import { DEFAULT_BROWSER_SETTINGS } from "@shared/BrowserSettings"
|
||||
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "@shared/AutoApprovalSettings"
|
||||
import { GlobalStateKey, SecretKey } from "./state-keys"
|
||||
@@ -51,8 +51,32 @@ export async function getWorkspaceState(context: vscode.ExtensionContext, key: s
|
||||
return await context.workspaceState.get(key)
|
||||
}
|
||||
|
||||
async function migrateMcpMarketplaceEnableSetting(mcpMarketplaceEnabledRaw: boolean | undefined): Promise<boolean> {
|
||||
const config = vscode.workspace.getConfiguration("cline")
|
||||
const mcpMarketplaceEnabled = config.get<boolean>("mcpMarketplace.enabled")
|
||||
if (mcpMarketplaceEnabled !== undefined) {
|
||||
// Remove from VSCode configuration
|
||||
await config.update("mcpMarketplace.enabled", undefined, true)
|
||||
|
||||
return !mcpMarketplaceEnabled
|
||||
}
|
||||
return mcpMarketplaceEnabledRaw ?? true
|
||||
}
|
||||
|
||||
async function migrateEnableCheckpointsSetting(enableCheckpointsSettingRaw: boolean | undefined): Promise<boolean> {
|
||||
const config = vscode.workspace.getConfiguration("cline")
|
||||
const enableCheckpoints = config.get<boolean>("enableCheckpoints")
|
||||
if (enableCheckpoints !== undefined) {
|
||||
// Remove from VSCode configuration
|
||||
await config.update("enableCheckpoints", undefined, true)
|
||||
return enableCheckpoints
|
||||
}
|
||||
return enableCheckpointsSettingRaw ?? true
|
||||
}
|
||||
|
||||
export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
const [
|
||||
isNewUser,
|
||||
storedApiProvider,
|
||||
apiModelId,
|
||||
apiKey,
|
||||
@@ -136,7 +160,10 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
globalClineRulesToggles,
|
||||
requestTimeoutMs,
|
||||
shellIntegrationTimeout,
|
||||
enableCheckpointsSettingRaw,
|
||||
mcpMarketplaceEnabledRaw,
|
||||
] = await Promise.all([
|
||||
getGlobalState(context, "isNewUser") as Promise<boolean | undefined>,
|
||||
getGlobalState(context, "apiProvider") as Promise<ApiProvider | undefined>,
|
||||
getGlobalState(context, "apiModelId") as Promise<string | undefined>,
|
||||
getSecret(context, "apiKey") as Promise<string | undefined>,
|
||||
@@ -220,6 +247,9 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
getGlobalState(context, "globalClineRulesToggles") as Promise<ClineRulesToggles | undefined>,
|
||||
getGlobalState(context, "requestTimeoutMs") as Promise<number | undefined>,
|
||||
getGlobalState(context, "shellIntegrationTimeout") as Promise<number | undefined>,
|
||||
getGlobalState(context, "enableCheckpointsSetting") as Promise<boolean | undefined>,
|
||||
getGlobalState(context, "mcpMarketplaceEnabled") as Promise<boolean | undefined>,
|
||||
fetch,
|
||||
])
|
||||
|
||||
let apiProvider: ApiProvider
|
||||
@@ -238,9 +268,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
|
||||
const localClineRulesToggles = (await getWorkspaceState(context, "localClineRulesToggles")) as ClineRulesToggles
|
||||
|
||||
const o3MiniReasoningEffort = vscode.workspace.getConfiguration("cline.modelSettings.o3Mini").get("reasoningEffort", "medium")
|
||||
|
||||
const mcpMarketplaceEnabled = vscode.workspace.getConfiguration("cline").get<boolean>("mcpMarketplace.enabled", true)
|
||||
const mcpMarketplaceEnabled = await migrateMcpMarketplaceEnableSetting(mcpMarketplaceEnabledRaw)
|
||||
const enableCheckpointsSetting = await migrateEnableCheckpointsSetting(enableCheckpointsSettingRaw)
|
||||
|
||||
// Plan/Act separate models setting is a boolean indicating whether the user wants to use different models for plan and act. Existing users expect this to be enabled, while we want new users to opt in to this being disabled by default.
|
||||
// On win11 state sometimes initializes as empty string instead of undefined
|
||||
@@ -309,7 +338,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
openRouterModelInfo,
|
||||
openRouterProviderSorting,
|
||||
vsCodeLmModelSelector,
|
||||
o3MiniReasoningEffort,
|
||||
thinkingBudgetTokens,
|
||||
reasoningEffort,
|
||||
liteLlmBaseUrl,
|
||||
@@ -328,6 +356,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
favoritedModelIds,
|
||||
requestTimeoutMs,
|
||||
},
|
||||
isNewUser: isNewUser ?? true,
|
||||
lastShownAnnouncementId,
|
||||
customInstructions,
|
||||
taskHistory,
|
||||
@@ -335,7 +364,10 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
globalClineRulesToggles: globalClineRulesToggles || {},
|
||||
localClineRulesToggles: localClineRulesToggles || {},
|
||||
browserSettings: { ...DEFAULT_BROWSER_SETTINGS, ...browserSettings }, // this will ensure that older versions of browserSettings (e.g. before remoteBrowserEnabled was added) are merged with the default values (false for remoteBrowserEnabled)
|
||||
chatSettings: chatSettings || DEFAULT_CHAT_SETTINGS,
|
||||
chatSettings: {
|
||||
...DEFAULT_CHAT_SETTINGS, // Apply defaults first
|
||||
...(chatSettings || {}), // Spread fetched chatSettings, which includes preferredLanguage, and openAIReasoningEffort
|
||||
},
|
||||
userInfo,
|
||||
previousModeApiProvider,
|
||||
previousModeModelId,
|
||||
@@ -345,9 +377,10 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
previousModeReasoningEffort,
|
||||
previousModeAwsBedrockCustomSelected,
|
||||
previousModeAwsBedrockCustomModelBaseId,
|
||||
mcpMarketplaceEnabled,
|
||||
mcpMarketplaceEnabled: mcpMarketplaceEnabled,
|
||||
telemetrySetting: telemetrySetting || "unset",
|
||||
planActSeparateModelsSetting,
|
||||
enableCheckpointsSetting: enableCheckpointsSetting,
|
||||
shellIntegrationTimeout: shellIntegrationTimeout || 4000,
|
||||
}
|
||||
}
|
||||
|
||||
+89
-28
@@ -80,6 +80,7 @@ import {
|
||||
ensureTaskDirectoryExists,
|
||||
getSavedApiConversationHistory,
|
||||
getSavedClineMessages,
|
||||
GlobalFileNames,
|
||||
saveApiConversationHistory,
|
||||
saveClineMessages,
|
||||
} from "@core/storage/disk"
|
||||
@@ -87,13 +88,14 @@ import {
|
||||
getGlobalClineRules,
|
||||
getLocalClineRules,
|
||||
refreshClineRulesToggles,
|
||||
ensureLocalClinerulesDirExists,
|
||||
} from "@core/context/instructions/user-instructions/cline-rules"
|
||||
import { ensureLocalClineDirExists } from "../context/instructions/user-instructions/rule-helpers"
|
||||
import {
|
||||
refreshExternalRulesToggles,
|
||||
getLocalWindsurfRules,
|
||||
getLocalCursorRules,
|
||||
} from "@core/context/instructions/user-instructions/external-rules"
|
||||
import { refreshWorkflowToggles } from "../context/instructions/user-instructions/workflows"
|
||||
import { getGlobalState } from "@core/storage/state"
|
||||
import { parseSlashCommands } from "@core/slash-commands"
|
||||
import WorkspaceTracker from "@integrations/workspace/WorkspaceTracker"
|
||||
@@ -167,6 +169,7 @@ export class Task {
|
||||
private didAlreadyUseTool = false
|
||||
private didCompleteReadingStream = false
|
||||
private didAutomaticallyRetryFailedApiRequest = false
|
||||
private enableCheckpoints: boolean
|
||||
|
||||
constructor(
|
||||
context: vscode.ExtensionContext,
|
||||
@@ -182,6 +185,7 @@ export class Task {
|
||||
browserSettings: BrowserSettings,
|
||||
chatSettings: ChatSettings,
|
||||
shellIntegrationTimeout: number,
|
||||
enableCheckpointsSetting: boolean,
|
||||
customInstructions?: string,
|
||||
task?: string,
|
||||
images?: string[],
|
||||
@@ -207,6 +211,7 @@ export class Task {
|
||||
this.autoApprovalSettings = autoApprovalSettings
|
||||
this.browserSettings = browserSettings
|
||||
this.chatSettings = chatSettings
|
||||
this.enableCheckpoints = enableCheckpointsSetting
|
||||
|
||||
// Initialize taskId first
|
||||
if (historyItem) {
|
||||
@@ -222,11 +227,19 @@ export class Task {
|
||||
// Initialize file context tracker
|
||||
this.fileContextTracker = new FileContextTracker(context, this.taskId)
|
||||
this.modelContextTracker = new ModelContextTracker(context, this.taskId)
|
||||
// Now that taskId is initialized, we can build the API handler
|
||||
this.api = buildApiHandler({
|
||||
|
||||
// Prepare effective API configuration
|
||||
let effectiveApiConfiguration: ApiConfiguration = {
|
||||
...apiConfiguration,
|
||||
taskId: this.taskId,
|
||||
})
|
||||
}
|
||||
|
||||
if (apiConfiguration.apiProvider === "openai" || apiConfiguration.apiProvider === "openai-native") {
|
||||
effectiveApiConfiguration.reasoningEffort = chatSettings.openAIReasoningEffort
|
||||
}
|
||||
|
||||
// Now that taskId is initialized, we can build the API handler
|
||||
this.api = buildApiHandler(effectiveApiConfiguration)
|
||||
|
||||
// Set taskId on browserSession for telemetry tracking
|
||||
this.browserSession.setTaskId(this.taskId)
|
||||
@@ -314,6 +327,7 @@ export class Task {
|
||||
totalCost: apiMetrics.totalCost,
|
||||
size: taskDirSize,
|
||||
shadowGitConfigWorkTree: await this.checkpointTracker?.getShadowGitConfigWorkTree(),
|
||||
cwdOnTaskInitialization: cwd,
|
||||
conversationHistoryDeletedRange: this.conversationHistoryDeletedRange,
|
||||
isFavorited: this.taskIsFavorited,
|
||||
})
|
||||
@@ -341,9 +355,19 @@ export class Task {
|
||||
break
|
||||
case "taskAndWorkspace":
|
||||
case "workspace":
|
||||
if (!this.enableCheckpoints) {
|
||||
vscode.window.showErrorMessage("Checkpoints are disabled in settings.")
|
||||
didWorkspaceRestoreFail = true
|
||||
break
|
||||
}
|
||||
|
||||
if (!this.checkpointTracker && !this.checkpointTrackerErrorMessage) {
|
||||
try {
|
||||
this.checkpointTracker = await CheckpointTracker.create(this.taskId, this.context.globalStorageUri.fsPath)
|
||||
this.checkpointTracker = await CheckpointTracker.create(
|
||||
this.taskId,
|
||||
this.context.globalStorageUri.fsPath,
|
||||
this.enableCheckpoints,
|
||||
)
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error"
|
||||
console.error("Failed to initialize checkpoint tracker:", errorMessage)
|
||||
@@ -450,6 +474,11 @@ export class Task {
|
||||
const relinquishButton = () => {
|
||||
this.postMessageToWebview({ type: "relinquishControl" })
|
||||
}
|
||||
if (!this.enableCheckpoints) {
|
||||
vscode.window.showInformationMessage("Checkpoints are disabled in settings. Cannot show diff.")
|
||||
relinquishButton()
|
||||
return
|
||||
}
|
||||
|
||||
console.log("presentMultifileDiff", messageTs)
|
||||
const messageIndex = this.clineMessages.findIndex((m) => m.ts === messageTs)
|
||||
@@ -467,9 +496,13 @@ export class Task {
|
||||
}
|
||||
|
||||
// TODO: handle if this is called from outside original workspace, in which case we need to show user error message we can't show diff outside of workspace?
|
||||
if (!this.checkpointTracker && !this.checkpointTrackerErrorMessage) {
|
||||
if (!this.checkpointTracker && this.enableCheckpoints && !this.checkpointTrackerErrorMessage) {
|
||||
try {
|
||||
this.checkpointTracker = await CheckpointTracker.create(this.taskId, this.context.globalStorageUri.fsPath)
|
||||
this.checkpointTracker = await CheckpointTracker.create(
|
||||
this.taskId,
|
||||
this.context.globalStorageUri.fsPath,
|
||||
this.enableCheckpoints,
|
||||
)
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error"
|
||||
console.error("Failed to initialize checkpoint tracker:", errorMessage)
|
||||
@@ -567,6 +600,10 @@ export class Task {
|
||||
}
|
||||
|
||||
async doesLatestTaskCompletionHaveNewChanges() {
|
||||
if (!this.enableCheckpoints) {
|
||||
return false
|
||||
}
|
||||
|
||||
const messageIndex = findLastIndex(this.clineMessages, (m) => m.say === "completion_result")
|
||||
const message = this.clineMessages[messageIndex]
|
||||
if (!message) {
|
||||
@@ -579,9 +616,13 @@ export class Task {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!this.checkpointTracker && !this.checkpointTrackerErrorMessage) {
|
||||
if (this.enableCheckpoints && !this.checkpointTracker && !this.checkpointTrackerErrorMessage) {
|
||||
try {
|
||||
this.checkpointTracker = await CheckpointTracker.create(this.taskId, this.context.globalStorageUri.fsPath)
|
||||
this.checkpointTracker = await CheckpointTracker.create(
|
||||
this.taskId,
|
||||
this.context.globalStorageUri.fsPath,
|
||||
this.enableCheckpoints,
|
||||
)
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error"
|
||||
console.error("Failed to initialize checkpoint tracker:", errorMessage)
|
||||
@@ -1080,6 +1121,10 @@ export class Task {
|
||||
// Checkpoints
|
||||
|
||||
async saveCheckpoint(isAttemptCompletionMessage: boolean = false) {
|
||||
if (!this.enableCheckpoints) {
|
||||
// If checkpoints are disabled, do nothing.
|
||||
return
|
||||
}
|
||||
// Set isCheckpointCheckedOut to false for all checkpoint_created messages
|
||||
this.clineMessages.forEach((message) => {
|
||||
if (message.say === "checkpoint_created") {
|
||||
@@ -1447,7 +1492,7 @@ export class Task {
|
||||
*/
|
||||
private async migrateDisableBrowserToolSetting(): Promise<void> {
|
||||
const config = vscode.workspace.getConfiguration("cline")
|
||||
const disableBrowserTool = vscode.workspace.getConfiguration("cline").get<boolean>("disableBrowserTool")
|
||||
const disableBrowserTool = config.get<boolean>("disableBrowserTool")
|
||||
|
||||
if (disableBrowserTool !== undefined) {
|
||||
this.browserSettings.disableToolUse = disableBrowserTool
|
||||
@@ -1456,6 +1501,16 @@ export class Task {
|
||||
}
|
||||
}
|
||||
|
||||
private async migratePreferredLanguageToolSetting(): Promise<void> {
|
||||
const config = vscode.workspace.getConfiguration("cline")
|
||||
const preferredLanguage = config.get<LanguageDisplay>("preferredLanguage")
|
||||
if (preferredLanguage !== undefined) {
|
||||
this.chatSettings.preferredLanguage = preferredLanguage
|
||||
// Remove from VSCode configuration
|
||||
await config.update("preferredLanguage", undefined, true)
|
||||
}
|
||||
}
|
||||
|
||||
async *attemptApiRequest(previousApiReqIndex: number): ApiStream {
|
||||
// Wait for MCP servers to be connected before generating system prompt
|
||||
await pWaitFor(() => this.mcpHub.isConnecting !== true, { timeout: 10_000 }).catch(() => {
|
||||
@@ -1472,9 +1527,8 @@ export class Task {
|
||||
let systemPrompt = await SYSTEM_PROMPT(cwd, supportsBrowserUse, this.mcpHub, this.browserSettings)
|
||||
|
||||
let settingsCustomInstructions = this.customInstructions?.trim()
|
||||
const preferredLanguage = getLanguageKey(
|
||||
vscode.workspace.getConfiguration("cline").get<LanguageDisplay>("preferredLanguage"),
|
||||
)
|
||||
await this.migratePreferredLanguageToolSetting()
|
||||
const preferredLanguage = getLanguageKey(this.chatSettings.preferredLanguage as LanguageDisplay)
|
||||
const preferredLanguageInstructions =
|
||||
preferredLanguage && preferredLanguage !== DEFAULT_LANGUAGE_SETTINGS
|
||||
? `# Preferred Language\n\nSpeak in ${preferredLanguage}.`
|
||||
@@ -3669,17 +3723,11 @@ export class Task {
|
||||
}),
|
||||
)
|
||||
|
||||
if (isFirstRequest) {
|
||||
await this.say("checkpoint_created") // no hash since we need to wait for CheckpointTracker to be initialized
|
||||
}
|
||||
|
||||
// use this opportunity to initialize the checkpoint tracker (can be expensive to initialize in the constructor)
|
||||
// FIXME: right now we're letting users init checkpoints for old tasks, but this could be a problem if opening a task in the wrong workspace
|
||||
// isNewTask &&
|
||||
if (!this.checkpointTracker && !this.checkpointTrackerErrorMessage) {
|
||||
// Initialize checkpoint tracker first if enabled and it's the first request
|
||||
if (isFirstRequest && this.enableCheckpoints && !this.checkpointTracker && !this.checkpointTrackerErrorMessage) {
|
||||
try {
|
||||
this.checkpointTracker = await pTimeout(
|
||||
CheckpointTracker.create(this.taskId, this.context.globalStorageUri.fsPath),
|
||||
CheckpointTracker.create(this.taskId, this.context.globalStorageUri.fsPath, this.enableCheckpoints),
|
||||
{
|
||||
milliseconds: 15_000,
|
||||
message:
|
||||
@@ -3693,14 +3741,22 @@ export class Task {
|
||||
}
|
||||
}
|
||||
|
||||
// Now that checkpoint tracker is initialized, update the dummy checkpoint_created message with the commit hash. (This is necessary since we use the API request loading as an opportunity to initialize the checkpoint tracker, which can take some time)
|
||||
if (isFirstRequest) {
|
||||
const commitHash = await this.checkpointTracker?.commit()
|
||||
// Now, if it's the first request AND checkpoints are enabled AND tracker was successfully initialized,
|
||||
// then say "checkpoint_created" and perform the commit.
|
||||
if (isFirstRequest && this.enableCheckpoints && this.checkpointTracker) {
|
||||
await this.say("checkpoint_created") // Now this is conditional
|
||||
const commitHash = await this.checkpointTracker.commit() // Actual commit
|
||||
const lastCheckpointMessage = findLast(this.clineMessages, (m) => m.say === "checkpoint_created")
|
||||
if (lastCheckpointMessage) {
|
||||
lastCheckpointMessage.lastCheckpointHash = commitHash
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
// saveClineMessagesAndUpdateHistory will be called later after API response,
|
||||
// so no need to call it here unless this is the only modification to this message.
|
||||
// For now, assuming it's handled later.
|
||||
}
|
||||
} else if (isFirstRequest && this.enableCheckpoints && !this.checkpointTracker && this.checkpointTrackerErrorMessage) {
|
||||
// Checkpoints are enabled, but tracker failed to initialize.
|
||||
// checkpointTrackerErrorMessage is already set and will be part of the state.
|
||||
// No explicit UI message here, error message will be in ExtensionState.
|
||||
}
|
||||
|
||||
const [parsedUserContent, environmentDetails, clinerulesError] = await this.loadContext(userContent, includeFileDetails)
|
||||
@@ -4013,6 +4069,8 @@ export class Task {
|
||||
// Track if we need to check clinerulesFile
|
||||
let needsClinerulesFileCheck = false
|
||||
|
||||
const workflowToggles = await refreshWorkflowToggles(this.getContext(), cwd)
|
||||
|
||||
const processUserContent = async () => {
|
||||
// This is a temporary solution to dynamically load context mentions from tool results. It checks for the presence of tags that indicate that the tool was rejected and feedback was provided (see formatToolDeniedFeedback, attemptCompletion, executeCommand, and consecutiveMistakeCount >= 3) or "<answer>" (see askFollowupQuestion), we place all user generated content in these tags so they can effectively be used as markers for when we should parse mentions). However if we allow multiple tools responses in the future, we will need to parse mentions specifically within the user content tags.
|
||||
// (Note: this caused the @/ import alias bug where file contents were being parsed as well, since v2 converted tool results to text blocks)
|
||||
@@ -4035,7 +4093,10 @@ export class Task {
|
||||
)
|
||||
|
||||
// when parsing slash commands, we still want to allow the user to provide their desired context
|
||||
const { processedText, needsClinerulesFileCheck: needsCheck } = parseSlashCommands(parsedText)
|
||||
const { processedText, needsClinerulesFileCheck: needsCheck } = await parseSlashCommands(
|
||||
parsedText,
|
||||
workflowToggles,
|
||||
)
|
||||
|
||||
if (needsCheck) {
|
||||
needsClinerulesFileCheck = true
|
||||
@@ -4061,7 +4122,7 @@ export class Task {
|
||||
// After processing content, check clinerulesData if needed
|
||||
let clinerulesError = false
|
||||
if (needsClinerulesFileCheck) {
|
||||
clinerulesError = await ensureLocalClinerulesDirExists(cwd)
|
||||
clinerulesError = await ensureLocalClineDirExists(cwd, GlobalFileNames.clineRules)
|
||||
}
|
||||
|
||||
// Return all results
|
||||
|
||||
@@ -328,6 +328,7 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script src="http://localhost:8097"></script>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
|
||||
<meta http-equiv="Content-Security-Policy" content="${csp.join("; ")}">
|
||||
|
||||
@@ -21,15 +21,17 @@ class CheckpointTracker {
|
||||
this.cwd = cwd
|
||||
}
|
||||
|
||||
public static async create(taskId: string, provider?: ClineProvider): Promise<CheckpointTracker | undefined> {
|
||||
public static async create(
|
||||
taskId: string,
|
||||
enableCheckpointsSetting: boolean,
|
||||
provider?: ClineProvider,
|
||||
): Promise<CheckpointTracker | undefined> {
|
||||
try {
|
||||
if (!provider) {
|
||||
throw new Error("Provider is required to create a checkpoint tracker")
|
||||
}
|
||||
|
||||
// Check if checkpoints are disabled in VS Code settings
|
||||
const enableCheckpoints = vscode.workspace.getConfiguration("cline").get<boolean>("enableCheckpoints") ?? true
|
||||
if (!enableCheckpoints) {
|
||||
if (!enableCheckpointsSetting) {
|
||||
return undefined // Don't create tracker when disabled
|
||||
}
|
||||
|
||||
|
||||
@@ -90,7 +90,11 @@ class CheckpointTracker {
|
||||
* Configuration:
|
||||
* - Respects 'cline.enableCheckpoints' VS Code setting
|
||||
*/
|
||||
public static async create(taskId: string, globalStoragePath: string | undefined): Promise<CheckpointTracker | undefined> {
|
||||
public static async create(
|
||||
taskId: string,
|
||||
globalStoragePath: string | undefined,
|
||||
enableCheckpointsSetting: boolean,
|
||||
): Promise<CheckpointTracker | undefined> {
|
||||
if (!globalStoragePath) {
|
||||
throw new Error("Global storage path is required to create a checkpoint tracker")
|
||||
}
|
||||
@@ -98,9 +102,9 @@ class CheckpointTracker {
|
||||
console.info(`Creating new CheckpointTracker for task ${taskId}`)
|
||||
const startTime = performance.now()
|
||||
|
||||
// Check if checkpoints are disabled in VS Code settings
|
||||
const enableCheckpoints = vscode.workspace.getConfiguration("cline").get<boolean>("enableCheckpoints") ?? true
|
||||
if (!enableCheckpoints) {
|
||||
// Check if checkpoints are disabled by setting
|
||||
if (!enableCheckpointsSetting) {
|
||||
console.info(`Checkpoints disabled by setting for task ${taskId}`)
|
||||
return undefined // Don't create tracker when disabled
|
||||
}
|
||||
|
||||
|
||||
@@ -433,12 +433,6 @@ export class DiffViewProvider {
|
||||
|
||||
// close editor if open?
|
||||
async reset() {
|
||||
// releasing memory by clearing the diff editor
|
||||
try {
|
||||
await this.closeAllDiffViews()
|
||||
} catch (error) {
|
||||
console.error("Error closing diff views:", error)
|
||||
}
|
||||
this.editType = undefined
|
||||
this.isEditing = false
|
||||
this.originalContent = undefined
|
||||
|
||||
@@ -76,6 +76,8 @@ class PostHogClient {
|
||||
BROWSER_TOOL_END: "task.browser_tool_end",
|
||||
// Tracks when browser errors occur
|
||||
BROWSER_ERROR: "task.browser_error",
|
||||
// Tracks Gemini API specific performance metrics
|
||||
GEMINI_API_PERFORMANCE: "task.gemini_api_performance",
|
||||
// Collection of all task events
|
||||
TASK_COLLECTION: "task.collection",
|
||||
},
|
||||
@@ -730,6 +732,43 @@ class PostHogClient {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures Gemini API performance metrics.
|
||||
* @param taskId Unique identifier for the task
|
||||
* @param modelId Specific Gemini model ID
|
||||
* @param data Performance data including TTFT, durations, token counts, cache stats, and API success status
|
||||
* @param collect If true, collect event instead of sending
|
||||
*/
|
||||
public captureGeminiApiPerformance(
|
||||
taskId: string,
|
||||
modelId: string,
|
||||
data: {
|
||||
ttftSec?: number
|
||||
totalDurationSec?: number
|
||||
promptTokens: number
|
||||
outputTokens: number
|
||||
cacheReadTokens: number
|
||||
cacheHit: boolean
|
||||
cacheHitPercentage?: number
|
||||
apiSuccess: boolean
|
||||
apiError?: string
|
||||
throughputTokensPerSec?: number
|
||||
},
|
||||
collect: boolean = false,
|
||||
) {
|
||||
this.capture(
|
||||
{
|
||||
event: PostHogClient.EVENTS.TASK.GEMINI_API_PERFORMANCE,
|
||||
properties: {
|
||||
taskId,
|
||||
modelId,
|
||||
...data,
|
||||
},
|
||||
},
|
||||
collect,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when the user uses the model favorite button in the model picker
|
||||
* @param model The name of the model the user has interacted with
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
export type OpenAIReasoningEffort = "low" | "medium" | "high"
|
||||
|
||||
export interface ChatSettings {
|
||||
mode: "plan" | "act"
|
||||
preferredLanguage?: string
|
||||
openAIReasoningEffort?: OpenAIReasoningEffort
|
||||
}
|
||||
|
||||
export type PartialChatSettings = Partial<ChatSettings>
|
||||
|
||||
export const DEFAULT_CHAT_SETTINGS: ChatSettings = {
|
||||
mode: "act",
|
||||
preferredLanguage: "English",
|
||||
openAIReasoningEffort: "medium",
|
||||
}
|
||||
|
||||
@@ -115,6 +115,7 @@ export type Platform = "aix" | "darwin" | "freebsd" | "linux" | "openbsd" | "sun
|
||||
export const DEFAULT_PLATFORM = "unknown"
|
||||
|
||||
export interface ExtensionState {
|
||||
isNewUser: boolean
|
||||
apiConfiguration?: ApiConfiguration
|
||||
autoApprovalSettings: AutoApprovalSettings
|
||||
browserSettings: BrowserSettings
|
||||
@@ -126,6 +127,7 @@ export interface ExtensionState {
|
||||
customInstructions?: string
|
||||
mcpMarketplaceEnabled?: boolean
|
||||
planActSeparateModelsSetting: boolean
|
||||
enableCheckpointsSetting?: boolean
|
||||
platform: Platform
|
||||
shouldShowAnnouncement: boolean
|
||||
taskHistory: HistoryItem[]
|
||||
@@ -141,6 +143,7 @@ export interface ExtensionState {
|
||||
vscMachineId: string
|
||||
globalClineRulesToggles: ClineRulesToggles
|
||||
localClineRulesToggles: ClineRulesToggles
|
||||
workflowToggles: ClineRulesToggles
|
||||
localCursorRulesToggles: ClineRulesToggles
|
||||
localWindsurfRulesToggles: ClineRulesToggles
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ export type HistoryItem = {
|
||||
|
||||
size?: number
|
||||
shadowGitConfigWorkTree?: string
|
||||
cwdOnTaskInitialization?: string
|
||||
conversationHistoryDeletedRange?: [number, number]
|
||||
isFavorited?: boolean
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ export interface WebviewMessage {
|
||||
| "reportBug"
|
||||
| "askResponse"
|
||||
| "didShowAnnouncement"
|
||||
| "selectImages"
|
||||
| "resetState"
|
||||
| "openInBrowser"
|
||||
| "openMention"
|
||||
@@ -58,6 +57,7 @@ export interface WebviewMessage {
|
||||
| "toggleClineRule"
|
||||
| "toggleCursorRule"
|
||||
| "toggleWindsurfRule"
|
||||
| "toggleWorkflow"
|
||||
| "deleteClineRule"
|
||||
| "copyToClipboard"
|
||||
| "updateTerminalConnectionTimeout"
|
||||
@@ -90,6 +90,8 @@ export interface WebviewMessage {
|
||||
// For openInBrowser
|
||||
url?: string
|
||||
planActSeparateModelsSetting?: boolean
|
||||
enableCheckpointsSetting?: boolean
|
||||
mcpMarketplaceEnabled?: boolean
|
||||
telemetrySetting?: TelemetrySetting
|
||||
customInstructionsSetting?: string
|
||||
// For task feedback
|
||||
@@ -108,9 +110,10 @@ export interface WebviewMessage {
|
||||
grpc_request_cancel?: {
|
||||
request_id: string // ID of the request to cancel
|
||||
}
|
||||
// For cline rules
|
||||
// For cline rules and workflows
|
||||
isGlobal?: boolean
|
||||
rulePath?: string
|
||||
workflowPath?: string
|
||||
enabled?: boolean
|
||||
filename?: string
|
||||
|
||||
|
||||
+8
-1
@@ -80,7 +80,6 @@ export interface ApiHandlerOptions {
|
||||
mistralApiKey?: string
|
||||
azureApiVersion?: string
|
||||
vsCodeLmModelSelector?: LanguageModelChatSelector
|
||||
o3MiniReasoningEffort?: string
|
||||
qwenApiLine?: string
|
||||
asksageApiUrl?: string
|
||||
asksageApiKey?: string
|
||||
@@ -433,6 +432,14 @@ export const vertexModels = {
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
"gemini-2.5-pro-exp-03-25": {
|
||||
maxTokens: 65536,
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
"gemini-2.5-pro-preview-05-06": {
|
||||
maxTokens: 65536,
|
||||
contextWindow: 1_048_576,
|
||||
|
||||
@@ -2,22 +2,24 @@ import { RuleFileRequest } from "../../proto/file"
|
||||
|
||||
// Helper for creating delete requests
|
||||
export const DeleteRuleFileRequest = {
|
||||
create: (params: { rulePath: string; isGlobal: boolean; metadata?: any }): RuleFileRequest => {
|
||||
create: (params: { rulePath: string; isGlobal: boolean; metadata?: any; type?: string }): RuleFileRequest => {
|
||||
return RuleFileRequest.create({
|
||||
rulePath: params.rulePath,
|
||||
isGlobal: params.isGlobal,
|
||||
metadata: params.metadata,
|
||||
type: params.type,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
// Helper for creating create requests
|
||||
export const CreateRuleFileRequest = {
|
||||
create: (params: { filename: string; isGlobal: boolean; metadata?: any }): RuleFileRequest => {
|
||||
create: (params: { filename: string; isGlobal: boolean; metadata?: any; type?: string }): RuleFileRequest => {
|
||||
return RuleFileRequest.create({
|
||||
filename: params.filename,
|
||||
isGlobal: params.isGlobal,
|
||||
metadata: params.metadata,
|
||||
type: params.type,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
/* eslint-disable */
|
||||
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"
|
||||
import { Empty, Metadata, StringRequest } from "./common"
|
||||
import { Empty, EmptyRequest, Metadata, StringArray, StringRequest } from "./common"
|
||||
|
||||
export const protobufPackage = "cline"
|
||||
|
||||
@@ -73,6 +73,8 @@ export interface RuleFileRequest {
|
||||
rulePath?: string | undefined
|
||||
/** Filename field for createRuleFile (optional) */
|
||||
filename?: string | undefined
|
||||
/** Type of the file to create (optional) */
|
||||
type?: string | undefined
|
||||
}
|
||||
|
||||
/** Result for rule file operations with meaningful data only */
|
||||
@@ -682,7 +684,7 @@ export const GitCommit: MessageFns<GitCommit> = {
|
||||
}
|
||||
|
||||
function createBaseRuleFileRequest(): RuleFileRequest {
|
||||
return { metadata: undefined, isGlobal: false, rulePath: undefined, filename: undefined }
|
||||
return { metadata: undefined, isGlobal: false, rulePath: undefined, filename: undefined, type: undefined }
|
||||
}
|
||||
|
||||
export const RuleFileRequest: MessageFns<RuleFileRequest> = {
|
||||
@@ -699,6 +701,9 @@ export const RuleFileRequest: MessageFns<RuleFileRequest> = {
|
||||
if (message.filename !== undefined) {
|
||||
writer.uint32(34).string(message.filename)
|
||||
}
|
||||
if (message.type !== undefined) {
|
||||
writer.uint32(42).string(message.type)
|
||||
}
|
||||
return writer
|
||||
},
|
||||
|
||||
@@ -741,6 +746,14 @@ export const RuleFileRequest: MessageFns<RuleFileRequest> = {
|
||||
message.filename = reader.string()
|
||||
continue
|
||||
}
|
||||
case 5: {
|
||||
if (tag !== 42) {
|
||||
break
|
||||
}
|
||||
|
||||
message.type = reader.string()
|
||||
continue
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break
|
||||
@@ -756,6 +769,7 @@ export const RuleFileRequest: MessageFns<RuleFileRequest> = {
|
||||
isGlobal: isSet(object.isGlobal) ? globalThis.Boolean(object.isGlobal) : false,
|
||||
rulePath: isSet(object.rulePath) ? globalThis.String(object.rulePath) : undefined,
|
||||
filename: isSet(object.filename) ? globalThis.String(object.filename) : undefined,
|
||||
type: isSet(object.type) ? globalThis.String(object.type) : undefined,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -773,6 +787,9 @@ export const RuleFileRequest: MessageFns<RuleFileRequest> = {
|
||||
if (message.filename !== undefined) {
|
||||
obj.filename = message.filename
|
||||
}
|
||||
if (message.type !== undefined) {
|
||||
obj.type = message.type
|
||||
}
|
||||
return obj
|
||||
},
|
||||
|
||||
@@ -786,6 +803,7 @@ export const RuleFileRequest: MessageFns<RuleFileRequest> = {
|
||||
message.isGlobal = object.isGlobal ?? false
|
||||
message.rulePath = object.rulePath ?? undefined
|
||||
message.filename = object.filename ?? undefined
|
||||
message.type = object.type ?? undefined
|
||||
return message
|
||||
},
|
||||
}
|
||||
@@ -933,6 +951,15 @@ export const FileServiceDefinition = {
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
/** Select images from the file system and return as data URLs */
|
||||
selectImages: {
|
||||
name: "selectImages",
|
||||
requestType: EmptyRequest,
|
||||
requestStream: false,
|
||||
responseType: StringArray,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
/** Convert URIs to workspace-relative paths */
|
||||
getRelativePaths: {
|
||||
name: "getRelativePaths",
|
||||
|
||||
@@ -50,6 +50,7 @@ export interface GetTaskHistoryRequest {
|
||||
favoritesOnly: boolean
|
||||
searchQuery: string
|
||||
sortBy: string
|
||||
currentWorkspaceOnly: boolean
|
||||
}
|
||||
|
||||
/** Response for task history */
|
||||
@@ -550,7 +551,7 @@ export const DeleteNonFavoritedTasksResults: MessageFns<DeleteNonFavoritedTasksR
|
||||
}
|
||||
|
||||
function createBaseGetTaskHistoryRequest(): GetTaskHistoryRequest {
|
||||
return { metadata: undefined, favoritesOnly: false, searchQuery: "", sortBy: "" }
|
||||
return { metadata: undefined, favoritesOnly: false, searchQuery: "", sortBy: "", currentWorkspaceOnly: false }
|
||||
}
|
||||
|
||||
export const GetTaskHistoryRequest: MessageFns<GetTaskHistoryRequest> = {
|
||||
@@ -567,6 +568,9 @@ export const GetTaskHistoryRequest: MessageFns<GetTaskHistoryRequest> = {
|
||||
if (message.sortBy !== "") {
|
||||
writer.uint32(34).string(message.sortBy)
|
||||
}
|
||||
if (message.currentWorkspaceOnly !== false) {
|
||||
writer.uint32(40).bool(message.currentWorkspaceOnly)
|
||||
}
|
||||
return writer
|
||||
},
|
||||
|
||||
@@ -609,6 +613,14 @@ export const GetTaskHistoryRequest: MessageFns<GetTaskHistoryRequest> = {
|
||||
message.sortBy = reader.string()
|
||||
continue
|
||||
}
|
||||
case 5: {
|
||||
if (tag !== 40) {
|
||||
break
|
||||
}
|
||||
|
||||
message.currentWorkspaceOnly = reader.bool()
|
||||
continue
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break
|
||||
@@ -624,6 +636,7 @@ export const GetTaskHistoryRequest: MessageFns<GetTaskHistoryRequest> = {
|
||||
favoritesOnly: isSet(object.favoritesOnly) ? globalThis.Boolean(object.favoritesOnly) : false,
|
||||
searchQuery: isSet(object.searchQuery) ? globalThis.String(object.searchQuery) : "",
|
||||
sortBy: isSet(object.sortBy) ? globalThis.String(object.sortBy) : "",
|
||||
currentWorkspaceOnly: isSet(object.currentWorkspaceOnly) ? globalThis.Boolean(object.currentWorkspaceOnly) : false,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -641,6 +654,9 @@ export const GetTaskHistoryRequest: MessageFns<GetTaskHistoryRequest> = {
|
||||
if (message.sortBy !== "") {
|
||||
obj.sortBy = message.sortBy
|
||||
}
|
||||
if (message.currentWorkspaceOnly !== false) {
|
||||
obj.currentWorkspaceOnly = message.currentWorkspaceOnly
|
||||
}
|
||||
return obj
|
||||
},
|
||||
|
||||
@@ -654,6 +670,7 @@ export const GetTaskHistoryRequest: MessageFns<GetTaskHistoryRequest> = {
|
||||
message.favoritesOnly = object.favoritesOnly ?? false
|
||||
message.searchQuery = object.searchQuery ?? ""
|
||||
message.sortBy = object.sortBy ?? ""
|
||||
message.currentWorkspaceOnly = object.currentWorkspaceOnly ?? false
|
||||
return message
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import * as grpc from "@grpc/grpc-js"
|
||||
import { Controller } from "../core/controller"
|
||||
|
||||
/**
|
||||
* Type definition for a gRPC handler function.
|
||||
* This represents a function that takes a Controller instance and a request object,
|
||||
* and returns a Promise of the response type.
|
||||
*
|
||||
* @template TRequest - The type of the request object
|
||||
* @template TResponse - The type of the response object
|
||||
*/
|
||||
export type GrpcHandler<TRequest, TResponse> = (controller: Controller, req: TRequest) => Promise<TResponse>
|
||||
|
||||
export type GrpcStreamingResponseHandler<TRequest, TResponse> = (
|
||||
controller: Controller,
|
||||
req: TRequest,
|
||||
streamResponseHandler: StreamingResponseWriter<TResponse>,
|
||||
requestId?: string,
|
||||
) => Promise<TResponse>
|
||||
|
||||
/**
|
||||
* Type definition for the wrapper function that converts a Promise-based handler
|
||||
* to a gRPC callback-style handler.
|
||||
*
|
||||
* @template TRequest - The type of the request object
|
||||
* @template TResponse - The type of the response object
|
||||
*/
|
||||
export type GrpcHandlerWrapper = <TRequest, TResponse>(
|
||||
handler: GrpcHandler<TRequest, TResponse>,
|
||||
controller: Controller,
|
||||
) => grpc.handleUnaryCall<TRequest, TResponse>
|
||||
|
||||
export type GrpcStreamingResponseHandlerWrapper = <TRequest, TResponse>(
|
||||
handler: GrpcStreamingResponseHandler<TRequest, TResponse>,
|
||||
controller: Controller,
|
||||
) => grpc.handleServerStreamingCall<TRequest, TResponse>
|
||||
|
||||
export type StreamingResponseWriter<TResponse> = (response: TResponse, isLast?: boolean, sequenceNumber?: number) => Promise<void>
|
||||
@@ -0,0 +1,155 @@
|
||||
// GENERATED CODE -- DO NOT EDIT!
|
||||
// Generated by generate-server-setup.mjs
|
||||
import * as grpc from "@grpc/grpc-js"
|
||||
import { Controller } from "../core/controller"
|
||||
import { GrpcHandlerWrapper, GrpcStreamingResponseHandlerWrapper } from "./grpc-types"
|
||||
|
||||
// Account Service
|
||||
import { accountLoginClicked } from "../core/controller/account/accountLoginClicked"
|
||||
|
||||
// Browser Service
|
||||
import { getBrowserConnectionInfo } from "../core/controller/browser/getBrowserConnectionInfo"
|
||||
import { testBrowserConnection } from "../core/controller/browser/testBrowserConnection"
|
||||
import { discoverBrowser } from "../core/controller/browser/discoverBrowser"
|
||||
import { getDetectedChromePath } from "../core/controller/browser/getDetectedChromePath"
|
||||
import { updateBrowserSettings } from "../core/controller/browser/updateBrowserSettings"
|
||||
|
||||
// Checkpoints Service
|
||||
import { checkpointDiff } from "../core/controller/checkpoints/checkpointDiff"
|
||||
import { checkpointRestore } from "../core/controller/checkpoints/checkpointRestore"
|
||||
|
||||
// File Service
|
||||
import { openFile } from "../core/controller/file/openFile"
|
||||
import { openImage } from "../core/controller/file/openImage"
|
||||
import { deleteRuleFile } from "../core/controller/file/deleteRuleFile"
|
||||
import { createRuleFile } from "../core/controller/file/createRuleFile"
|
||||
import { searchCommits } from "../core/controller/file/searchCommits"
|
||||
import { selectImages } from "../core/controller/file/selectImages"
|
||||
import { getRelativePaths } from "../core/controller/file/getRelativePaths"
|
||||
import { searchFiles } from "../core/controller/file/searchFiles"
|
||||
|
||||
// Mcp Service
|
||||
import { toggleMcpServer } from "../core/controller/mcp/toggleMcpServer"
|
||||
import { updateMcpTimeout } from "../core/controller/mcp/updateMcpTimeout"
|
||||
import { addRemoteMcpServer } from "../core/controller/mcp/addRemoteMcpServer"
|
||||
import { downloadMcp } from "../core/controller/mcp/downloadMcp"
|
||||
|
||||
// Models Service
|
||||
import { getOllamaModels } from "../core/controller/models/getOllamaModels"
|
||||
import { getLmStudioModels } from "../core/controller/models/getLmStudioModels"
|
||||
import { getVsCodeLmModels } from "../core/controller/models/getVsCodeLmModels"
|
||||
import { refreshOpenRouterModels } from "../core/controller/models/refreshOpenRouterModels"
|
||||
import { refreshOpenAiModels } from "../core/controller/models/refreshOpenAiModels"
|
||||
import { refreshRequestyModels } from "../core/controller/models/refreshRequestyModels"
|
||||
|
||||
// Slash Service
|
||||
import { reportBug } from "../core/controller/slash/reportBug"
|
||||
import { condense } from "../core/controller/slash/condense"
|
||||
|
||||
// State Service
|
||||
import { getLatestState } from "../core/controller/state/getLatestState"
|
||||
import { subscribeToState } from "../core/controller/state/subscribeToState"
|
||||
import { toggleFavoriteModel } from "../core/controller/state/toggleFavoriteModel"
|
||||
|
||||
// Task Service
|
||||
import { cancelTask } from "../core/controller/task/cancelTask"
|
||||
import { clearTask } from "../core/controller/task/clearTask"
|
||||
import { deleteTasksWithIds } from "../core/controller/task/deleteTasksWithIds"
|
||||
import { newTask } from "../core/controller/task/newTask"
|
||||
import { showTaskWithId } from "../core/controller/task/showTaskWithId"
|
||||
import { exportTaskWithId } from "../core/controller/task/exportTaskWithId"
|
||||
import { toggleTaskFavorite } from "../core/controller/task/toggleTaskFavorite"
|
||||
import { deleteNonFavoritedTasks } from "../core/controller/task/deleteNonFavoritedTasks"
|
||||
import { getTaskHistory } from "../core/controller/task/getTaskHistory"
|
||||
|
||||
// Web Service
|
||||
import { checkIsImageUrl } from "../core/controller/web/checkIsImageUrl"
|
||||
|
||||
export function addServices(
|
||||
server: grpc.Server,
|
||||
proto: any,
|
||||
controller: Controller,
|
||||
wrapper: GrpcHandlerWrapper,
|
||||
wrapStreamingResponse: GrpcStreamingResponseHandlerWrapper,
|
||||
): void {
|
||||
// Account Service
|
||||
server.addService(proto.cline.AccountService.service, {
|
||||
accountLoginClicked: wrapper(accountLoginClicked, controller),
|
||||
})
|
||||
|
||||
// Browser Service
|
||||
server.addService(proto.cline.BrowserService.service, {
|
||||
getBrowserConnectionInfo: wrapper(getBrowserConnectionInfo, controller),
|
||||
testBrowserConnection: wrapper(testBrowserConnection, controller),
|
||||
discoverBrowser: wrapper(discoverBrowser, controller),
|
||||
getDetectedChromePath: wrapper(getDetectedChromePath, controller),
|
||||
updateBrowserSettings: wrapper(updateBrowserSettings, controller),
|
||||
})
|
||||
|
||||
// Checkpoints Service
|
||||
server.addService(proto.cline.CheckpointsService.service, {
|
||||
checkpointDiff: wrapper(checkpointDiff, controller),
|
||||
checkpointRestore: wrapper(checkpointRestore, controller),
|
||||
})
|
||||
|
||||
// File Service
|
||||
server.addService(proto.cline.FileService.service, {
|
||||
openFile: wrapper(openFile, controller),
|
||||
openImage: wrapper(openImage, controller),
|
||||
deleteRuleFile: wrapper(deleteRuleFile, controller),
|
||||
createRuleFile: wrapper(createRuleFile, controller),
|
||||
searchCommits: wrapper(searchCommits, controller),
|
||||
selectImages: wrapper(selectImages, controller),
|
||||
getRelativePaths: wrapper(getRelativePaths, controller),
|
||||
searchFiles: wrapper(searchFiles, controller),
|
||||
})
|
||||
|
||||
// Mcp Service
|
||||
server.addService(proto.cline.McpService.service, {
|
||||
toggleMcpServer: wrapper(toggleMcpServer, controller),
|
||||
updateMcpTimeout: wrapper(updateMcpTimeout, controller),
|
||||
addRemoteMcpServer: wrapper(addRemoteMcpServer, controller),
|
||||
downloadMcp: wrapper(downloadMcp, controller),
|
||||
})
|
||||
|
||||
// Models Service
|
||||
server.addService(proto.cline.ModelsService.service, {
|
||||
getOllamaModels: wrapper(getOllamaModels, controller),
|
||||
getLmStudioModels: wrapper(getLmStudioModels, controller),
|
||||
getVsCodeLmModels: wrapper(getVsCodeLmModels, controller),
|
||||
refreshOpenRouterModels: wrapper(refreshOpenRouterModels, controller),
|
||||
refreshOpenAiModels: wrapper(refreshOpenAiModels, controller),
|
||||
refreshRequestyModels: wrapper(refreshRequestyModels, controller),
|
||||
})
|
||||
|
||||
// Slash Service
|
||||
server.addService(proto.cline.SlashService.service, {
|
||||
reportBug: wrapper(reportBug, controller),
|
||||
condense: wrapper(condense, controller),
|
||||
})
|
||||
|
||||
// State Service
|
||||
server.addService(proto.cline.StateService.service, {
|
||||
getLatestState: wrapper(getLatestState, controller),
|
||||
subscribeToState: wrapStreamingResponse(subscribeToState, controller),
|
||||
toggleFavoriteModel: wrapper(toggleFavoriteModel, controller),
|
||||
})
|
||||
|
||||
// Task Service
|
||||
server.addService(proto.cline.TaskService.service, {
|
||||
cancelTask: wrapper(cancelTask, controller),
|
||||
clearTask: wrapper(clearTask, controller),
|
||||
deleteTasksWithIds: wrapper(deleteTasksWithIds, controller),
|
||||
newTask: wrapper(newTask, controller),
|
||||
showTaskWithId: wrapper(showTaskWithId, controller),
|
||||
exportTaskWithId: wrapper(exportTaskWithId, controller),
|
||||
toggleTaskFavorite: wrapper(toggleTaskFavorite, controller),
|
||||
deleteNonFavoritedTasks: wrapper(deleteNonFavoritedTasks, controller),
|
||||
getTaskHistory: wrapper(getTaskHistory, controller),
|
||||
})
|
||||
|
||||
// Web Service
|
||||
server.addService(proto.cline.WebService.service, {
|
||||
checkIsImageUrl: wrapper(checkIsImageUrl, controller),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import * as grpc from "@grpc/grpc-js"
|
||||
import { ReflectionService } from "@grpc/reflection"
|
||||
import * as health from "grpc-health-check"
|
||||
|
||||
import { activate } from "../extension"
|
||||
import { Controller } from "../core/controller"
|
||||
import { extensionContext, outputChannel, postMessage } from "./vscode-context"
|
||||
import { packageDefinition, proto, log, camelToSnakeCase, snakeToCamelCase } from "./utils"
|
||||
import { GrpcHandler, GrpcStreamingResponseHandler } from "./grpc-types"
|
||||
import { addServices } from "./server-setup"
|
||||
import { StreamingResponseHandler } from "@/core/controller/grpc-handler"
|
||||
|
||||
function main() {
|
||||
log("Starting service...")
|
||||
|
||||
activate(extensionContext)
|
||||
const controller = new Controller(extensionContext, outputChannel, postMessage)
|
||||
const server = new grpc.Server()
|
||||
|
||||
// Set up health check.
|
||||
const healthImpl = new health.HealthImplementation({ "": "SERVING" })
|
||||
healthImpl.addToServer(server)
|
||||
|
||||
// Add all the handlers for the ProtoBus services to the server.
|
||||
addServices(server, proto, controller, wrapHandler, wrapStreamingResponseHandler)
|
||||
|
||||
// Set up reflection.
|
||||
const reflection = new ReflectionService(packageDefinition)
|
||||
reflection.addToServer(server)
|
||||
|
||||
// Start the server.
|
||||
const host = "127.0.0.1:50051"
|
||||
server.bindAsync(host, grpc.ServerCredentials.createInsecure(), (err) => {
|
||||
if (err) {
|
||||
log(`Error: Failed to bind to ${host}, port may be unavailable ${err.message}`)
|
||||
process.exit(1)
|
||||
} else {
|
||||
server.start()
|
||||
log(`gRPC server listening on ${host}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a Promise-based handler function to make it compatible with gRPC's callback-based API.
|
||||
* This function converts an async handler that returns a Promise into a function that uses
|
||||
* the gRPC callback pattern.
|
||||
*
|
||||
* @template TRequest - The type of the request object
|
||||
* @template TResponse - The type of the response object
|
||||
* @param handler - The Promise-based handler function to wrap
|
||||
* @param controllerInstance - The controller instance to pass to the handler
|
||||
* @returns A gRPC-compatible callback-style handler function
|
||||
*/
|
||||
function wrapHandler<TRequest, TResponse>(
|
||||
handler: GrpcHandler<TRequest, TResponse>,
|
||||
controller: Controller,
|
||||
): grpc.handleUnaryCall<TRequest, TResponse> {
|
||||
return async (call: grpc.ServerUnaryCall<TRequest, TResponse>, callback: grpc.sendUnaryData<TResponse>) => {
|
||||
try {
|
||||
log(`gRPC request: ${call.getPath()}`)
|
||||
const result = await handler(controller, snakeToCamelCase(call.request))
|
||||
// The grpc-js serializer expects the proto message to be in the same
|
||||
// case as the proto file. This is a work around until we find a solution.
|
||||
callback(null, camelToSnakeCase(result))
|
||||
} catch (err: any) {
|
||||
log(`gRPC handler error: ${call.getPath()}\n${err.stack}`)
|
||||
callback({
|
||||
code: grpc.status.INTERNAL,
|
||||
message: err.message || "Internal error",
|
||||
} as grpc.ServiceError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function wrapStreamingResponseHandler<TRequest, TResponse>(
|
||||
handler: GrpcStreamingResponseHandler<TRequest, TResponse>,
|
||||
controller: Controller,
|
||||
): grpc.handleServerStreamingCall<TRequest, TResponse> {
|
||||
return async (call: grpc.ServerWritableStream<TRequest, TResponse>) => {
|
||||
try {
|
||||
const requestId = call.metadata.get("request-id").pop()?.toString()
|
||||
log(`gRPC streaming request: ${call.getPath()}`)
|
||||
|
||||
const responseHandler: StreamingResponseHandler = (response, isLast, sequenceNumber) => {
|
||||
try {
|
||||
// The grpc-js serializer expects the proto message to be in the same
|
||||
// case as the proto file. This is a work around until we find a solution.
|
||||
call.write(camelToSnakeCase(response)) // Use a bound version of call.write to maintain proper 'this' context
|
||||
|
||||
if (isLast === true) {
|
||||
log(`Closing stream for ${requestId}`)
|
||||
call.end()
|
||||
}
|
||||
return Promise.resolve()
|
||||
} catch (error) {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
}
|
||||
await handler(controller, snakeToCamelCase(call.request), responseHandler, requestId)
|
||||
} catch (err: any) {
|
||||
log(`gRPC handler error: ${call.getPath()}\n${err.stack}`)
|
||||
call.destroy({
|
||||
code: grpc.status.INTERNAL,
|
||||
message: err.message || "Internal error",
|
||||
} as grpc.ServiceError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,65 @@
|
||||
import * as fs from "fs"
|
||||
import * as grpc from "@grpc/grpc-js"
|
||||
import * as protoLoader from "@grpc/proto-loader"
|
||||
import * as health from "grpc-health-check"
|
||||
|
||||
const log = (...args: unknown[]) => {
|
||||
const timestamp = new Date().toISOString()
|
||||
console.log(`[${timestamp}]`, "#bot.cline.server.ts", ...args)
|
||||
}
|
||||
|
||||
// Load service definitions.
|
||||
const descriptorSet = fs.readFileSync("proto/descriptor_set.pb")
|
||||
const clineDef = protoLoader.loadFileDescriptorSetFromBuffer(descriptorSet)
|
||||
const healthDef = protoLoader.loadSync(health.protoPath)
|
||||
const packageDefinition = { ...clineDef, ...healthDef }
|
||||
const proto = grpc.loadPackageDefinition(packageDefinition) as unknown
|
||||
|
||||
// Helper function to convert camelCase to snake_case
|
||||
function camelToSnakeCase(obj: any): any {
|
||||
if (obj === null || typeof obj !== "object") {
|
||||
return obj
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(camelToSnakeCase)
|
||||
}
|
||||
|
||||
return Object.keys(obj).reduce((acc: any, key: string) => {
|
||||
// Convert key from camelCase to snake_case
|
||||
const snakeKey = key
|
||||
.replace(/([A-Z])/g, "_$1")
|
||||
.replace(/^_+/, "")
|
||||
.toLowerCase()
|
||||
|
||||
// Convert value recursively if it's an object
|
||||
const value = obj[key]
|
||||
acc[snakeKey] = camelToSnakeCase(value)
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
// Helper function to convert snake_case to camelCase
|
||||
function snakeToCamelCase(obj: any): any {
|
||||
if (obj === null || typeof obj !== "object") {
|
||||
return obj
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(snakeToCamelCase)
|
||||
}
|
||||
|
||||
return Object.keys(obj).reduce((acc: any, key: string) => {
|
||||
// Convert key from snake_case to camelCase
|
||||
const camelKey = key.replace(/_([a-z0-9])/g, (_, char) => char.toUpperCase())
|
||||
|
||||
// Convert value recursively if it's an object
|
||||
const value = obj[key]
|
||||
acc[camelKey] = snakeToCamelCase(value)
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
export { packageDefinition, proto, log, camelToSnakeCase, snakeToCamelCase }
|
||||
@@ -0,0 +1,164 @@
|
||||
// @ts-nocheck
|
||||
import * as vscode from "vscode"
|
||||
import open from "open"
|
||||
import { log } from "./utils"
|
||||
|
||||
function stubUri(path: string): vscode.Uri {
|
||||
console.log(`Using file path: ${path}`)
|
||||
return {
|
||||
fsPath: path,
|
||||
scheme: "",
|
||||
authority: "",
|
||||
path: "",
|
||||
query: "",
|
||||
fragment: "",
|
||||
with: function (change: {
|
||||
scheme?: string
|
||||
authority?: string
|
||||
path?: string
|
||||
query?: string
|
||||
fragment?: string
|
||||
}): vscode.Uri {
|
||||
return stubUri(path)
|
||||
},
|
||||
toString: function (skipEncoding?: boolean): string {
|
||||
return path
|
||||
},
|
||||
toJSON: function () {
|
||||
return {}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function createMemento(): vscode.Memento {
|
||||
const store = {}
|
||||
return {
|
||||
keys: function (): readonly string[] {
|
||||
return Object.keys(store)
|
||||
},
|
||||
get: function <T>(key: string): T | undefined {
|
||||
return key in store ? store[key] : undefined
|
||||
},
|
||||
update: function (key: string, value: any): Thenable<void> {
|
||||
store[key] = value
|
||||
return Promise.resolve()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const extensionContext: vscode.ExtensionContext = {
|
||||
extensionPath: "/tmp/vscode/extension",
|
||||
extensionUri: stubUri("/tmp/vscode/extension"),
|
||||
|
||||
globalStoragePath: "/tmp/vscode/global",
|
||||
globalStorageUri: stubUri("/tmp/vscode/global"),
|
||||
|
||||
storagePath: "/tmp/vscode/storage",
|
||||
storageUri: stubUri("/tmp/vscode/storage"),
|
||||
|
||||
logPath: "/tmp/vscode/log",
|
||||
logUri: stubUri("/tmp/vscode/log"),
|
||||
|
||||
globalState: createMemento(),
|
||||
workspaceState: createMemento(),
|
||||
storageState: createMemento(),
|
||||
|
||||
environmentVariableCollection: {
|
||||
getScoped: function (scope: vscode.EnvironmentVariableScope): vscode.EnvironmentVariableCollection {
|
||||
return {
|
||||
persistent: false,
|
||||
description: undefined,
|
||||
replace: function (variable: string, value: string, options?: vscode.EnvironmentVariableMutatorOptions): void {},
|
||||
append: function (variable: string, value: string, options?: vscode.EnvironmentVariableMutatorOptions): void {},
|
||||
prepend: function (variable: string, value: string, options?: vscode.EnvironmentVariableMutatorOptions): void {},
|
||||
get: function (variable: string): vscode.EnvironmentVariableMutator | undefined {
|
||||
return undefined
|
||||
},
|
||||
forEach: function (
|
||||
callback: (
|
||||
variable: string,
|
||||
mutator: vscode.EnvironmentVariableMutator,
|
||||
collection: vscode.EnvironmentVariableCollection,
|
||||
) => any,
|
||||
thisArg?: any,
|
||||
): void {},
|
||||
delete: function (variable: string): void {},
|
||||
clear: function (): void {},
|
||||
[Symbol.iterator]: function (): Iterator<
|
||||
[variable: string, mutator: vscode.EnvironmentVariableMutator],
|
||||
any,
|
||||
any
|
||||
> {
|
||||
throw new Error("environmentVariableCollection.getScoped.Iterator not implemented")
|
||||
},
|
||||
}
|
||||
},
|
||||
persistent: false,
|
||||
description: undefined,
|
||||
replace: function (variable: string, value: string, options?: vscode.EnvironmentVariableMutatorOptions): void {},
|
||||
append: function (variable: string, value: string, options?: vscode.EnvironmentVariableMutatorOptions): void {},
|
||||
prepend: function (variable: string, value: string, options?: vscode.EnvironmentVariableMutatorOptions): void {},
|
||||
get: function (variable: string): vscode.EnvironmentVariableMutator | undefined {
|
||||
return undefined
|
||||
},
|
||||
forEach: function (
|
||||
callback: (
|
||||
variable: string,
|
||||
mutator: vscode.EnvironmentVariableMutator,
|
||||
collection: vscode.EnvironmentVariableCollection,
|
||||
) => any,
|
||||
thisArg?: any,
|
||||
): void {
|
||||
throw new Error("environmentVariableCollection.forEach not implemented")
|
||||
},
|
||||
delete: function (variable: string): void {},
|
||||
clear: function (): void {},
|
||||
[Symbol.iterator]: function (): Iterator<[variable: string, mutator: vscode.EnvironmentVariableMutator], any, any> {
|
||||
throw new Error("environmentVariableCollection.Iterator not implemented")
|
||||
},
|
||||
},
|
||||
|
||||
extensionMode: 1, // Development
|
||||
|
||||
extension: {
|
||||
id: "your.extension.id",
|
||||
isActive: true,
|
||||
extensionPath: "/tmp/vscode/extension",
|
||||
extensionUri: stubUri("/tmp/vscode/extension"),
|
||||
packageJSON: {},
|
||||
exports: {},
|
||||
activate: async () => {},
|
||||
extensionKind: vscode.ExtensionKind.UI,
|
||||
},
|
||||
|
||||
subscriptions: [],
|
||||
|
||||
asAbsolutePath: (relPath) => `/tmp/vscode/extension/${relPath}`,
|
||||
|
||||
secrets: {
|
||||
store: async () => {},
|
||||
get: async () => undefined,
|
||||
delete: async () => {},
|
||||
onDidChange: {},
|
||||
},
|
||||
}
|
||||
|
||||
const outputChannel: vscode.OutputChannel = {
|
||||
append: (text) => process.stdout.write(text),
|
||||
appendLine: (line) => console.log(line),
|
||||
clear: () => {},
|
||||
show: () => {},
|
||||
hide: () => {},
|
||||
dispose: () => {},
|
||||
name: "",
|
||||
replace: function (value: string): void {},
|
||||
}
|
||||
|
||||
function postMessage(message: ExtensionMessage): Promise<boolean> {
|
||||
log("postMessage called:", message)
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
|
||||
console.log("Finished loading vscode context...")
|
||||
|
||||
export { extensionContext, outputChannel, postMessage }
|
||||
+169
-1
@@ -3,7 +3,7 @@ import { after, describe, it } from "mocha"
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
import "should"
|
||||
import { createDirectoriesForFile, fileExistsAtPath, isDirectory } from "./fs"
|
||||
import { createDirectoriesForFile, fileExistsAtPath, isDirectory, readDirectory } from "./fs"
|
||||
|
||||
describe("Filesystem Utilities", () => {
|
||||
const tmpDir = path.join(os.tmpdir(), "cline-test-" + Math.random().toString(36).slice(2))
|
||||
@@ -88,4 +88,172 @@ describe("Filesystem Utilities", () => {
|
||||
isDir.should.be.false()
|
||||
})
|
||||
})
|
||||
|
||||
describe("readDirectory", () => {
|
||||
it("should list files in a directory", async () => {
|
||||
// Create test directory with files
|
||||
const testDir = path.join(tmpDir, "read-test")
|
||||
await fs.mkdir(testDir, { recursive: true })
|
||||
await fs.writeFile(path.join(testDir, "file1.txt"), "content")
|
||||
await fs.writeFile(path.join(testDir, "file2.txt"), "content")
|
||||
|
||||
// Get files
|
||||
const files = await readDirectory(testDir)
|
||||
files.length.should.equal(2)
|
||||
files.should.containDeep([path.resolve(testDir, "file1.txt"), path.resolve(testDir, "file2.txt")])
|
||||
})
|
||||
|
||||
it("should exclude specified directories", async () => {
|
||||
// Create test directory with files and an excluded directory
|
||||
const testDir = path.join(tmpDir, "exclude-test")
|
||||
const excludeDir = path.join(testDir, "exclude-me")
|
||||
await fs.mkdir(excludeDir, { recursive: true })
|
||||
await fs.writeFile(path.join(testDir, "include.txt"), "content")
|
||||
await fs.writeFile(path.join(excludeDir, "excluded.txt"), "content")
|
||||
|
||||
// Get files, excluding the "exclude-me" directory
|
||||
const files = await readDirectory(testDir, [["exclude-me"]])
|
||||
files.length.should.equal(1)
|
||||
files.should.containDeep([path.resolve(testDir, "include.txt")])
|
||||
files.should.not.containDeep([path.resolve(excludeDir, "excluded.txt")])
|
||||
})
|
||||
})
|
||||
|
||||
it("should correctly handle complex nested directory structures", async () => {
|
||||
// Create a complex directory structure
|
||||
const complexDir = path.join(tmpDir, "complex-test")
|
||||
|
||||
// Create main dir
|
||||
await fs.mkdir(complexDir, { recursive: true })
|
||||
await fs.writeFile(path.join(complexDir, "root.txt"), "content")
|
||||
|
||||
// Create first branch
|
||||
await fs.mkdir(path.join(complexDir, "dir1"), { recursive: true })
|
||||
await fs.writeFile(path.join(complexDir, "dir1", "file1.txt"), "content")
|
||||
|
||||
// Create second branch with nested structure
|
||||
await fs.mkdir(path.join(complexDir, "dir2", "subdir1"), { recursive: true })
|
||||
await fs.writeFile(path.join(complexDir, "dir2", "file2.txt"), "content")
|
||||
await fs.writeFile(path.join(complexDir, "dir2", "subdir1", "file3.txt"), "content")
|
||||
|
||||
// Create third branch with deep nesting
|
||||
await fs.mkdir(path.join(complexDir, "dir3", "subdir2", "deepdir"), { recursive: true })
|
||||
await fs.writeFile(path.join(complexDir, "dir3", "file4.txt"), "content")
|
||||
await fs.writeFile(path.join(complexDir, "dir3", "subdir2", "file5.txt"), "content")
|
||||
await fs.writeFile(path.join(complexDir, "dir3", "subdir2", "deepdir", "file6.txt"), "content")
|
||||
|
||||
// Get all files
|
||||
const files = await readDirectory(complexDir)
|
||||
|
||||
const expectedFiles = [
|
||||
path.resolve(complexDir, "root.txt"),
|
||||
path.resolve(complexDir, "dir1", "file1.txt"),
|
||||
path.resolve(complexDir, "dir2", "file2.txt"),
|
||||
path.resolve(complexDir, "dir2", "subdir1", "file3.txt"),
|
||||
path.resolve(complexDir, "dir3", "file4.txt"),
|
||||
path.resolve(complexDir, "dir3", "subdir2", "file5.txt"),
|
||||
path.resolve(complexDir, "dir3", "subdir2", "deepdir", "file6.txt"),
|
||||
]
|
||||
|
||||
files.length.should.equal(expectedFiles.length)
|
||||
|
||||
files.sort().should.deepEqual(expectedFiles.sort())
|
||||
})
|
||||
|
||||
it("should correctly exclude multiple directories in complex structures", async () => {
|
||||
// Use the same complex directory structure
|
||||
const complexDir = path.join(tmpDir, "complex-exclude-test")
|
||||
|
||||
// Create main dir
|
||||
await fs.mkdir(complexDir, { recursive: true })
|
||||
await fs.writeFile(path.join(complexDir, "root.txt"), "content")
|
||||
|
||||
// Create first branch
|
||||
await fs.mkdir(path.join(complexDir, "dir1"), { recursive: true })
|
||||
await fs.writeFile(path.join(complexDir, "dir1", "file1.txt"), "content")
|
||||
|
||||
// Create second branch with nested structure
|
||||
await fs.mkdir(path.join(complexDir, "dir2", "subdir1"), { recursive: true })
|
||||
await fs.writeFile(path.join(complexDir, "dir2", "file2.txt"), "content")
|
||||
await fs.writeFile(path.join(complexDir, "dir2", "subdir1", "file3.txt"), "content")
|
||||
|
||||
// Create third branch with deep nesting
|
||||
await fs.mkdir(path.join(complexDir, "dir3", "subdir2", "deepdir"), { recursive: true })
|
||||
await fs.writeFile(path.join(complexDir, "dir3", "file4.txt"), "content")
|
||||
await fs.writeFile(path.join(complexDir, "dir3", "subdir2", "file5.txt"), "content")
|
||||
await fs.writeFile(path.join(complexDir, "dir3", "subdir2", "deepdir", "file6.txt"), "content")
|
||||
|
||||
// Get files excluding multiple directories
|
||||
const files = await readDirectory(complexDir, [["dir1"], ["subdir2"]])
|
||||
|
||||
const expectedFiles = [
|
||||
path.resolve(complexDir, "root.txt"),
|
||||
path.resolve(complexDir, "dir2", "file2.txt"),
|
||||
path.resolve(complexDir, "dir2", "subdir1", "file3.txt"),
|
||||
path.resolve(complexDir, "dir3", "file4.txt"),
|
||||
]
|
||||
|
||||
files.length.should.equal(expectedFiles.length)
|
||||
|
||||
files.sort().should.deepEqual(expectedFiles.sort())
|
||||
})
|
||||
|
||||
it("should exclude .clinerules/workflows directory specifically", async () => {
|
||||
// Create a test directory structure
|
||||
const clinerulesDirTest = path.join(tmpDir, "clinerules-test")
|
||||
const clinerulesDirPath = path.join(clinerulesDirTest, ".clinerules")
|
||||
|
||||
// Create .clinerules directory and root files
|
||||
await fs.mkdir(clinerulesDirPath, { recursive: true })
|
||||
await fs.writeFile(path.join(clinerulesDirPath, "config.json"), "{}")
|
||||
await fs.writeFile(path.join(clinerulesDirPath, "settings.js"), "// settings")
|
||||
|
||||
// Create .clinerules/other directory and files
|
||||
const otherDirPath = path.join(clinerulesDirPath, "other")
|
||||
await fs.mkdir(otherDirPath, { recursive: true })
|
||||
await fs.writeFile(path.join(otherDirPath, "helper.js"), "// helper code")
|
||||
await fs.writeFile(path.join(otherDirPath, "util.js"), "// util functions")
|
||||
|
||||
// Create .clinerules/workflows directory and files
|
||||
const workflowsDirPath = path.join(clinerulesDirPath, "workflows")
|
||||
await fs.mkdir(workflowsDirPath, { recursive: true })
|
||||
await fs.writeFile(path.join(workflowsDirPath, "workflow1.js"), "// workflow1")
|
||||
await fs.writeFile(path.join(workflowsDirPath, "workflow2.js"), "// workflow2")
|
||||
|
||||
// Get all files WITHOUT exclusion
|
||||
const allFiles = await readDirectory(clinerulesDirPath)
|
||||
|
||||
// Verify all files are included
|
||||
allFiles.length.should.equal(6) // 2 in root + 2 in other + 2 in workflows
|
||||
allFiles.some((file) => file.includes("workflow1.js")).should.be.true()
|
||||
allFiles.some((file) => file.includes("workflow2.js")).should.be.true()
|
||||
|
||||
// Get files WITH workflows directory excluded
|
||||
const filteredFiles = await readDirectory(clinerulesDirPath, [[".clinerules", "workflows"]])
|
||||
|
||||
// Verify workflows files are excluded but others remain
|
||||
filteredFiles.length.should.equal(4) // 2 in root + 2 in other
|
||||
|
||||
const expectedFiles = [
|
||||
path.resolve(clinerulesDirPath, "config.json"),
|
||||
path.resolve(clinerulesDirPath, "settings.js"),
|
||||
path.resolve(otherDirPath, "helper.js"),
|
||||
path.resolve(otherDirPath, "util.js"),
|
||||
]
|
||||
|
||||
filteredFiles.sort().should.deepEqual(expectedFiles.sort())
|
||||
|
||||
// Test with multiple exclusions
|
||||
const multiExcludeFiles = await readDirectory(clinerulesDirPath, [
|
||||
[".clinerules", "workflows"],
|
||||
[".clinerules", "other"],
|
||||
])
|
||||
|
||||
// Verify both workflows and other directories are excluded
|
||||
multiExcludeFiles.length.should.equal(2) // only the 2 files in root
|
||||
|
||||
const rootOnlyFiles = [path.resolve(clinerulesDirPath, "config.json"), path.resolve(clinerulesDirPath, "settings.js")]
|
||||
|
||||
multiExcludeFiles.sort().should.deepEqual(rootOnlyFiles.sort())
|
||||
})
|
||||
})
|
||||
|
||||
+19
-1
@@ -86,16 +86,34 @@ const OS_GENERATED_FILES = [
|
||||
* Recursively reads a directory and returns an array of absolute file paths.
|
||||
*
|
||||
* @param directoryPath - The path to the directory to read.
|
||||
* @param excludedPaths - Nested array of paths to ignore.
|
||||
* @returns A promise that resolves to an array of absolute file paths.
|
||||
* @throws Error if the directory cannot be read.
|
||||
*/
|
||||
export const readDirectory = async (directoryPath: string) => {
|
||||
export const readDirectory = async (directoryPath: string, excludedPaths: string[][] = []) => {
|
||||
try {
|
||||
const filePaths = await fs
|
||||
.readdir(directoryPath, { withFileTypes: true, recursive: true })
|
||||
.then((entries) => entries.filter((entry) => !OS_GENERATED_FILES.includes(entry.name)))
|
||||
.then((entries) => entries.filter((entry) => entry.isFile()))
|
||||
.then((files) => files.map((file) => path.resolve(file.parentPath, file.name)))
|
||||
.then((filePaths) =>
|
||||
filePaths.filter((filePath) => {
|
||||
if (excludedPaths.length === 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
for (const excludedPathList of excludedPaths) {
|
||||
const pathToSearchFor = path.sep + excludedPathList.join(path.sep) + path.sep
|
||||
if (filePath.includes(pathToSearchFor)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}),
|
||||
)
|
||||
|
||||
return filePaths
|
||||
} catch {
|
||||
throw new Error(`Error reading directory at ${directoryPath}`)
|
||||
|
||||
+514
@@ -0,0 +1,514 @@
|
||||
{
|
||||
"name": "Cline standalone",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "Cline standalone",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "^1.13.3",
|
||||
"@grpc/reflection": "^1.0.4",
|
||||
"grpc-health-check": "^2.0.2",
|
||||
"open": "^10.1.2",
|
||||
"vscode": "file:./vscode"
|
||||
}
|
||||
},
|
||||
"node_modules/@grpc/grpc-js": {
|
||||
"version": "1.13.3",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.3.tgz",
|
||||
"integrity": "sha512-FTXHdOoPbZrBjlVLHuKbDZnsTxXv2BlHF57xw6LuThXacXvtkahEPED0CKMk6obZDf65Hv4k3z62eyPNpvinIg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@grpc/proto-loader": "^0.7.13",
|
||||
"@js-sdsl/ordered-map": "^4.4.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@grpc/proto-loader": {
|
||||
"version": "0.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz",
|
||||
"integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"long": "^5.0.0",
|
||||
"protobufjs": "^7.2.5",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
"proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@grpc/reflection": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/reflection/-/reflection-1.0.4.tgz",
|
||||
"integrity": "sha512-znA8v4AviOD3OPOxy11pxrtP8k8DanpefeTymS8iGW1fVr1U2cHuzfhYqDPHnVNDf4qvF9E25KtSihPy2DBWfQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@grpc/proto-loader": "^0.7.13",
|
||||
"protobufjs": "^7.2.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@grpc/grpc-js": "^1.8.21"
|
||||
}
|
||||
},
|
||||
"node_modules/@js-sdsl/ordered-map": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz",
|
||||
"integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/js-sdsl"
|
||||
}
|
||||
},
|
||||
"node_modules/@protobufjs/aspromise": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
||||
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/base64": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
|
||||
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/codegen": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
|
||||
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/eventemitter": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
|
||||
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/fetch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
|
||||
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.1",
|
||||
"@protobufjs/inquire": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@protobufjs/float": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
|
||||
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/inquire": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
|
||||
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/path": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
|
||||
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/pool": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
|
||||
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/utf8": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
|
||||
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.15.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.18.tgz",
|
||||
"integrity": "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/bundle-name": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
|
||||
"integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"run-applescript": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/default-browser": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz",
|
||||
"integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bundle-name": "^4.1.0",
|
||||
"default-browser-id": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/default-browser-id": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz",
|
||||
"integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/define-lazy-prop": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
|
||||
"integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/grpc-health-check": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/grpc-health-check/-/grpc-health-check-2.0.2.tgz",
|
||||
"integrity": "sha512-5XKOdg/gIlTsZZR+8QJjzmW2CHBnn+NfM8zevIpzg+96i9dAuoH+Guu/L/5vuUkSbVZG69wVPnLpEnW+Smon1A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@grpc/proto-loader": "^0.7.13"
|
||||
}
|
||||
},
|
||||
"node_modules/is-docker": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
|
||||
"integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"is-docker": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-inside-container": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
|
||||
"integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-docker": "^3.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"is-inside-container": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-wsl": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz",
|
||||
"integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-inside-container": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash.camelcase": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
||||
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/open": {
|
||||
"version": "10.1.2",
|
||||
"resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz",
|
||||
"integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"default-browser": "^5.2.1",
|
||||
"define-lazy-prop": "^3.0.0",
|
||||
"is-inside-container": "^1.0.0",
|
||||
"is-wsl": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/protobufjs": {
|
||||
"version": "7.5.2",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.2.tgz",
|
||||
"integrity": "sha512-f2ls6rpO6G153Cy+o2XQ+Y0sARLOZ17+OGVLHrc3VUKcLHYKEKWbkSujdBWQXM7gKn5NTfp0XnRPZn1MIu8n9w==",
|
||||
"hasInstallScript": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.2",
|
||||
"@protobufjs/base64": "^1.1.2",
|
||||
"@protobufjs/codegen": "^2.0.4",
|
||||
"@protobufjs/eventemitter": "^1.1.0",
|
||||
"@protobufjs/fetch": "^1.1.0",
|
||||
"@protobufjs/float": "^1.0.2",
|
||||
"@protobufjs/inquire": "^1.1.0",
|
||||
"@protobufjs/path": "^1.1.2",
|
||||
"@protobufjs/pool": "^1.1.0",
|
||||
"@protobufjs/utf8": "^1.1.0",
|
||||
"@types/node": ">=13.7.0",
|
||||
"long": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/run-applescript": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz",
|
||||
"integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vscode": {
|
||||
"resolved": "vscode",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^8.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.3",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"vscode": {
|
||||
"version": "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Cline standalone",
|
||||
"version": "1.0.0",
|
||||
"main": "standalone.js",
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "^1.13.3",
|
||||
"@grpc/reflection": "^1.0.4",
|
||||
"grpc-health-check": "^2.0.2",
|
||||
"open": "^10.1.2",
|
||||
"vscode": "file:./vscode"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
const stubs = require("./vscode-stubs.js")
|
||||
const impls = require("./vscode-impls.js")
|
||||
|
||||
module.exports = {
|
||||
...stubs,
|
||||
...impls,
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "vscode",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js"
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
function createStub(path) {
|
||||
return new Proxy(function () {}, {
|
||||
get: (target, prop) => {
|
||||
const fullPath = `${path}.${String(prop)}`
|
||||
console.log(`Accessed stub: ${fullPath}`)
|
||||
return createStub(fullPath)
|
||||
},
|
||||
apply: (target, thisArg, args) => {
|
||||
console.log(`Called stub: ${path} with args:`, args)
|
||||
return createStub(path)
|
||||
},
|
||||
construct: (target, args) => {
|
||||
console.log(`Constructed stub: ${path} with args:`, args)
|
||||
return createStub(path)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = { createStub }
|
||||
@@ -0,0 +1,149 @@
|
||||
console.log("Loading stub impls...")
|
||||
|
||||
const { createStub } = require("./stub-utils")
|
||||
const open = require("open").default
|
||||
|
||||
vscode.window = {
|
||||
showInformationMessage: (...args) => {
|
||||
console.log("Stubbed showInformationMessage:", ...args)
|
||||
return Promise.resolve(undefined)
|
||||
},
|
||||
showWarningMessage: (...args) => {
|
||||
console.log("Stubbed showWarningMessage:", ...args)
|
||||
return Promise.resolve(undefined)
|
||||
},
|
||||
showErrorMessage: (...args) => {
|
||||
console.log("Stubbed showErrorMessage:", ...args)
|
||||
return Promise.resolve(undefined)
|
||||
},
|
||||
showInputBox: async (options) => {
|
||||
console.log("Stubbed showInputBox:", options)
|
||||
return ""
|
||||
},
|
||||
showOpenDialog: async (options) => {
|
||||
console.log("Stubbed showOpenDialog:", options)
|
||||
return []
|
||||
},
|
||||
showSaveDialog: async (options) => {
|
||||
console.log("Stubbed showSaveDialog:", options)
|
||||
return undefined
|
||||
},
|
||||
showTextDocument: async (...args) => {
|
||||
console.log("Stubbed showTextDocument:", ...args)
|
||||
return {}
|
||||
},
|
||||
createOutputChannel: (name) => {
|
||||
console.log("Stubbed createOutputChannel:", name)
|
||||
return {
|
||||
appendLine: console.log,
|
||||
show: () => {},
|
||||
dispose: () => {},
|
||||
}
|
||||
},
|
||||
createTerminal: (...args) => {
|
||||
console.log("Stubbed createTerminal:", ...args)
|
||||
return {
|
||||
sendText: console.log,
|
||||
show: () => {},
|
||||
dispose: () => {},
|
||||
}
|
||||
},
|
||||
activeTextEditor: undefined,
|
||||
visibleTextEditors: [],
|
||||
tabGroups: {
|
||||
all: [],
|
||||
close: async () => {},
|
||||
},
|
||||
withProgress: async (_options, task) => {
|
||||
console.log("Stubbed withProgress")
|
||||
return task({ report: () => {} })
|
||||
},
|
||||
registerUriHandler: () => ({ dispose: () => {} }),
|
||||
registerWebviewViewProvider: () => ({ dispose: () => {} }),
|
||||
onDidChangeActiveTextEditor: () => ({ dispose: () => {} }),
|
||||
createTextEditorDecorationType: () => ({ dispose: () => {} }),
|
||||
createWebviewPanel: (...args) => {
|
||||
console.log("Stubbed createWebviewPanel:", ...args)
|
||||
return {
|
||||
webview: {},
|
||||
reveal: () => {},
|
||||
dispose: () => {},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
vscode.env = {
|
||||
uriScheme: "vscode",
|
||||
appName: "Visual Studio Code",
|
||||
appRoot: "/tmp/vscode/appRoot",
|
||||
language: "en",
|
||||
machineId: "stub-machine-id",
|
||||
remoteName: undefined,
|
||||
sessionId: "stub-session-id",
|
||||
shell: "/bin/bash",
|
||||
|
||||
clipboard: createStub("vscode.env.clipboard"),
|
||||
openExternal: createStub("vscode.env.openExternal"),
|
||||
getQueryParameter: createStub("vscode.env.getQueryParameter"),
|
||||
onDidChangeTelemetryEnabled: createStub("vscode.env.onDidChangeTelemetryEnabled"),
|
||||
isTelemetryEnabled: createStub("vscode.env.isTelemetryEnabled"),
|
||||
telemetryConfiguration: createStub("vscode.env.telemetryConfiguration"),
|
||||
onDidChangeTelemetryConfiguration: createStub("vscode.env.onDidChangeTelemetryConfiguration"),
|
||||
createTelemetryLogger: createStub("vscode.env.createTelemetryLogger"),
|
||||
}
|
||||
|
||||
vscode.Uri = {
|
||||
parse: (uriString) => {
|
||||
const url = new URL(uriString)
|
||||
return {
|
||||
scheme: url.protocol.replace(":", ""),
|
||||
authority: url.hostname,
|
||||
path: url.pathname,
|
||||
query: url.search.slice(1),
|
||||
fragment: url.hash.slice(1),
|
||||
fsPath: `/tmp${url.pathname}`,
|
||||
toString: () => uriString,
|
||||
toJSON: () => uriString,
|
||||
with: (change) => {
|
||||
const newUrl = new URL(uriString)
|
||||
if (change.scheme) newUrl.protocol = change.scheme + ":"
|
||||
if (change.authority) newUrl.hostname = change.authority
|
||||
if (change.path) newUrl.pathname = change.path
|
||||
if (change.query) newUrl.search = "?" + change.query
|
||||
if (change.fragment) newUrl.hash = "#" + change.fragment
|
||||
return vscode.Uri.parse(newUrl.toString())
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
file: (path) => {
|
||||
return {
|
||||
scheme: "file",
|
||||
authority: "",
|
||||
path,
|
||||
fsPath: path,
|
||||
query: "",
|
||||
fragment: "",
|
||||
toString: () => `file://${path}`,
|
||||
toJSON: () => `file://${path}`,
|
||||
with: (change) => {
|
||||
const modified = Object.assign({}, vscode.Uri.file(path), change)
|
||||
return modified
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
joinPath: (...segments) => {
|
||||
const joined = segments.map((s) => (typeof s === "string" ? s : s.path)).join("/")
|
||||
return vscode.Uri.file("/" + joined.replace(/\/+/g, "/"))
|
||||
},
|
||||
}
|
||||
|
||||
vscode.env.openExternal = async (uri) => {
|
||||
const url = typeof uri === "string" ? uri : (uri.toString?.() ?? "")
|
||||
console.log("Opening browser:", url)
|
||||
await open(url)
|
||||
return true
|
||||
}
|
||||
|
||||
console.log("Finished loading stub impls...")
|
||||
File diff suppressed because it is too large
Load Diff
Generated
+2001
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,8 @@
|
||||
"lint": "eslint .",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest dev",
|
||||
"test:coverage": "vitest run --coverage"
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"devtools": "react-devtools"
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "^0.27.4",
|
||||
@@ -67,6 +68,7 @@
|
||||
"eslint-plugin-react-refresh": "^0.4.16",
|
||||
"globals": "^15.14.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"react-devtools": "^6.1.2",
|
||||
"tailwindcss": "^4.1.5",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.18.2",
|
||||
|
||||
@@ -259,7 +259,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const { filePaths, chatSettings, apiConfiguration, openRouterModels, platform } = useExtensionState()
|
||||
const { filePaths, chatSettings, apiConfiguration, openRouterModels, platform, workflowToggles } = useExtensionState()
|
||||
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false)
|
||||
const [isDraggingOver, setIsDraggingOver] = useState(false)
|
||||
const [gitCommits, setGitCommits] = useState<GitCommit[]>([])
|
||||
@@ -373,6 +373,25 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
}
|
||||
}, [showContextMenu, setShowContextMenu])
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutsideSlashMenu = (event: MouseEvent) => {
|
||||
if (
|
||||
slashCommandsMenuContainerRef.current &&
|
||||
!slashCommandsMenuContainerRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setShowSlashCommandsMenu(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (showSlashCommandsMenu) {
|
||||
document.addEventListener("mousedown", handleClickOutsideSlashMenu)
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutsideSlashMenu)
|
||||
}
|
||||
}, [showSlashCommandsMenu])
|
||||
|
||||
const handleMentionSelect = useCallback(
|
||||
(type: ContextMenuOptionType, value?: string) => {
|
||||
if (type === ContextMenuOptionType.NoResults) {
|
||||
@@ -463,13 +482,18 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
event.preventDefault()
|
||||
setSelectedSlashCommandsIndex((prevIndex) => {
|
||||
const direction = event.key === "ArrowUp" ? -1 : 1
|
||||
const commands = getMatchingSlashCommands(slashCommandsQuery)
|
||||
// Get commands with workflow toggles
|
||||
const allCommands = getMatchingSlashCommands(slashCommandsQuery, workflowToggles)
|
||||
|
||||
if (commands.length === 0) {
|
||||
if (allCommands.length === 0) {
|
||||
return prevIndex
|
||||
}
|
||||
|
||||
const newIndex = (prevIndex + direction + commands.length) % commands.length
|
||||
// Calculate total command count
|
||||
const totalCommandCount = allCommands.length
|
||||
|
||||
// Create wraparound navigation - moves from last item to first and vice versa
|
||||
const newIndex = (prevIndex + direction + totalCommandCount) % totalCommandCount
|
||||
return newIndex
|
||||
})
|
||||
return
|
||||
@@ -477,7 +501,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
|
||||
if ((event.key === "Enter" || event.key === "Tab") && selectedSlashCommandsIndex !== -1) {
|
||||
event.preventDefault()
|
||||
const commands = getMatchingSlashCommands(slashCommandsQuery)
|
||||
const commands = getMatchingSlashCommands(slashCommandsQuery, workflowToggles)
|
||||
if (commands.length > 0) {
|
||||
handleSlashCommandsSelect(commands[selectedSlashCommandsIndex])
|
||||
}
|
||||
@@ -880,7 +904,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
|
||||
// extract and validate the exact command text
|
||||
const commandText = processedText.substring(slashIndex + 1, endIndex)
|
||||
const isValidCommand = validateSlashCommand(commandText)
|
||||
const isValidCommand = validateSlashCommand(commandText, workflowToggles)
|
||||
|
||||
if (isValidCommand) {
|
||||
const fullCommand = processedText.substring(slashIndex, endIndex) // includes slash
|
||||
@@ -893,7 +917,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
highlightLayerRef.current.innerHTML = processedText
|
||||
highlightLayerRef.current.scrollTop = textAreaRef.current.scrollTop
|
||||
highlightLayerRef.current.scrollLeft = textAreaRef.current.scrollLeft
|
||||
}, [])
|
||||
}, [workflowToggles])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
updateHighlights()
|
||||
@@ -1373,6 +1397,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
setSelectedIndex={setSelectedSlashCommandsIndex}
|
||||
onMouseDown={handleMenuMouseDown}
|
||||
query={slashCommandsQuery}
|
||||
workflowToggles={workflowToggles}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -18,11 +18,10 @@ import { combineCommandSequences } from "@shared/combineCommandSequences"
|
||||
import { getApiMetrics } from "@shared/getApiMetrics"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { TaskServiceClient, SlashServiceClient } from "@/services/grpc-client"
|
||||
import { TaskServiceClient, SlashServiceClient, FileServiceClient } from "@/services/grpc-client"
|
||||
import HistoryPreview from "@/components/history/HistoryPreview"
|
||||
import { normalizeApiConfiguration } from "@/components/settings/ApiOptions"
|
||||
import Announcement from "@/components/chat/Announcement"
|
||||
import AutoApproveMenu from "@/components/chat/auto-approve-menu/AutoApproveMenu"
|
||||
import BrowserSessionRow from "@/components/chat/BrowserSessionRow"
|
||||
import ChatRow from "@/components/chat/ChatRow"
|
||||
import ChatTextArea from "@/components/chat/ChatTextArea"
|
||||
@@ -34,7 +33,8 @@ import remarkStringify from "remark-stringify"
|
||||
import rehypeRemark from "rehype-remark"
|
||||
import rehypeParse from "rehype-parse"
|
||||
import HomeHeader from "../welcome/HomeHeader"
|
||||
|
||||
import AutoApproveBar from "./auto-approve-menu/AutoApproveBar"
|
||||
import { SuggestedTasks } from "../welcome/SuggestedTasks"
|
||||
interface ChatViewProps {
|
||||
isHidden: boolean
|
||||
showAnnouncement: boolean
|
||||
@@ -633,8 +633,15 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
return normalizeApiConfiguration(apiConfiguration)
|
||||
}, [apiConfiguration])
|
||||
|
||||
const selectImages = useCallback(() => {
|
||||
vscode.postMessage({ type: "selectImages" })
|
||||
const selectImages = useCallback(async () => {
|
||||
try {
|
||||
const response = await FileServiceClient.selectImages({})
|
||||
if (response && response.values && response.values.length > 0) {
|
||||
setSelectedImages((prevImages) => [...prevImages, ...response.values].slice(0, MAX_IMAGES_PER_MESSAGE))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error selecting images:", error)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const shouldDisableImages = !selectedModelInfo.supportsImages || selectedImages.length >= MAX_IMAGES_PER_MESSAGE
|
||||
@@ -977,6 +984,14 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
)
|
||||
}
|
||||
|
||||
// We display certain statuses for the last message only
|
||||
// If the last message is a checkpoint, we want to show the status of the previous message
|
||||
const nextMessage = index < groupedMessages.length - 1 && groupedMessages[index + 1]
|
||||
const isNextCheckpoint = !Array.isArray(nextMessage) && nextMessage && nextMessage?.say === "checkpoint_created"
|
||||
const isLastMessageGroup = isNextCheckpoint && index === groupedMessages.length - 2
|
||||
|
||||
const isLast = index === groupedMessages.length - 1 || isLastMessageGroup
|
||||
|
||||
// regular message
|
||||
return (
|
||||
<ChatRow
|
||||
@@ -985,7 +1000,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
isExpanded={expandedRows[messageOrGroup.ts] || false}
|
||||
onToggleExpand={() => toggleRowExpansion(messageOrGroup.ts)}
|
||||
lastModifiedMessage={modifiedMessages.at(-1)}
|
||||
isLast={index === groupedMessages.length - 1}
|
||||
isLast={isLast}
|
||||
onHeightChange={handleRowHeightChange}
|
||||
inputValue={inputValue}
|
||||
sendMessageFromChatRow={handleSendMessage}
|
||||
@@ -1047,7 +1062,12 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!task && <AutoApproveMenu />}
|
||||
{!task && (
|
||||
<>
|
||||
<SuggestedTasks />
|
||||
<AutoApproveBar />
|
||||
</>
|
||||
)}
|
||||
|
||||
{task && (
|
||||
<>
|
||||
@@ -1081,7 +1101,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
initialTopMostItemIndex={groupedMessages.length - 1}
|
||||
/>
|
||||
</div>
|
||||
<AutoApproveMenu />
|
||||
<AutoApproveBar />
|
||||
{showScrollToBottom ? (
|
||||
<div
|
||||
style={{
|
||||
|
||||
@@ -7,9 +7,17 @@ interface SlashCommandMenuProps {
|
||||
setSelectedIndex: (index: number) => void
|
||||
onMouseDown: () => void
|
||||
query: string
|
||||
workflowToggles?: Record<string, boolean>
|
||||
}
|
||||
|
||||
const SlashCommandMenu: React.FC<SlashCommandMenuProps> = ({ onSelect, selectedIndex, setSelectedIndex, onMouseDown, query }) => {
|
||||
const SlashCommandMenu: React.FC<SlashCommandMenuProps> = ({
|
||||
onSelect,
|
||||
selectedIndex,
|
||||
setSelectedIndex,
|
||||
onMouseDown,
|
||||
query,
|
||||
workflowToggles = {},
|
||||
}) => {
|
||||
const menuRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const handleClick = useCallback(
|
||||
@@ -19,10 +27,9 @@ const SlashCommandMenu: React.FC<SlashCommandMenuProps> = ({ onSelect, selectedI
|
||||
[onSelect],
|
||||
)
|
||||
|
||||
// Auto-scroll logic remains the same...
|
||||
useEffect(() => {
|
||||
if (menuRef.current) {
|
||||
const selectedElement = menuRef.current.children[selectedIndex] as HTMLElement
|
||||
const selectedElement = menuRef.current.querySelector(`#slash-command-menu-item-${selectedIndex}`) as HTMLElement
|
||||
if (selectedElement) {
|
||||
const menuRect = menuRef.current.getBoundingClientRect()
|
||||
const selectedRect = selectedElement.getBoundingClientRect()
|
||||
@@ -37,7 +44,46 @@ const SlashCommandMenu: React.FC<SlashCommandMenuProps> = ({ onSelect, selectedI
|
||||
}, [selectedIndex])
|
||||
|
||||
// Filter commands based on query
|
||||
const filteredCommands = getMatchingSlashCommands(query)
|
||||
const filteredCommands = getMatchingSlashCommands(query, workflowToggles)
|
||||
const defaultCommands = filteredCommands.filter((cmd) => cmd.section === "default" || !cmd.section)
|
||||
const workflowCommands = filteredCommands.filter((cmd) => cmd.section === "custom")
|
||||
|
||||
// Create a reusable function for rendering a command section
|
||||
const renderCommandSection = (commands: SlashCommand[], title: string, indexOffset: number, showDescriptions: boolean) => {
|
||||
if (commands.length === 0) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="text-xs text-[var(--vscode-descriptionForeground)] px-3 py-1 font-bold border-b border-[var(--vscode-editorGroup-border)]">
|
||||
{title}
|
||||
</div>
|
||||
{commands.map((command, index) => {
|
||||
const itemIndex = index + indexOffset
|
||||
return (
|
||||
<div
|
||||
key={command.name}
|
||||
id={`slash-command-menu-item-${itemIndex}`}
|
||||
className={`slash-command-menu-item py-2 px-3 cursor-pointer flex flex-col border-b border-[var(--vscode-editorGroup-border)] ${
|
||||
itemIndex === selectedIndex
|
||||
? "bg-[var(--vscode-quickInputList-focusBackground)] text-[var(--vscode-quickInputList-focusForeground)]"
|
||||
: ""
|
||||
} hover:bg-[var(--vscode-list-hoverBackground)]`}
|
||||
onClick={() => handleClick(command)}
|
||||
onMouseEnter={() => setSelectedIndex(itemIndex)}>
|
||||
<div className="font-bold whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
<span className="ph-no-capture">/{command.name}</span>
|
||||
</div>
|
||||
{showDescriptions && command.description && (
|
||||
<div className="text-[0.85em] text-[var(--vscode-descriptionForeground)] whitespace-normal overflow-hidden text-ellipsis">
|
||||
<span className="ph-no-capture">{command.description}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -45,33 +91,15 @@ const SlashCommandMenu: React.FC<SlashCommandMenuProps> = ({ onSelect, selectedI
|
||||
onMouseDown={onMouseDown}>
|
||||
<div
|
||||
ref={menuRef}
|
||||
className="bg-[var(--vscode-dropdown-background)] border border-[var(--vscode-editorGroup-border)] rounded-[3px] shadow-[0_4px_10px_rgba(0,0,0,0.25)] flex flex-col max-h-[200px] overflow-y-auto" // Corrected rounded and shadow
|
||||
>
|
||||
className="bg-[var(--vscode-dropdown-background)] border border-[var(--vscode-editorGroup-border)] rounded-[3px] shadow-[0_4px_10px_rgba(0,0,0,0.25)] flex flex-col overflow-y-auto"
|
||||
style={{ maxHeight: "min(200px, calc(50vh))", overscrollBehavior: "contain" }}>
|
||||
{filteredCommands.length > 0 ? (
|
||||
filteredCommands.map((command, index) => (
|
||||
<div
|
||||
key={command.name}
|
||||
id={`slash-command-menu-item-${index}`}
|
||||
className={`slash-command-menu-item py-2 px-3 cursor-pointer flex flex-col border-b border-[var(--vscode-editorGroup-border)] ${
|
||||
// Corrected padding
|
||||
index === selectedIndex
|
||||
? "bg-[var(--vscode-quickInputList-focusBackground)] text-[var(--vscode-quickInputList-focusForeground)]"
|
||||
: "" // Removed bg-transparent
|
||||
} hover:bg-[var(--vscode-list-hoverBackground)]`}
|
||||
onClick={() => handleClick(command)}
|
||||
onMouseEnter={() => setSelectedIndex(index)}>
|
||||
<div className="font-bold whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
<span className="ph-no-capture">/{command.name}</span>
|
||||
</div>
|
||||
<div className="text-[0.85em] text-[var(--vscode-descriptionForeground)] whitespace-normal overflow-hidden text-ellipsis">
|
||||
<span className="ph-no-capture">{command.description}</span>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
<>
|
||||
{renderCommandSection(defaultCommands, "Default Commands", 0, true)}
|
||||
{renderCommandSection(workflowCommands, "Workflow Commands", defaultCommands.length, false)}
|
||||
</>
|
||||
) : (
|
||||
<div className="py-2 px-3 cursor-default flex flex-col">
|
||||
{" "}
|
||||
{/* Corrected padding, removed border, changed cursor */}
|
||||
<div className="text-[0.85em] text-[var(--vscode-descriptionForeground)]">No matching commands found</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useMemo, useState, useRef, useEffect } from "react"
|
||||
import React, { useMemo, useState, useRef, useEffect, useCallback } from "react"
|
||||
import { Virtuoso } from "react-virtuoso"
|
||||
import { ClineMessage } from "@shared/ExtensionMessage"
|
||||
import { combineApiRequests } from "@shared/combineApiRequests"
|
||||
import { combineCommandSequences } from "@shared/combineCommandSequences"
|
||||
@@ -94,8 +95,6 @@ const getBlockColor = (message: ClineMessage): string => {
|
||||
}
|
||||
|
||||
const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages }) => {
|
||||
const [hoveredMessage, setHoveredMessage] = useState<ClineMessage | null>(null)
|
||||
const [tooltipPosition, setTooltipPosition] = useState<{ x: number; y: number } | null>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const scrollableRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
@@ -136,27 +135,48 @@ const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages }) => {
|
||||
}
|
||||
}, [taskTimelinePropsMessages])
|
||||
|
||||
// Calculate the item size (width of block + gap)
|
||||
const itemWidth = parseInt(BLOCK_WIDTH.replace("px", "")) + parseInt(BLOCK_GAP.replace("px", ""))
|
||||
|
||||
// Virtuoso requires a reference to scroll to the end
|
||||
const virtuosoRef = useRef<any>(null)
|
||||
|
||||
// Render a timeline block
|
||||
const TimelineBlock = useCallback(
|
||||
(index: number) => {
|
||||
const message = taskTimelinePropsMessages[index]
|
||||
return (
|
||||
<TaskTimelineTooltip message={message}>
|
||||
<div
|
||||
style={{
|
||||
width: BLOCK_WIDTH,
|
||||
height: "100%",
|
||||
backgroundColor: getBlockColor(message),
|
||||
flexShrink: 0,
|
||||
cursor: "pointer",
|
||||
marginRight: BLOCK_GAP,
|
||||
}}
|
||||
/>
|
||||
</TaskTimelineTooltip>
|
||||
)
|
||||
},
|
||||
[taskTimelinePropsMessages],
|
||||
)
|
||||
|
||||
// Scroll to the end when messages change
|
||||
useEffect(() => {
|
||||
if (virtuosoRef.current && taskTimelinePropsMessages.length > 0) {
|
||||
virtuosoRef.current.scrollToIndex({
|
||||
index: taskTimelinePropsMessages.length - 1,
|
||||
align: "end",
|
||||
})
|
||||
}
|
||||
}, [taskTimelinePropsMessages])
|
||||
|
||||
if (taskTimelinePropsMessages.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const handleMouseEnter = (message: ClineMessage, event: React.MouseEvent<HTMLDivElement>) => {
|
||||
setHoveredMessage(message)
|
||||
|
||||
const viewportWidth = window.innerWidth
|
||||
const tooltipWidth = viewportWidth - TOOLTIP_MARGIN * 2
|
||||
|
||||
// Center the tooltip horizontally in the viewport
|
||||
const x = TOOLTIP_MARGIN
|
||||
|
||||
setTooltipPosition({ x, y: event.clientY })
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setHoveredMessage(null)
|
||||
setTooltipPosition(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
@@ -167,55 +187,32 @@ const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages }) => {
|
||||
marginBottom: "4px",
|
||||
overflow: "hidden",
|
||||
}}>
|
||||
<div
|
||||
ref={scrollableRef}
|
||||
style={{
|
||||
display: "flex",
|
||||
height: TIMELINE_HEIGHT,
|
||||
overflowX: "auto",
|
||||
scrollbarWidth: "none",
|
||||
msOverflowStyle: "none",
|
||||
width: "100%",
|
||||
WebkitOverflowScrolling: "touch",
|
||||
gap: BLOCK_GAP, // Using flexbox gap instead of marginRight
|
||||
}}>
|
||||
<style>
|
||||
{`
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
div::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
{taskTimelinePropsMessages.map((message, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
width: BLOCK_WIDTH,
|
||||
height: "100%",
|
||||
backgroundColor: getBlockColor(message),
|
||||
flexShrink: 0,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onMouseEnter={(e) => handleMouseEnter(message, e)}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<style>
|
||||
{`
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
.timeline-virtuoso::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.timeline-virtuoso {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
{hoveredMessage && containerRef.current && tooltipPosition && (
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
left: `${tooltipPosition.x}px`,
|
||||
top: `${tooltipPosition.y + 20}px`,
|
||||
zIndex: 1000,
|
||||
pointerEvents: "none",
|
||||
width: `calc(100% - ${TOOLTIP_MARGIN * 2}px)`,
|
||||
}}>
|
||||
<TaskTimelineTooltip message={hoveredMessage} />
|
||||
</div>
|
||||
)}
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
className="timeline-virtuoso"
|
||||
style={{
|
||||
height: TIMELINE_HEIGHT,
|
||||
width: "100%",
|
||||
}}
|
||||
totalCount={taskTimelinePropsMessages.length}
|
||||
itemContent={TimelineBlock}
|
||||
horizontalDirection={true}
|
||||
increaseViewportBy={12}
|
||||
fixedItemHeight={itemWidth}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import React from "react"
|
||||
import { ClineMessage } from "@shared/ExtensionMessage"
|
||||
import { COLOR_WHITE, COLOR_GRAY, COLOR_DARK_GRAY, COLOR_BEIGE, COLOR_BLUE, COLOR_RED, COLOR_PURPLE, COLOR_GREEN } from "./colors"
|
||||
import { Tooltip } from "@heroui/react"
|
||||
|
||||
// Color mapping for different message types
|
||||
|
||||
interface TaskTimelineTooltipProps {
|
||||
message: ClineMessage
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const TaskTimelineTooltip: React.FC<TaskTimelineTooltipProps> = ({ message }) => {
|
||||
const TaskTimelineTooltip = ({ message, children }: TaskTimelineTooltipProps) => {
|
||||
const getMessageDescription = (message: ClineMessage): string => {
|
||||
if (message.type === "say") {
|
||||
switch (message.say) {
|
||||
@@ -227,53 +229,61 @@ const TaskTimelineTooltip: React.FC<TaskTimelineTooltipProps> = ({ message }) =>
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: "var(--vscode-editor-background)",
|
||||
color: "var(--vscode-editor-foreground)",
|
||||
border: "1px solid var(--vscode-widget-border)",
|
||||
borderRadius: "3px",
|
||||
padding: "8px",
|
||||
width: "100%", // Fill the container width
|
||||
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.15)",
|
||||
fontSize: "12px",
|
||||
}}>
|
||||
<div style={{ fontWeight: "bold", marginBottom: "4px", display: "flex", alignItems: "center" }}>
|
||||
<div
|
||||
style={{
|
||||
width: "10px",
|
||||
height: "10px",
|
||||
minWidth: "10px", // Ensure fixed width
|
||||
minHeight: "10px", // Ensure fixed height
|
||||
borderRadius: "50%",
|
||||
backgroundColor: getMessageColor(message),
|
||||
marginRight: "8px",
|
||||
display: "inline-block",
|
||||
flexShrink: 0, // Prevent shrinking when space is limited
|
||||
}}
|
||||
/>
|
||||
{getMessageDescription(message)}
|
||||
{getTimestamp(message) && (
|
||||
<span style={{ fontWeight: "normal", fontSize: "10px", marginLeft: "8px" }}>{getTimestamp(message)}</span>
|
||||
)}
|
||||
</div>
|
||||
{getMessageContent(message) && (
|
||||
<div
|
||||
style={{
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-word",
|
||||
maxHeight: "150px",
|
||||
overflowY: "auto",
|
||||
fontSize: "11px",
|
||||
fontFamily: "var(--vscode-editor-font-family)",
|
||||
backgroundColor: "var(--vscode-textBlockQuote-background)",
|
||||
padding: "4px",
|
||||
borderRadius: "2px",
|
||||
}}>
|
||||
{getMessageContent(message)}
|
||||
<Tooltip
|
||||
content={
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-wrap items-center font-bold mb-1">
|
||||
<div className="mr-4 mb-0.5">
|
||||
<div
|
||||
style={{
|
||||
width: "10px",
|
||||
height: "10px",
|
||||
minWidth: "10px", // Ensure fixed width
|
||||
minHeight: "10px", // Ensure fixed height
|
||||
borderRadius: "50%",
|
||||
backgroundColor: getMessageColor(message),
|
||||
marginRight: "8px",
|
||||
display: "inline-block",
|
||||
flexShrink: 0, // Prevent shrinking when space is limited
|
||||
}}
|
||||
/>
|
||||
{getMessageDescription(message)}
|
||||
</div>
|
||||
{getTimestamp(message) && (
|
||||
<span className="font-normal text-tiny" style={{ fontWeight: "normal", fontSize: "10px" }}>
|
||||
{getTimestamp(message)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{getMessageContent(message) && (
|
||||
<div
|
||||
style={{
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-word",
|
||||
maxHeight: "150px",
|
||||
overflowY: "auto",
|
||||
fontSize: "11px",
|
||||
fontFamily: "var(--vscode-editor-font-family)",
|
||||
backgroundColor: "var(--vscode-textBlockQuote-background)",
|
||||
padding: "4px",
|
||||
borderRadius: "2px",
|
||||
scrollbarWidth: "none",
|
||||
}}>
|
||||
{getMessageContent(message)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
classNames={{
|
||||
base: "bg-[var(--vscode-editor-background)] text-[var(--vscode-editor-foreground)] border-[var(--vscode-widget-border)] py-1 rounded-[3px] max-w-[calc(100dvw-2rem)] text-xs",
|
||||
}}
|
||||
shadow="sm"
|
||||
placement="bottom"
|
||||
disableAnimation
|
||||
closeDelay={100}
|
||||
isKeyboardDismissDisabled={true}>
|
||||
{children}
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { useCallback, useRef, useState, useMemo } from "react"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
|
||||
import { getAsVar, VSC_TITLEBAR_INACTIVE_FOREGROUND } from "@/utils/vscStyles"
|
||||
import AutoApproveMenuItem from "./AutoApproveMenuItem"
|
||||
import AutoApproveModal from "./AutoApproveModal"
|
||||
import { ACTION_METADATA, NOTIFICATIONS_SETTING } from "./constants"
|
||||
import { ActionMetadata } from "./types"
|
||||
|
||||
interface AutoApproveBarProps {
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
|
||||
const AutoApproveBar = ({ style }: AutoApproveBarProps) => {
|
||||
const { autoApprovalSettings } = useExtensionState()
|
||||
const [isModalVisible, setIsModalVisible] = useState(false)
|
||||
const buttonRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Favorites are derived from autoApprovalSettings
|
||||
const favorites = useMemo(() => autoApprovalSettings.favorites || [], [autoApprovalSettings.favorites])
|
||||
|
||||
// Render a favorited item with a checkbox
|
||||
const renderFavoritedItem = (favId: string) => {
|
||||
const actions = [...ACTION_METADATA.flatMap((a) => [a, a.subAction]), NOTIFICATIONS_SETTING]
|
||||
const action = actions.find((a) => a?.id === favId)
|
||||
if (!action) return null
|
||||
|
||||
return (
|
||||
<AutoApproveMenuItem
|
||||
action={action}
|
||||
isChecked={isChecked}
|
||||
isFavorited={isFavorited}
|
||||
onToggle={updateAction}
|
||||
condensed={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const getQuickAccessItems = () => {
|
||||
const notificationsEnabled = autoApprovalSettings.enableNotifications
|
||||
const enabledActionsNames = Object.keys(autoApprovalSettings.actions).filter(
|
||||
(key) => autoApprovalSettings.actions[key as keyof typeof autoApprovalSettings.actions],
|
||||
)
|
||||
const enabledActions = enabledActionsNames.map((action) => {
|
||||
return ACTION_METADATA.flatMap((a) => [a, a.subAction]).find((a) => a?.id === action)
|
||||
})
|
||||
|
||||
let minusFavorites = enabledActions.filter((action) => !favorites.includes(action?.id ?? "") && action?.shortName)
|
||||
|
||||
if (notificationsEnabled) {
|
||||
minusFavorites.push(NOTIFICATIONS_SETTING)
|
||||
}
|
||||
|
||||
return [
|
||||
...favorites.map((favId) => renderFavoritedItem(favId)),
|
||||
minusFavorites.length > 0 ? (
|
||||
<span className="text-[color:var(--vscode-foreground-muted)] pl-[10px] opacity-60" key="separator">
|
||||
✓
|
||||
</span>
|
||||
) : null,
|
||||
...minusFavorites.map((action, index) => (
|
||||
<span className="text-[color:var(--vscode-foreground-muted)] opacity-60" key={action?.id}>
|
||||
{action?.shortName}
|
||||
{index < minusFavorites.length - 1 && ","}
|
||||
</span>
|
||||
)),
|
||||
]
|
||||
}
|
||||
|
||||
const isChecked = (action: ActionMetadata): boolean => {
|
||||
if (action.id === "enableNotifications") {
|
||||
return autoApprovalSettings.enableNotifications
|
||||
}
|
||||
if (action.id === "enableAll") {
|
||||
return Object.values(autoApprovalSettings.actions).every(Boolean)
|
||||
}
|
||||
return autoApprovalSettings.actions[action.id] ?? false
|
||||
}
|
||||
|
||||
const isFavorited = (action: ActionMetadata): boolean => {
|
||||
return favorites.includes(action.id)
|
||||
}
|
||||
|
||||
const updateAction = useCallback(() => {
|
||||
// This is just a placeholder since we need to pass it to AutoApproveMenuItem
|
||||
// The actual implementation is in the modal component
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
className="px-[10px] mx-[5px] select-none rounded-[10px_10px_0_0]"
|
||||
style={{
|
||||
borderTop: `0.5px solid color-mix(in srgb, ${getAsVar(VSC_TITLEBAR_INACTIVE_FOREGROUND)} 20%, transparent)`,
|
||||
overflowY: "auto",
|
||||
backgroundColor: isModalVisible ? CODE_BLOCK_BG_COLOR : "transparent",
|
||||
...style,
|
||||
}}>
|
||||
<div
|
||||
ref={buttonRef}
|
||||
className="cursor-pointer py-[8px] pr-[2px] flex items-center justify-between gap-[8px]"
|
||||
onClick={() => {
|
||||
setIsModalVisible((prev) => !prev)
|
||||
}}>
|
||||
<div
|
||||
className="flex flex-nowrap items-center overflow-x-auto gap-[4px] whitespace-nowrap"
|
||||
style={{
|
||||
msOverflowStyle: "none",
|
||||
scrollbarWidth: "none",
|
||||
WebkitOverflowScrolling: "touch",
|
||||
}}>
|
||||
<span>Auto-approve:</span>
|
||||
{getQuickAccessItems()}
|
||||
</div>
|
||||
{isModalVisible ? (
|
||||
<span className="codicon codicon-chevron-down" />
|
||||
) : (
|
||||
<span className="codicon codicon-chevron-up" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<AutoApproveModal
|
||||
isVisible={isModalVisible}
|
||||
setIsVisible={setIsModalVisible}
|
||||
buttonRef={buttonRef}
|
||||
ACTION_METADATA={ACTION_METADATA}
|
||||
NOTIFICATIONS_SETTING={NOTIFICATIONS_SETTING}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AutoApproveBar
|
||||
@@ -1,513 +0,0 @@
|
||||
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
|
||||
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
|
||||
import AutoApproveMenuItem from "./AutoApproveMenuItem"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { getAsVar, VSC_FOREGROUND, VSC_TITLEBAR_INACTIVE_FOREGROUND, VSC_FOREGROUND_MUTED } from "@/utils/vscStyles"
|
||||
import { useClickAway } from "react-use"
|
||||
import HeroTooltip from "@/components/common/HeroTooltip"
|
||||
|
||||
const breakpoint = 500
|
||||
|
||||
interface AutoApproveMenuProps {
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
|
||||
export interface ActionMetadata {
|
||||
id: keyof AutoApprovalSettings["actions"] | "enableNotifications" | "enableAll"
|
||||
label: string
|
||||
shortName: string
|
||||
description: string
|
||||
icon: string
|
||||
subAction?: ActionMetadata
|
||||
sub?: boolean
|
||||
parentActionId?: string
|
||||
}
|
||||
|
||||
const ACTION_METADATA: ActionMetadata[] = [
|
||||
{
|
||||
id: "enableAll",
|
||||
label: "Enable all",
|
||||
shortName: "All",
|
||||
description: "Enable all actions.",
|
||||
icon: "codicon-checklist",
|
||||
},
|
||||
{
|
||||
id: "readFiles",
|
||||
label: "Read project files",
|
||||
shortName: "Read",
|
||||
description: "Allows Cline to read files within your workspace.",
|
||||
icon: "codicon-search",
|
||||
subAction: {
|
||||
id: "readFilesExternally",
|
||||
label: "Read all files",
|
||||
shortName: "Read (all)",
|
||||
description: "Allows Cline to read any file on your computer.",
|
||||
icon: "codicon-folder-opened",
|
||||
parentActionId: "readFiles",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "editFiles",
|
||||
label: "Edit project files",
|
||||
shortName: "Edit",
|
||||
description: "Allows Cline to modify files within your workspace.",
|
||||
icon: "codicon-edit",
|
||||
subAction: {
|
||||
id: "editFilesExternally",
|
||||
label: "Edit all files",
|
||||
shortName: "Edit (all)",
|
||||
description: "Allows Cline to modify any file on your computer.",
|
||||
icon: "codicon-files",
|
||||
parentActionId: "editFiles",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "executeSafeCommands",
|
||||
label: "Execute safe commands",
|
||||
shortName: "Safe Commands",
|
||||
description:
|
||||
"Allows Cline to execute safe terminal commands. If the model determines a command is potentially destructive, it will still require approval.",
|
||||
icon: "codicon-terminal",
|
||||
subAction: {
|
||||
id: "executeAllCommands",
|
||||
label: "Execute all commands",
|
||||
shortName: "All Commands",
|
||||
description: "Allows Cline to execute all terminal commands. Use at your own risk.",
|
||||
icon: "codicon-terminal-bash",
|
||||
parentActionId: "executeSafeCommands",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "useBrowser",
|
||||
label: "Use the browser",
|
||||
shortName: "Browser",
|
||||
description: "Allows Cline to launch and interact with any website in a browser.",
|
||||
icon: "codicon-globe",
|
||||
},
|
||||
{
|
||||
id: "useMcp",
|
||||
label: "Use MCP servers",
|
||||
shortName: "MCP",
|
||||
description: "Allows Cline to use configured MCP servers which may modify filesystem or interact with APIs.",
|
||||
icon: "codicon-server",
|
||||
},
|
||||
]
|
||||
|
||||
const NOTIFICATIONS_SETTING: ActionMetadata = {
|
||||
id: "enableNotifications",
|
||||
label: "Enable notifications",
|
||||
shortName: "Notifications",
|
||||
description: "Receive system notifications when Cline requires approval to proceed or when a task is completed.",
|
||||
icon: "codicon-bell",
|
||||
}
|
||||
|
||||
const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
|
||||
const { autoApprovalSettings } = useExtensionState()
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const [containerWidth, setContainerWidth] = useState(0)
|
||||
// Favorites are now derived from autoApprovalSettings
|
||||
const favorites = useMemo(() => autoApprovalSettings.favorites || [], [autoApprovalSettings.favorites])
|
||||
const menuRef = useRef<HTMLDivElement>(null)
|
||||
const itemsContainerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Track container width for responsive layout
|
||||
useEffect(() => {
|
||||
if (!isExpanded) return
|
||||
|
||||
const updateWidth = () => {
|
||||
if (itemsContainerRef.current) {
|
||||
setContainerWidth(itemsContainerRef.current.offsetWidth)
|
||||
}
|
||||
}
|
||||
|
||||
// Initial measurement
|
||||
updateWidth()
|
||||
|
||||
// Set up resize observer
|
||||
const resizeObserver = new ResizeObserver(updateWidth)
|
||||
if (itemsContainerRef.current) {
|
||||
resizeObserver.observe(itemsContainerRef.current)
|
||||
}
|
||||
|
||||
// Clean up
|
||||
return () => {
|
||||
resizeObserver.disconnect()
|
||||
}
|
||||
}, [isExpanded])
|
||||
|
||||
const toggleFavorite = useCallback(
|
||||
(actionId: string) => {
|
||||
const currentFavorites = autoApprovalSettings.favorites || []
|
||||
let newFavorites: string[]
|
||||
|
||||
if (currentFavorites.includes(actionId)) {
|
||||
newFavorites = currentFavorites.filter((id) => id !== actionId)
|
||||
} else {
|
||||
newFavorites = [...currentFavorites, actionId]
|
||||
}
|
||||
|
||||
vscode.postMessage({
|
||||
type: "autoApprovalSettings",
|
||||
autoApprovalSettings: {
|
||||
...autoApprovalSettings,
|
||||
version: (autoApprovalSettings.version ?? 1) + 1,
|
||||
favorites: newFavorites,
|
||||
},
|
||||
})
|
||||
},
|
||||
[autoApprovalSettings],
|
||||
)
|
||||
|
||||
const updateAction = useCallback(
|
||||
(action: ActionMetadata, value: boolean) => {
|
||||
const actionId = action.id
|
||||
const subActionId = action.subAction?.id
|
||||
|
||||
if (actionId === "enableAll" || subActionId === "enableAll") {
|
||||
toggleAll(action, value)
|
||||
return
|
||||
}
|
||||
|
||||
if (actionId === "enableNotifications" || subActionId === "enableNotifications") {
|
||||
updateNotifications(action, value)
|
||||
return
|
||||
}
|
||||
|
||||
let newActions = {
|
||||
...autoApprovalSettings.actions,
|
||||
[actionId]: value,
|
||||
}
|
||||
|
||||
if (value === false && subActionId) {
|
||||
newActions[subActionId] = false
|
||||
}
|
||||
|
||||
if (value === true && action.parentActionId) {
|
||||
newActions[action.parentActionId as keyof AutoApprovalSettings["actions"]] = true
|
||||
}
|
||||
|
||||
// Check if this will result in any enabled actions
|
||||
const willHaveEnabledActions = Object.values(newActions).some(Boolean)
|
||||
|
||||
vscode.postMessage({
|
||||
type: "autoApprovalSettings",
|
||||
autoApprovalSettings: {
|
||||
...autoApprovalSettings,
|
||||
version: (autoApprovalSettings.version ?? 1) + 1,
|
||||
actions: newActions,
|
||||
enabled: willHaveEnabledActions,
|
||||
},
|
||||
})
|
||||
},
|
||||
[autoApprovalSettings],
|
||||
)
|
||||
|
||||
const updateMaxRequests = useCallback(
|
||||
(maxRequests: number) => {
|
||||
const currentSettings = autoApprovalSettings
|
||||
vscode.postMessage({
|
||||
type: "autoApprovalSettings",
|
||||
autoApprovalSettings: {
|
||||
...currentSettings,
|
||||
version: (currentSettings.version ?? 1) + 1,
|
||||
maxRequests,
|
||||
},
|
||||
})
|
||||
},
|
||||
[autoApprovalSettings],
|
||||
)
|
||||
|
||||
const updateNotifications = useCallback(
|
||||
(action: ActionMetadata, checked: boolean) => {
|
||||
if (action.id === "enableNotifications") {
|
||||
const currentSettings = autoApprovalSettings
|
||||
vscode.postMessage({
|
||||
type: "autoApprovalSettings",
|
||||
autoApprovalSettings: {
|
||||
...currentSettings,
|
||||
version: (currentSettings.version ?? 1) + 1,
|
||||
enableNotifications: checked,
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
[autoApprovalSettings],
|
||||
)
|
||||
|
||||
const toggleAll = useCallback(
|
||||
(action: ActionMetadata, checked: boolean) => {
|
||||
let actions = { ...autoApprovalSettings.actions }
|
||||
|
||||
for (const action of Object.keys(actions)) {
|
||||
actions[action as keyof AutoApprovalSettings["actions"]] = checked
|
||||
}
|
||||
|
||||
vscode.postMessage({
|
||||
type: "autoApprovalSettings",
|
||||
autoApprovalSettings: {
|
||||
...autoApprovalSettings,
|
||||
version: (autoApprovalSettings.version ?? 1) + 1,
|
||||
actions,
|
||||
},
|
||||
})
|
||||
},
|
||||
[autoApprovalSettings],
|
||||
)
|
||||
|
||||
// Handle clicks outside the menu to close it
|
||||
useClickAway(menuRef, () => {
|
||||
if (isExpanded) {
|
||||
setIsExpanded(false)
|
||||
}
|
||||
})
|
||||
|
||||
// Render a favorited item with a checkbox
|
||||
const renderFavoritedItem = (favId: string) => {
|
||||
const actions = [...ACTION_METADATA.flatMap((a) => [a, a.subAction]), NOTIFICATIONS_SETTING]
|
||||
const action = actions.find((a) => a?.id === favId)
|
||||
if (!action) return null
|
||||
|
||||
return (
|
||||
<AutoApproveMenuItem
|
||||
action={action}
|
||||
isChecked={isChecked}
|
||||
isFavorited={isFavorited}
|
||||
onToggle={updateAction}
|
||||
condensed={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// Render a favorited item with a checkbox
|
||||
const getQuickAccessItems = () => {
|
||||
const notificationsEnabled = autoApprovalSettings.enableNotifications
|
||||
const enabledActionsNames = Object.keys(autoApprovalSettings.actions).filter(
|
||||
(key) => autoApprovalSettings.actions[key as keyof AutoApprovalSettings["actions"]],
|
||||
)
|
||||
const enabledActions = enabledActionsNames.map((action) => {
|
||||
return ACTION_METADATA.flatMap((a) => [a, a.subAction]).find((a) => a?.id === action)
|
||||
})
|
||||
|
||||
let minusFavorites = enabledActions.filter((action) => !favorites.includes(action?.id ?? "") && action?.shortName)
|
||||
|
||||
if (notificationsEnabled) {
|
||||
minusFavorites.push(NOTIFICATIONS_SETTING)
|
||||
}
|
||||
|
||||
return [
|
||||
...favorites.map((favId) => renderFavoritedItem(favId)),
|
||||
minusFavorites.length > 0 ? (
|
||||
<span style={{ color: getAsVar(VSC_FOREGROUND_MUTED), paddingLeft: "10px", opacity: 0.6 }} key="separator">
|
||||
✓
|
||||
</span>
|
||||
) : null,
|
||||
...minusFavorites.map((action, index) => (
|
||||
<span
|
||||
style={{
|
||||
color: getAsVar(VSC_FOREGROUND_MUTED),
|
||||
opacity: 0.6,
|
||||
}}
|
||||
key={action?.id}>
|
||||
{action?.shortName}
|
||||
{index < minusFavorites.length - 1 && ","}
|
||||
</span>
|
||||
)),
|
||||
]
|
||||
}
|
||||
|
||||
const isChecked = (action: ActionMetadata): boolean => {
|
||||
if (action.id === "enableNotifications") {
|
||||
return autoApprovalSettings.enableNotifications
|
||||
}
|
||||
if (action.id === "enableAll") {
|
||||
return Object.values(autoApprovalSettings.actions).every(Boolean)
|
||||
}
|
||||
return autoApprovalSettings.actions[action.id] ?? false
|
||||
}
|
||||
|
||||
const isFavorited = (action: ActionMetadata): boolean => {
|
||||
return favorites.includes(action.id)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={menuRef}
|
||||
style={{
|
||||
padding: "0 4px 0 10px",
|
||||
margin: "0 5px",
|
||||
userSelect: "none",
|
||||
borderTop: `0.5px solid color-mix(in srgb, ${getAsVar(VSC_TITLEBAR_INACTIVE_FOREGROUND)} 20%, transparent)`,
|
||||
overflowY: "auto",
|
||||
borderRadius: "10px 10px 0 0",
|
||||
backgroundColor: isExpanded ? CODE_BLOCK_BG_COLOR : "transparent",
|
||||
...style,
|
||||
}}>
|
||||
{/* Collapsed view with favorited items */}
|
||||
{!isExpanded && (
|
||||
<div
|
||||
onClick={() => setIsExpanded(true)}
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
paddingTop: "6px",
|
||||
paddingRight: "2px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: "8px",
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexWrap: "nowrap",
|
||||
alignItems: "center",
|
||||
overflowX: "auto",
|
||||
msOverflowStyle: "none",
|
||||
scrollbarWidth: "none",
|
||||
WebkitOverflowScrolling: "touch",
|
||||
gap: "4px",
|
||||
whiteSpace: "nowrap", // Prevent text wrapping
|
||||
}}>
|
||||
<span>Auto-approve:</span>
|
||||
{getQuickAccessItems()}
|
||||
</div>
|
||||
<span className="codicon codicon-chevron-right" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Expanded view */}
|
||||
<div
|
||||
style={{
|
||||
maxHeight: isExpanded ? "1000px" : favorites.length > 0 ? "40px" : "22px", // Large enough to fit content
|
||||
opacity: isExpanded ? 1 : 0,
|
||||
overflow: "hidden",
|
||||
transition: "max-height 0.3s ease-in-out, opacity 0.3s ease-in-out",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "4px",
|
||||
}}>
|
||||
{isExpanded && ( // Re-added conditional rendering for content
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: "8px 4px 8px 0",
|
||||
cursor: "pointer",
|
||||
position: "relative", // Added for positioning context
|
||||
}}
|
||||
onClick={() => setIsExpanded(false)}>
|
||||
<HeroTooltip
|
||||
content="Auto-approve allows Cline to perform the following actions without asking for permission. Please use with caution and only enable if you understand the risks."
|
||||
placement="top">
|
||||
<span style={{ color: getAsVar(VSC_FOREGROUND), fontWeight: 500 }}>Auto-approve:</span>
|
||||
</HeroTooltip>
|
||||
<span className="codicon codicon-chevron-down" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={itemsContainerRef}
|
||||
style={{
|
||||
columnCount: containerWidth > breakpoint ? 2 : 1,
|
||||
columnGap: "4px",
|
||||
margin: "4px 0 16px 0",
|
||||
position: "relative", // For absolute positioning of the separator
|
||||
}}>
|
||||
{/* Vertical separator line - only visible in two-column mode */}
|
||||
{containerWidth > breakpoint && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: "50%",
|
||||
top: "0",
|
||||
bottom: "0",
|
||||
width: "0.5px",
|
||||
background: getAsVar(VSC_TITLEBAR_INACTIVE_FOREGROUND),
|
||||
opacity: 0.2,
|
||||
transform: "translateX(-50%)", // Center the line
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* All items in a single list - CSS Grid will handle the column distribution */}
|
||||
{ACTION_METADATA.map((action) => (
|
||||
<AutoApproveMenuItem
|
||||
key={action.id}
|
||||
action={action}
|
||||
isChecked={isChecked}
|
||||
isFavorited={isFavorited}
|
||||
onToggle={updateAction}
|
||||
onToggleFavorite={toggleFavorite}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<span style={{ color: getAsVar(VSC_FOREGROUND), marginBottom: 4, fontWeight: 500 }}>Quick Settings:</span>
|
||||
<AutoApproveMenuItem
|
||||
key={NOTIFICATIONS_SETTING.id}
|
||||
action={NOTIFICATIONS_SETTING}
|
||||
isChecked={isChecked}
|
||||
isFavorited={isFavorited}
|
||||
onToggle={updateAction}
|
||||
onToggleFavorite={toggleFavorite}
|
||||
/>
|
||||
<HeroTooltip
|
||||
content="Cline will automatically make this many API requests before asking for approval to proceed with the task."
|
||||
placement="top">
|
||||
<div
|
||||
style={{
|
||||
margin: "2px 10px 20px 5px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "8px",
|
||||
width: "100%",
|
||||
}}>
|
||||
<span className="codicon codicon-settings" style={{ color: "#CCCCCC", fontSize: "14px" }} />
|
||||
<span style={{ color: "#CCCCCC", fontSize: "12px", fontWeight: 500 }}>Max Requests:</span>
|
||||
<VSCodeTextField
|
||||
style={{ flex: "1", width: "100%", paddingRight: "35px" }}
|
||||
value={autoApprovalSettings.maxRequests.toString()}
|
||||
onInput={(e) => {
|
||||
const input = e.target as HTMLInputElement
|
||||
// Remove any non-numeric characters
|
||||
input.value = input.value.replace(/[^0-9]/g, "")
|
||||
const value = parseInt(input.value)
|
||||
if (!isNaN(value) && value > 0) {
|
||||
updateMaxRequests(value)
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
// Prevent non-numeric keys (except for backspace, delete, arrows)
|
||||
if (
|
||||
!/^\d$/.test(e.key) &&
|
||||
!["Backspace", "Delete", "ArrowLeft", "ArrowRight"].includes(e.key)
|
||||
) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</HeroTooltip>
|
||||
</>
|
||||
)}
|
||||
{isExpanded && (
|
||||
<span
|
||||
className="codicon codicon-chevron-up"
|
||||
style={{
|
||||
paddingBottom: "4px",
|
||||
paddingRight: "3px",
|
||||
marginLeft: "auto",
|
||||
marginTop: "-20px",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => setIsExpanded(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AutoApproveMenu
|
||||
@@ -1,8 +1,7 @@
|
||||
import React, { type ChangeEvent, type ChangeEventHandler } from "react"
|
||||
import React from "react"
|
||||
import styled from "styled-components"
|
||||
import HeroTooltip from "@/components/common/HeroTooltip"
|
||||
import { ActionMetadata } from "./AutoApproveMenu"
|
||||
import { useState } from "react"
|
||||
import { ActionMetadata } from "./types"
|
||||
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
interface AutoApproveMenuItemProps {
|
||||
|
||||
@@ -0,0 +1,333 @@
|
||||
import React, { useRef, useState, useEffect, useMemo, useCallback } from "react"
|
||||
import { useClickAway, useWindowSize } from "react-use"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { VSCodeTextField, VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import { getAsVar, VSC_FOREGROUND, VSC_TITLEBAR_INACTIVE_FOREGROUND } from "@/utils/vscStyles"
|
||||
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
|
||||
import HeroTooltip from "@/components/common/HeroTooltip"
|
||||
import AutoApproveMenuItem from "./AutoApproveMenuItem"
|
||||
import { ActionMetadata } from "./types"
|
||||
|
||||
const breakpoint = 500
|
||||
|
||||
interface AutoApproveModalProps {
|
||||
isVisible: boolean
|
||||
setIsVisible: (visible: boolean) => void
|
||||
buttonRef: React.RefObject<HTMLDivElement>
|
||||
ACTION_METADATA: ActionMetadata[]
|
||||
NOTIFICATIONS_SETTING: ActionMetadata
|
||||
}
|
||||
|
||||
const AutoApproveModal: React.FC<AutoApproveModalProps> = ({
|
||||
isVisible,
|
||||
setIsVisible,
|
||||
buttonRef,
|
||||
ACTION_METADATA,
|
||||
NOTIFICATIONS_SETTING,
|
||||
}) => {
|
||||
const { autoApprovalSettings } = useExtensionState()
|
||||
const modalRef = useRef<HTMLDivElement>(null)
|
||||
const itemsContainerRef = useRef<HTMLDivElement>(null)
|
||||
const { width: viewportWidth, height: viewportHeight } = useWindowSize()
|
||||
const [arrowPosition, setArrowPosition] = useState(0)
|
||||
const [menuPosition, setMenuPosition] = useState(0)
|
||||
const [containerWidth, setContainerWidth] = useState(0)
|
||||
|
||||
// Favorites are derived from autoApprovalSettings
|
||||
const favorites = useMemo(() => autoApprovalSettings.favorites || [], [autoApprovalSettings.favorites])
|
||||
|
||||
useClickAway(modalRef, (e) => {
|
||||
// Skip if click was on the button that toggles the modal
|
||||
if (buttonRef.current && buttonRef.current.contains(e.target as Node)) {
|
||||
return
|
||||
}
|
||||
setIsVisible(false)
|
||||
})
|
||||
|
||||
// Calculate positions for modal and arrow
|
||||
useEffect(() => {
|
||||
if (isVisible && buttonRef.current) {
|
||||
const buttonRect = buttonRef.current.getBoundingClientRect()
|
||||
const buttonCenter = buttonRect.left + buttonRect.width / 2
|
||||
const rightPosition = document.documentElement.clientWidth - buttonCenter - 5
|
||||
|
||||
setArrowPosition(rightPosition)
|
||||
setMenuPosition(buttonRect.top + 1)
|
||||
}
|
||||
}, [isVisible, viewportWidth, viewportHeight, buttonRef])
|
||||
|
||||
// Track container width for responsive layout
|
||||
useEffect(() => {
|
||||
if (!isVisible) return
|
||||
|
||||
const updateWidth = () => {
|
||||
if (itemsContainerRef.current) {
|
||||
setContainerWidth(itemsContainerRef.current.offsetWidth)
|
||||
}
|
||||
}
|
||||
|
||||
// Initial measurement
|
||||
updateWidth()
|
||||
|
||||
// Set up resize observer
|
||||
const resizeObserver = new ResizeObserver(updateWidth)
|
||||
if (itemsContainerRef.current) {
|
||||
resizeObserver.observe(itemsContainerRef.current)
|
||||
}
|
||||
|
||||
// Clean up
|
||||
return () => {
|
||||
resizeObserver.disconnect()
|
||||
}
|
||||
}, [isVisible])
|
||||
|
||||
const toggleFavorite = useCallback(
|
||||
(actionId: string) => {
|
||||
const currentFavorites = autoApprovalSettings.favorites || []
|
||||
let newFavorites: string[]
|
||||
|
||||
if (currentFavorites.includes(actionId)) {
|
||||
newFavorites = currentFavorites.filter((id) => id !== actionId)
|
||||
} else {
|
||||
newFavorites = [...currentFavorites, actionId]
|
||||
}
|
||||
|
||||
vscode.postMessage({
|
||||
type: "autoApprovalSettings",
|
||||
autoApprovalSettings: {
|
||||
...autoApprovalSettings,
|
||||
version: (autoApprovalSettings.version ?? 1) + 1,
|
||||
favorites: newFavorites,
|
||||
},
|
||||
})
|
||||
},
|
||||
[autoApprovalSettings],
|
||||
)
|
||||
|
||||
const updateAction = useCallback(
|
||||
(action: ActionMetadata, value: boolean) => {
|
||||
const actionId = action.id
|
||||
const subActionId = action.subAction?.id
|
||||
|
||||
if (actionId === "enableAll" || subActionId === "enableAll") {
|
||||
toggleAll(action, value)
|
||||
return
|
||||
}
|
||||
|
||||
if (actionId === "enableNotifications" || subActionId === "enableNotifications") {
|
||||
updateNotifications(action, value)
|
||||
return
|
||||
}
|
||||
|
||||
let newActions = {
|
||||
...autoApprovalSettings.actions,
|
||||
[actionId]: value,
|
||||
}
|
||||
|
||||
if (value === false && subActionId) {
|
||||
newActions[subActionId] = false
|
||||
}
|
||||
|
||||
if (value === true && action.parentActionId) {
|
||||
newActions[action.parentActionId as keyof AutoApprovalSettings["actions"]] = true
|
||||
}
|
||||
|
||||
// Check if this will result in any enabled actions
|
||||
const willHaveEnabledActions = Object.values(newActions).some(Boolean)
|
||||
|
||||
vscode.postMessage({
|
||||
type: "autoApprovalSettings",
|
||||
autoApprovalSettings: {
|
||||
...autoApprovalSettings,
|
||||
version: (autoApprovalSettings.version ?? 1) + 1,
|
||||
actions: newActions,
|
||||
enabled: willHaveEnabledActions,
|
||||
},
|
||||
})
|
||||
},
|
||||
[autoApprovalSettings],
|
||||
)
|
||||
|
||||
const updateMaxRequests = useCallback(
|
||||
(maxRequests: number) => {
|
||||
const currentSettings = autoApprovalSettings
|
||||
vscode.postMessage({
|
||||
type: "autoApprovalSettings",
|
||||
autoApprovalSettings: {
|
||||
...currentSettings,
|
||||
version: (currentSettings.version ?? 1) + 1,
|
||||
maxRequests,
|
||||
},
|
||||
})
|
||||
},
|
||||
[autoApprovalSettings],
|
||||
)
|
||||
|
||||
const updateNotifications = useCallback(
|
||||
(action: ActionMetadata, checked: boolean) => {
|
||||
if (action.id === "enableNotifications") {
|
||||
const currentSettings = autoApprovalSettings
|
||||
vscode.postMessage({
|
||||
type: "autoApprovalSettings",
|
||||
autoApprovalSettings: {
|
||||
...currentSettings,
|
||||
version: (currentSettings.version ?? 1) + 1,
|
||||
enableNotifications: checked,
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
[autoApprovalSettings],
|
||||
)
|
||||
|
||||
const toggleAll = useCallback(
|
||||
(action: ActionMetadata, checked: boolean) => {
|
||||
let actions = { ...autoApprovalSettings.actions }
|
||||
|
||||
for (const action of Object.keys(actions)) {
|
||||
actions[action as keyof AutoApprovalSettings["actions"]] = checked
|
||||
}
|
||||
|
||||
vscode.postMessage({
|
||||
type: "autoApprovalSettings",
|
||||
autoApprovalSettings: {
|
||||
...autoApprovalSettings,
|
||||
version: (autoApprovalSettings.version ?? 1) + 1,
|
||||
actions,
|
||||
},
|
||||
})
|
||||
},
|
||||
[autoApprovalSettings],
|
||||
)
|
||||
|
||||
// Check if action is enabled
|
||||
const isChecked = (action: ActionMetadata): boolean => {
|
||||
if (action.id === "enableNotifications") {
|
||||
return autoApprovalSettings.enableNotifications
|
||||
}
|
||||
if (action.id === "enableAll") {
|
||||
return Object.values(autoApprovalSettings.actions).every(Boolean)
|
||||
}
|
||||
return autoApprovalSettings.actions[action.id] ?? false
|
||||
}
|
||||
|
||||
// Check if action is favorited
|
||||
const isFavorited = (action: ActionMetadata): boolean => {
|
||||
return favorites.includes(action.id)
|
||||
}
|
||||
|
||||
if (!isVisible) return null
|
||||
|
||||
return (
|
||||
<div ref={modalRef}>
|
||||
<div
|
||||
className="fixed left-[15px] right-[15px] border border-[var(--vscode-editorGroup-border)] p-3 rounded z-[1000] overflow-y-auto"
|
||||
style={{
|
||||
bottom: `calc(100vh - ${menuPosition}px + 6px)`,
|
||||
background: CODE_BLOCK_BG_COLOR,
|
||||
maxHeight: "calc(100vh - 100px)",
|
||||
overscrollBehavior: "contain",
|
||||
}}>
|
||||
<div
|
||||
className="fixed w-[10px] h-[10px] z-[-1] rotate-45 border-r border-b border-[var(--vscode-editorGroup-border)]"
|
||||
style={{
|
||||
bottom: `calc(100vh - ${menuPosition}px)`,
|
||||
right: arrowPosition,
|
||||
background: CODE_BLOCK_BG_COLOR,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<div className="m-0 text-base font-semibold">Auto-approve Settings</div>
|
||||
<VSCodeButton appearance="icon" onClick={() => setIsVisible(false)}>
|
||||
<span className="codicon codicon-close text-[10px]"></span>
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
|
||||
<HeroTooltip
|
||||
content="Auto-approve allows Cline to perform the following actions without asking for permission. Please use with caution and only enable if you understand the risks."
|
||||
placement="top">
|
||||
<div className="mb-3">
|
||||
<span className="text-[color:var(--vscode-foreground)] font-medium">Actions:</span>
|
||||
</div>
|
||||
</HeroTooltip>
|
||||
|
||||
<div
|
||||
ref={itemsContainerRef}
|
||||
className="relative mb-6"
|
||||
style={{
|
||||
columnCount: containerWidth > breakpoint ? 2 : 1,
|
||||
columnGap: "4px",
|
||||
}}>
|
||||
{/* Vertical separator line - only visible in two-column mode */}
|
||||
{containerWidth > breakpoint && (
|
||||
<div
|
||||
className="absolute left-1/2 top-0 bottom-0 w-[0.5px] opacity-20"
|
||||
style={{
|
||||
background: getAsVar(VSC_TITLEBAR_INACTIVE_FOREGROUND),
|
||||
transform: "translateX(-50%)", // Center the line
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* All items in a single list - CSS Grid will handle the column distribution */}
|
||||
{ACTION_METADATA.map((action) => (
|
||||
<AutoApproveMenuItem
|
||||
key={action.id}
|
||||
action={action}
|
||||
isChecked={isChecked}
|
||||
isFavorited={isFavorited}
|
||||
onToggle={updateAction}
|
||||
onToggleFavorite={toggleFavorite}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<span className="text-[color:var(--vscode-foreground)] font-medium">Quick Settings:</span>
|
||||
</div>
|
||||
|
||||
<AutoApproveMenuItem
|
||||
key={NOTIFICATIONS_SETTING.id}
|
||||
action={NOTIFICATIONS_SETTING}
|
||||
isChecked={isChecked}
|
||||
isFavorited={isFavorited}
|
||||
onToggle={updateAction}
|
||||
onToggleFavorite={toggleFavorite}
|
||||
/>
|
||||
|
||||
<HeroTooltip
|
||||
content="Cline will automatically make this many API requests before asking for approval to proceed with the task."
|
||||
placement="top">
|
||||
<div className="flex items-center pl-1.5 my-2">
|
||||
<span className="codicon codicon-settings text-[#CCCCCC] text-[14px]" />
|
||||
<span className="text-[#CCCCCC] text-xs font-medium ml-2">Max Requests:</span>
|
||||
<VSCodeTextField
|
||||
className="flex-1 w-full pr-[35px] ml-4"
|
||||
value={autoApprovalSettings.maxRequests.toString()}
|
||||
onInput={(e) => {
|
||||
const input = e.target as HTMLInputElement
|
||||
// Remove any non-numeric characters
|
||||
input.value = input.value.replace(/[^0-9]/g, "")
|
||||
const value = parseInt(input.value)
|
||||
if (!isNaN(value) && value > 0) {
|
||||
updateMaxRequests(value)
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
// Prevent non-numeric keys (except for backspace, delete, arrows)
|
||||
if (!/^\d$/.test(e.key) && !["Backspace", "Delete", "ArrowLeft", "ArrowRight"].includes(e.key)) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</HeroTooltip>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AutoApproveModal
|
||||
@@ -0,0 +1,79 @@
|
||||
import { ActionMetadata } from "./types"
|
||||
|
||||
export const ACTION_METADATA: ActionMetadata[] = [
|
||||
{
|
||||
id: "enableAll",
|
||||
label: "Enable all",
|
||||
shortName: "All",
|
||||
description: "Enable all actions.",
|
||||
icon: "codicon-checklist",
|
||||
},
|
||||
{
|
||||
id: "readFiles",
|
||||
label: "Read project files",
|
||||
shortName: "Read",
|
||||
description: "Allows Cline to read files within your workspace.",
|
||||
icon: "codicon-search",
|
||||
subAction: {
|
||||
id: "readFilesExternally",
|
||||
label: "Read all files",
|
||||
shortName: "Read (all)",
|
||||
description: "Allows Cline to read any file on your computer.",
|
||||
icon: "codicon-folder-opened",
|
||||
parentActionId: "readFiles",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "editFiles",
|
||||
label: "Edit project files",
|
||||
shortName: "Edit",
|
||||
description: "Allows Cline to modify files within your workspace.",
|
||||
icon: "codicon-edit",
|
||||
subAction: {
|
||||
id: "editFilesExternally",
|
||||
label: "Edit all files",
|
||||
shortName: "Edit (all)",
|
||||
description: "Allows Cline to modify any file on your computer.",
|
||||
icon: "codicon-files",
|
||||
parentActionId: "editFiles",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "executeSafeCommands",
|
||||
label: "Execute safe commands",
|
||||
shortName: "Safe Commands",
|
||||
description:
|
||||
"Allows Cline to execute safe terminal commands. If the model determines a command is potentially destructive, it will still require approval.",
|
||||
icon: "codicon-terminal",
|
||||
subAction: {
|
||||
id: "executeAllCommands",
|
||||
label: "Execute all commands",
|
||||
shortName: "All Commands",
|
||||
description: "Allows Cline to execute all terminal commands. Use at your own risk.",
|
||||
icon: "codicon-terminal-bash",
|
||||
parentActionId: "executeSafeCommands",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "useBrowser",
|
||||
label: "Use the browser",
|
||||
shortName: "Browser",
|
||||
description: "Allows Cline to launch and interact with any website in a browser.",
|
||||
icon: "codicon-globe",
|
||||
},
|
||||
{
|
||||
id: "useMcp",
|
||||
label: "Use MCP servers",
|
||||
shortName: "MCP",
|
||||
description: "Allows Cline to use configured MCP servers which may modify filesystem or interact with APIs.",
|
||||
icon: "codicon-server",
|
||||
},
|
||||
]
|
||||
|
||||
export const NOTIFICATIONS_SETTING: ActionMetadata = {
|
||||
id: "enableNotifications",
|
||||
label: "Enable notifications",
|
||||
shortName: "Notifications",
|
||||
description: "Receive system notifications when Cline requires approval to proceed or when a task is completed.",
|
||||
icon: "codicon-bell",
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
|
||||
|
||||
export interface ActionMetadata {
|
||||
id: keyof AutoApprovalSettings["actions"] | "enableNotifications" | "enableAll"
|
||||
label: string
|
||||
shortName: string
|
||||
description: string
|
||||
icon: string
|
||||
subAction?: ActionMetadata
|
||||
sub?: boolean
|
||||
parentActionId?: string
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { vscode } from "@/utils/vscode"
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import RulesToggleList from "./RulesToggleList"
|
||||
import Tooltip from "@/components/common/Tooltip"
|
||||
import styled from "styled-components"
|
||||
|
||||
const ClineRulesToggleModal: React.FC = () => {
|
||||
const {
|
||||
@@ -13,6 +14,7 @@ const ClineRulesToggleModal: React.FC = () => {
|
||||
localClineRulesToggles = {},
|
||||
localCursorRulesToggles = {},
|
||||
localWindsurfRulesToggles = {},
|
||||
workflowToggles = {},
|
||||
} = useExtensionState()
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
const buttonRef = useRef<HTMLDivElement>(null)
|
||||
@@ -20,6 +22,7 @@ const ClineRulesToggleModal: React.FC = () => {
|
||||
const { width: viewportWidth, height: viewportHeight } = useWindowSize()
|
||||
const [arrowPosition, setArrowPosition] = useState(0)
|
||||
const [menuPosition, setMenuPosition] = useState(0)
|
||||
const [currentView, setCurrentView] = useState<"rules" | "workflows">("rules")
|
||||
|
||||
useEffect(() => {
|
||||
if (isVisible) {
|
||||
@@ -45,6 +48,10 @@ const ClineRulesToggleModal: React.FC = () => {
|
||||
.map(([path, enabled]): [string, boolean] => [path, enabled as boolean])
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
|
||||
const workflows = Object.entries(workflowToggles || {})
|
||||
.map(([path, enabled]): [string, boolean] => [path, enabled as boolean])
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
|
||||
// Handle toggle rule
|
||||
const toggleRule = (isGlobal: boolean, rulePath: string, enabled: boolean) => {
|
||||
vscode.postMessage({
|
||||
@@ -71,6 +78,14 @@ const ClineRulesToggleModal: React.FC = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const toggleWorkflow = (workflowPath: string, enabled: boolean) => {
|
||||
vscode.postMessage({
|
||||
type: "toggleWorkflow",
|
||||
workflowPath,
|
||||
enabled,
|
||||
})
|
||||
}
|
||||
|
||||
// Close modal when clicking outside
|
||||
useClickAway(modalRef, () => {
|
||||
setIsVisible(false)
|
||||
@@ -91,7 +106,7 @@ const ClineRulesToggleModal: React.FC = () => {
|
||||
return (
|
||||
<div ref={modalRef}>
|
||||
<div ref={buttonRef} className="inline-flex min-w-0 max-w-full">
|
||||
<Tooltip tipText="Manage Cline Rules" visible={isVisible ? false : undefined}>
|
||||
<Tooltip tipText="Manage Cline Rules & Workflows" visible={isVisible ? false : undefined}>
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
aria-label="Cline Rules"
|
||||
@@ -125,68 +140,146 @@ const ClineRulesToggleModal: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex justify-between items-center mb-2.5">
|
||||
<div className="m-0 text-base font-semibold">Cline Rules</div>
|
||||
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
onClick={() => {
|
||||
vscode.postMessage({
|
||||
type: "openExtensionSettings",
|
||||
})
|
||||
setIsVisible(false)
|
||||
}}></VSCodeButton>
|
||||
{/* Tabs container */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: "10px",
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "1px",
|
||||
borderBottom: "1px solid var(--vscode-panel-border)",
|
||||
}}>
|
||||
<TabButton isActive={currentView === "rules"} onClick={() => setCurrentView("rules")}>
|
||||
Rules
|
||||
</TabButton>
|
||||
<TabButton isActive={currentView === "workflows"} onClick={() => setCurrentView("workflows")}>
|
||||
Workflows
|
||||
</TabButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Global Rules Section */}
|
||||
<div className="mb-3">
|
||||
<div className="text-sm font-normal mb-2">Global Rules</div>
|
||||
<RulesToggleList
|
||||
rules={globalRules}
|
||||
toggleRule={(rulePath, enabled) => toggleRule(true, rulePath, enabled)}
|
||||
listGap="small"
|
||||
isGlobal={true}
|
||||
ruleType={"cline"}
|
||||
showNewRule={true}
|
||||
showNoRules={true}
|
||||
/>
|
||||
{/* Description text */}
|
||||
<div className="text-xs text-[var(--vscode-descriptionForeground)] mb-4">
|
||||
{currentView === "rules" ? (
|
||||
<p>
|
||||
Rules allow you to provide Cline with system-level guidance. Think of them as a persistent way to
|
||||
include context and preferences for your projects or globally for every conversation.
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
Workflows allow you to define a series of steps to guide Cline through a repetitive set of tasks,
|
||||
such as deploying a service or submitting a PR. To invoke a workflow, type{" "}
|
||||
<span
|
||||
className="
|
||||
text-[var(--vscode-foreground)] font-bold">
|
||||
/workflow-name
|
||||
</span>{" "}
|
||||
in the chat.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Local Rules Section */}
|
||||
<div style={{ marginBottom: -10 }}>
|
||||
<div className="text-sm font-normal mb-2">Workspace Rules</div>
|
||||
<RulesToggleList
|
||||
rules={localRules}
|
||||
toggleRule={(rulePath, enabled) => toggleRule(false, rulePath, enabled)}
|
||||
listGap="small"
|
||||
isGlobal={false}
|
||||
ruleType={"cline"}
|
||||
showNewRule={false}
|
||||
showNoRules={false}
|
||||
/>
|
||||
<RulesToggleList
|
||||
rules={cursorRules}
|
||||
toggleRule={toggleCursorRule}
|
||||
listGap="small"
|
||||
isGlobal={false}
|
||||
ruleType={"cursor"}
|
||||
showNewRule={false}
|
||||
showNoRules={false}
|
||||
/>
|
||||
<RulesToggleList
|
||||
rules={windsurfRules}
|
||||
toggleRule={toggleWindsurfRule}
|
||||
listGap="small"
|
||||
isGlobal={false}
|
||||
ruleType={"windsurf"}
|
||||
showNewRule={true}
|
||||
showNoRules={localRules.length === 0 && cursorRules.length === 0 && windsurfRules.length === 0}
|
||||
/>
|
||||
</div>
|
||||
{currentView === "rules" ? (
|
||||
<>
|
||||
{/* Global Rules Section */}
|
||||
<div className="mb-3">
|
||||
<div className="text-sm font-normal mb-2">Global Rules</div>
|
||||
<RulesToggleList
|
||||
rules={globalRules}
|
||||
toggleRule={(rulePath, enabled) => toggleRule(true, rulePath, enabled)}
|
||||
listGap="small"
|
||||
isGlobal={true}
|
||||
ruleType={"cline"}
|
||||
showNewRule={true}
|
||||
showNoRules={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Local Rules Section */}
|
||||
<div style={{ marginBottom: -10 }}>
|
||||
<div className="text-sm font-normal mb-2">Workspace Rules</div>
|
||||
<RulesToggleList
|
||||
rules={localRules}
|
||||
toggleRule={(rulePath, enabled) => toggleRule(false, rulePath, enabled)}
|
||||
listGap="small"
|
||||
isGlobal={false}
|
||||
ruleType={"cline"}
|
||||
showNewRule={false}
|
||||
showNoRules={false}
|
||||
/>
|
||||
<RulesToggleList
|
||||
rules={cursorRules}
|
||||
toggleRule={toggleCursorRule}
|
||||
listGap="small"
|
||||
isGlobal={false}
|
||||
ruleType={"cursor"}
|
||||
showNewRule={false}
|
||||
showNoRules={false}
|
||||
/>
|
||||
<RulesToggleList
|
||||
rules={windsurfRules}
|
||||
toggleRule={toggleWindsurfRule}
|
||||
listGap="small"
|
||||
isGlobal={false}
|
||||
ruleType={"windsurf"}
|
||||
showNewRule={true}
|
||||
showNoRules={false}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
/* Workflows section */
|
||||
<div style={{ marginBottom: -10 }}>
|
||||
<div className="text-sm font-normal mb-2">Workspace Workflows</div>
|
||||
<RulesToggleList
|
||||
rules={workflows}
|
||||
toggleRule={toggleWorkflow}
|
||||
listGap="small"
|
||||
isGlobal={false}
|
||||
ruleType={"workflow"}
|
||||
showNewRule={true}
|
||||
showNoRules={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledTabButton = styled.button<{ isActive: boolean }>`
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 2px solid ${(props) => (props.isActive ? "var(--vscode-foreground)" : "transparent")};
|
||||
color: ${(props) => (props.isActive ? "var(--vscode-foreground)" : "var(--vscode-descriptionForeground)")};
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
margin-bottom: -1px;
|
||||
font-family: inherit;
|
||||
|
||||
&:hover {
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
`
|
||||
|
||||
export const TabButton = ({
|
||||
children,
|
||||
isActive,
|
||||
onClick,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
isActive: boolean
|
||||
onClick: () => void
|
||||
}) => (
|
||||
<StyledTabButton isActive={isActive} onClick={onClick}>
|
||||
{children}
|
||||
</StyledTabButton>
|
||||
)
|
||||
|
||||
export default ClineRulesToggleModal
|
||||
|
||||
@@ -7,9 +7,10 @@ import { CreateRuleFileRequest } from "@shared/proto-conversions/file/rule-files
|
||||
|
||||
interface NewRuleRowProps {
|
||||
isGlobal: boolean
|
||||
ruleType?: string
|
||||
}
|
||||
|
||||
const NewRuleRow: React.FC<NewRuleRowProps> = ({ isGlobal }) => {
|
||||
const NewRuleRow: React.FC<NewRuleRowProps> = ({ isGlobal, ruleType }) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const [filename, setFilename] = useState("")
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
@@ -64,6 +65,7 @@ const NewRuleRow: React.FC<NewRuleRowProps> = ({ isGlobal }) => {
|
||||
CreateRuleFileRequest.create({
|
||||
isGlobal,
|
||||
filename: finalFilename,
|
||||
type: ruleType || "cline",
|
||||
}),
|
||||
)
|
||||
} catch (err) {
|
||||
@@ -97,7 +99,11 @@ const NewRuleRow: React.FC<NewRuleRowProps> = ({ isGlobal }) => {
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
placeholder="rule-name (.md, .txt, or no extension)"
|
||||
placeholder={
|
||||
ruleType === "workflow"
|
||||
? "workflow-name (.md, .txt, or no extension)"
|
||||
: "rule-name (.md, .txt, or no extension)"
|
||||
}
|
||||
value={filename}
|
||||
onChange={(e) => setFilename(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
@@ -121,7 +127,7 @@ const NewRuleRow: React.FC<NewRuleRowProps> = ({ isGlobal }) => {
|
||||
) : (
|
||||
<>
|
||||
<span className="flex-1 text-[var(--vscode-descriptionForeground)] bg-[var(--vscode-input-background)] italic text-xs">
|
||||
New rule file...
|
||||
{ruleType === "workflow" ? "New workflow file..." : "New rule file..."}
|
||||
</span>
|
||||
<div className="flex items-center ml-2 space-x-2">
|
||||
<VSCodeButton
|
||||
|
||||
@@ -62,6 +62,7 @@ const RuleRow: React.FC<{
|
||||
DeleteRuleFileRequest.create({
|
||||
rulePath: rulePath,
|
||||
isGlobal: isGlobal,
|
||||
type: ruleType || "cline",
|
||||
}),
|
||||
).catch((err) => console.error("Failed to delete rule file:", err))
|
||||
}
|
||||
|
||||
@@ -40,16 +40,16 @@ const RulesToggleList = ({
|
||||
ruleType={ruleType}
|
||||
/>
|
||||
))}
|
||||
{showNewRule && <NewRuleRow isGlobal={isGlobal} />}
|
||||
{showNewRule && <NewRuleRow isGlobal={isGlobal} ruleType={ruleType} />}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{showNoRules && (
|
||||
<div className="flex flex-col items-center gap-3 my-3 text-[var(--vscode-descriptionForeground)]">
|
||||
No rules found
|
||||
{ruleType === "workflow" ? "No workflows found" : "No rules found"}
|
||||
</div>
|
||||
)}
|
||||
{showNewRule && <NewRuleRow isGlobal={isGlobal} />}
|
||||
{showNewRule && <NewRuleRow isGlobal={isGlobal} ruleType={ruleType} />}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { memo } from "react"
|
||||
import { memo, useState } from "react"
|
||||
import { TaskServiceClient } from "@/services/grpc-client"
|
||||
import { formatLargeNumber } from "@/utils/format"
|
||||
|
||||
@@ -11,10 +11,16 @@ type HistoryPreviewProps = {
|
||||
|
||||
const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
|
||||
const { taskHistory } = useExtensionState()
|
||||
const [isExpanded, setIsExpanded] = useState(true)
|
||||
|
||||
const handleHistorySelect = (id: string) => {
|
||||
TaskServiceClient.showTaskWithId({ value: id }).catch((error) => console.error("Error showing task:", error))
|
||||
}
|
||||
|
||||
const toggleExpanded = () => {
|
||||
setIsExpanded(!isExpanded)
|
||||
}
|
||||
|
||||
const formatDate = (timestamp: number) => {
|
||||
const date = new Date(timestamp)
|
||||
return date
|
||||
@@ -48,16 +54,31 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.history-header {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.history-header:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
<div
|
||||
className="history-header"
|
||||
onClick={toggleExpanded}
|
||||
style={{
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
margin: "10px 20px 10px 20px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}>
|
||||
<span
|
||||
className={`codicon codicon-chevron-${isExpanded ? "down" : "right"}`}
|
||||
style={{
|
||||
marginRight: "4px",
|
||||
transform: "scale(0.9)",
|
||||
}}></span>
|
||||
<span
|
||||
className="codicon codicon-comment-discussion"
|
||||
style={{
|
||||
@@ -74,102 +95,122 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style={{ padding: "0px 20px 0 20px" }}>
|
||||
{taskHistory
|
||||
.filter((item) => item.ts && item.task)
|
||||
.slice(0, 3)
|
||||
.map((item) => (
|
||||
<div key={item.id} className="history-preview-item" onClick={() => handleHistorySelect(item.id)}>
|
||||
<div style={{ padding: "12px" }}>
|
||||
<div style={{ marginBottom: "8px" }}>
|
||||
<span
|
||||
style={{
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
fontWeight: 500,
|
||||
fontSize: "0.85em",
|
||||
textTransform: "uppercase",
|
||||
}}>
|
||||
{formatDate(item.ts)}
|
||||
</span>
|
||||
</div>
|
||||
{item.isFavorited && (
|
||||
{isExpanded && (
|
||||
<div style={{ padding: "0px 20px 0 20px" }}>
|
||||
{taskHistory.filter((item) => item.ts && item.task).length > 0 ? (
|
||||
<>
|
||||
{taskHistory
|
||||
.filter((item) => item.ts && item.task)
|
||||
.slice(0, 3)
|
||||
.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="history-preview-item"
|
||||
onClick={() => handleHistorySelect(item.id)}>
|
||||
<div style={{ padding: "12px" }}>
|
||||
<div style={{ marginBottom: "8px" }}>
|
||||
<span
|
||||
style={{
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
fontWeight: 500,
|
||||
fontSize: "0.85em",
|
||||
textTransform: "uppercase",
|
||||
}}>
|
||||
{formatDate(item.ts)}
|
||||
</span>
|
||||
</div>
|
||||
{item.isFavorited && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "12px",
|
||||
right: "12px",
|
||||
color: "var(--vscode-button-background)",
|
||||
}}>
|
||||
<span className="codicon codicon-star-full" aria-label="Favorited" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
id={`history-preview-task-${item.id}`}
|
||||
className="history-preview-task"
|
||||
style={{
|
||||
fontSize: "var(--vscode-font-size)",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
marginBottom: "8px",
|
||||
display: "-webkit-box",
|
||||
WebkitLineClamp: 3,
|
||||
WebkitBoxOrient: "vertical",
|
||||
overflow: "hidden",
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-word",
|
||||
overflowWrap: "anywhere",
|
||||
}}>
|
||||
<span className="ph-no-capture">{item.task}</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "0.85em",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
<span>
|
||||
Tokens: ↑{formatLargeNumber(item.tokensIn || 0)} ↓
|
||||
{formatLargeNumber(item.tokensOut || 0)}
|
||||
</span>
|
||||
{!!item.cacheWrites && (
|
||||
<>
|
||||
{" • "}
|
||||
<span>
|
||||
Cache: +{formatLargeNumber(item.cacheWrites || 0)} →{" "}
|
||||
{formatLargeNumber(item.cacheReads || 0)}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{!!item.totalCost && (
|
||||
<>
|
||||
{" • "}
|
||||
<span>API Cost: ${item.totalCost?.toFixed(4)}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}>
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
onClick={() => showHistoryView()}
|
||||
style={{
|
||||
opacity: 0.9,
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "12px",
|
||||
right: "12px",
|
||||
color: "var(--vscode-button-background)",
|
||||
fontSize: "var(--vscode-font-size)",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
<span className="codicon codicon-star-full" aria-label="Favorited" />
|
||||
View all history
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
id={`history-preview-task-${item.id}`}
|
||||
className="history-preview-task"
|
||||
style={{
|
||||
fontSize: "var(--vscode-font-size)",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
marginBottom: "8px",
|
||||
display: "-webkit-box",
|
||||
WebkitLineClamp: 3,
|
||||
WebkitBoxOrient: "vertical",
|
||||
overflow: "hidden",
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-word",
|
||||
overflowWrap: "anywhere",
|
||||
}}>
|
||||
<span className="ph-no-capture">{item.task}</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "0.85em",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
<span>
|
||||
Tokens: ↑{formatLargeNumber(item.tokensIn || 0)} ↓{formatLargeNumber(item.tokensOut || 0)}
|
||||
</span>
|
||||
{!!item.cacheWrites && (
|
||||
<>
|
||||
{" • "}
|
||||
<span>
|
||||
Cache: +{formatLargeNumber(item.cacheWrites || 0)} →{" "}
|
||||
{formatLargeNumber(item.cacheReads || 0)}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{!!item.totalCost && (
|
||||
<>
|
||||
{" • "}
|
||||
<span>API Cost: ${item.totalCost?.toFixed(4)}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}>
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
onClick={() => showHistoryView()}
|
||||
style={{
|
||||
opacity: 0.9,
|
||||
}}>
|
||||
</>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
fontSize: "var(--vscode-font-size)",
|
||||
textAlign: "center",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
fontSize: "var(--vscode-font-size)",
|
||||
padding: "10px 0",
|
||||
}}>
|
||||
View all history
|
||||
No recent tasks
|
||||
</div>
|
||||
</VSCodeButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,14 +17,44 @@ type HistoryViewProps = {
|
||||
|
||||
type SortOption = "newest" | "oldest" | "mostExpensive" | "mostTokens" | "mostRelevant"
|
||||
|
||||
// Tailwind-styled radio with custom icon support - works independently of VSCodeRadioGroup but looks the same
|
||||
// Used for workspace and favorites filters
|
||||
|
||||
interface CustomFilterRadioProps {
|
||||
checked: boolean
|
||||
onChange: () => void
|
||||
icon: string
|
||||
label: string
|
||||
}
|
||||
|
||||
const CustomFilterRadio = ({ checked, onChange, icon, label }: CustomFilterRadioProps) => {
|
||||
return (
|
||||
<div
|
||||
onClick={onChange}
|
||||
className="flex items-center cursor-pointer py-[0.3em] px-0 mr-[10px] text-[var(--vscode-font-size)] select-none">
|
||||
<div
|
||||
className={`w-[14px] h-[14px] rounded-full border border-[var(--vscode-checkbox-border)] relative flex justify-center items-center mr-[6px] ${
|
||||
checked ? "bg-[var(--vscode-checkbox-background)]" : "bg-transparent"
|
||||
}`}>
|
||||
{checked && <div className="w-[6px] h-[6px] rounded-full bg-[var(--vscode-checkbox-foreground)]" />}
|
||||
</div>
|
||||
<span className="flex items-center gap-[3px]">
|
||||
<div className={`codicon codicon-${icon} text-[var(--vscode-button-background)] text-base`} />
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const HistoryView = ({ onDone }: HistoryViewProps) => {
|
||||
const { taskHistory, totalTasksSize } = useExtensionState()
|
||||
const { taskHistory, totalTasksSize, filePaths } = useExtensionState()
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [sortOption, setSortOption] = useState<SortOption>("newest")
|
||||
const [lastNonRelevantSort, setLastNonRelevantSort] = useState<SortOption | null>("newest")
|
||||
const [deleteAllDisabled, setDeleteAllDisabled] = useState(false)
|
||||
const [selectedItems, setSelectedItems] = useState<string[]>([])
|
||||
const [showFavoritesOnly, setShowFavoritesOnly] = useState(false)
|
||||
const [showCurrentWorkspaceOnly, setShowCurrentWorkspaceOnly] = useState(false)
|
||||
|
||||
// Keep track of pending favorite toggle operations
|
||||
const [pendingFavoriteToggles, setPendingFavoriteToggles] = useState<Record<string, boolean>>({})
|
||||
@@ -39,24 +69,23 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
||||
favoritesOnly: showFavoritesOnly,
|
||||
searchQuery: searchQuery || undefined,
|
||||
sortBy: sortOption,
|
||||
currentWorkspaceOnly: showCurrentWorkspaceOnly,
|
||||
})
|
||||
setFilteredTasks(response.tasks || [])
|
||||
} catch (error) {
|
||||
console.error("Error loading task history:", error)
|
||||
// Fallback to client-side filtering
|
||||
setFilteredTasks(
|
||||
taskHistory.filter((item) => {
|
||||
const valid = item.ts && item.task
|
||||
return valid && (!showFavoritesOnly || item.isFavorited)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}, [showFavoritesOnly, searchQuery, sortOption, taskHistory])
|
||||
}, [showFavoritesOnly, showCurrentWorkspaceOnly, searchQuery, sortOption, taskHistory])
|
||||
|
||||
// Load when filters change
|
||||
useEffect(() => {
|
||||
// Force a complete refresh when both filters are active
|
||||
// to ensure proper combined filtering
|
||||
if (showFavoritesOnly && showCurrentWorkspaceOnly) {
|
||||
setFilteredTasks([])
|
||||
}
|
||||
loadTaskHistory()
|
||||
}, [loadTaskHistory])
|
||||
}, [loadTaskHistory, showFavoritesOnly, showCurrentWorkspaceOnly])
|
||||
|
||||
const toggleFavorite = useCallback(
|
||||
async (taskId: string, currentValue: boolean) => {
|
||||
@@ -69,8 +98,8 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
||||
isFavorited: !currentValue,
|
||||
})
|
||||
|
||||
// Refresh if favorites filter is active
|
||||
if (showFavoritesOnly) {
|
||||
// Refresh if either filter is active to ensure proper combined filtering
|
||||
if (showFavoritesOnly || showCurrentWorkspaceOnly) {
|
||||
loadTaskHistory()
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -320,46 +349,18 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
||||
<VSCodeRadio value="mostRelevant" disabled={!searchQuery} style={{ opacity: searchQuery ? 1 : 0.5 }}>
|
||||
Most Relevant
|
||||
</VSCodeRadio>
|
||||
<div
|
||||
onClick={() => setShowFavoritesOnly(!showFavoritesOnly)}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
marginLeft: "6px",
|
||||
cursor: "pointer",
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
width: "14px",
|
||||
height: "14px",
|
||||
borderRadius: "50%",
|
||||
border: "1px solid var(--vscode-checkbox-border)",
|
||||
backgroundColor: showFavoritesOnly ? "var(--vscode-checkbox-background)" : "transparent",
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginRight: "6px",
|
||||
}}>
|
||||
{showFavoritesOnly && (
|
||||
<div
|
||||
style={{
|
||||
width: "6px",
|
||||
height: "6px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "var(--vscode-checkbox-foreground)",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<span style={{ display: "flex", alignItems: "center", gap: "6px", userSelect: "none" }}>
|
||||
<div
|
||||
className="codicon codicon-star-full"
|
||||
style={{ color: "var(--vscode-button-background)", fontSize: "14px" }}
|
||||
/>
|
||||
Favorites
|
||||
</span>
|
||||
</div>
|
||||
<CustomFilterRadio
|
||||
checked={showCurrentWorkspaceOnly}
|
||||
onChange={() => setShowCurrentWorkspaceOnly(!showCurrentWorkspaceOnly)}
|
||||
icon="workspace"
|
||||
label="Workspace"
|
||||
/>
|
||||
<CustomFilterRadio
|
||||
checked={showFavoritesOnly}
|
||||
onChange={() => setShowFavoritesOnly(!showFavoritesOnly)}
|
||||
icon="star-full"
|
||||
label="Favorites"
|
||||
/>
|
||||
</VSCodeRadioGroup>
|
||||
|
||||
<div style={{ display: "flex", justifyContent: "flex-end", gap: "10px" }}>
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { VSCodeCheckbox, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { memo } from "react"
|
||||
import { OpenAIReasoningEffort } from "@shared/ChatSettings"
|
||||
|
||||
const FeatureSettingsSection = () => {
|
||||
const {
|
||||
enableCheckpointsSetting,
|
||||
setEnableCheckpointsSetting,
|
||||
mcpMarketplaceEnabled,
|
||||
setMcpMarketplaceEnabled,
|
||||
chatSettings,
|
||||
setChatSettings,
|
||||
} = useExtensionState()
|
||||
|
||||
return (
|
||||
<div 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" }}>Feature Settings</h3>
|
||||
<div>
|
||||
<VSCodeCheckbox
|
||||
checked={enableCheckpointsSetting}
|
||||
onChange={(e: any) => {
|
||||
const checked = e.target.checked === true
|
||||
setEnableCheckpointsSetting(checked)
|
||||
}}>
|
||||
Enable Checkpoints
|
||||
</VSCodeCheckbox>
|
||||
<p className="text-xs text-[var(--vscode-descriptionForeground)]">
|
||||
Enables extension to save checkpoints of workspace throughout the task. Uses git under the hood which may not
|
||||
work well with large workspaces.
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<VSCodeCheckbox
|
||||
checked={mcpMarketplaceEnabled}
|
||||
onChange={(e: any) => {
|
||||
const checked = e.target.checked === true
|
||||
setMcpMarketplaceEnabled(checked)
|
||||
}}>
|
||||
Enable MCP Marketplace
|
||||
</VSCodeCheckbox>
|
||||
<p className="text-xs text-[var(--vscode-descriptionForeground)]">
|
||||
Enables the MCP Marketplace tab for discovering and installing MCP servers.
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<label
|
||||
htmlFor="openai-reasoning-effort-dropdown"
|
||||
className="block text-sm font-medium text-[var(--vscode-foreground)] mb-1">
|
||||
OpenAI Reasoning Effort
|
||||
</label>
|
||||
<VSCodeDropdown
|
||||
id="openai-reasoning-effort-dropdown"
|
||||
currentValue={chatSettings.openAIReasoningEffort || "medium"}
|
||||
onChange={(e: any) => {
|
||||
const newValue = e.target.currentValue as OpenAIReasoningEffort
|
||||
setChatSettings({
|
||||
...chatSettings,
|
||||
openAIReasoningEffort: newValue,
|
||||
})
|
||||
}}
|
||||
className="w-full">
|
||||
<VSCodeOption value="low">Low</VSCodeOption>
|
||||
<VSCodeOption value="medium">Medium</VSCodeOption>
|
||||
<VSCodeOption value="high">High</VSCodeOption>
|
||||
</VSCodeDropdown>
|
||||
<p className="text-xs mt-[5px] text-[var(--vscode-descriptionForeground)]">
|
||||
Reasoning effort for the OpenAI family of models(applies to all OpenAI model providers)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(FeatureSettingsSection)
|
||||
@@ -0,0 +1,53 @@
|
||||
import { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
|
||||
import React from "react"
|
||||
import { ChatSettings } from "@shared/ChatSettings"
|
||||
|
||||
interface PreferredLanguageSettingProps {
|
||||
chatSettings: ChatSettings
|
||||
setChatSettings: (settings: ChatSettings) => void
|
||||
}
|
||||
|
||||
const PreferredLanguageSetting: React.FC<PreferredLanguageSettingProps> = ({ chatSettings, setChatSettings }) => {
|
||||
return (
|
||||
<div style={{ marginTop: 10, marginBottom: 10 }}>
|
||||
<label htmlFor="preferred-language-dropdown" className="block mb-1 text-sm font-medium">
|
||||
Preferred Language
|
||||
</label>
|
||||
<VSCodeDropdown
|
||||
id="preferred-language-dropdown"
|
||||
currentValue={chatSettings.preferredLanguage || "English"}
|
||||
onChange={(e: any) => {
|
||||
const newLanguage = e.target.value
|
||||
setChatSettings({
|
||||
...chatSettings,
|
||||
preferredLanguage: newLanguage,
|
||||
}) // This constructs a full ChatSettings object
|
||||
}}
|
||||
style={{ width: "100%" }}>
|
||||
<VSCodeOption value="English">English</VSCodeOption>
|
||||
<VSCodeOption value="Arabic - العربية">Arabic - العربية</VSCodeOption>
|
||||
<VSCodeOption value="Portuguese - Português (Brasil)">Portuguese - Português (Brasil)</VSCodeOption>
|
||||
<VSCodeOption value="Czech - Čeština">Czech - Čeština</VSCodeOption>
|
||||
<VSCodeOption value="French - Français">French - Français</VSCodeOption>
|
||||
<VSCodeOption value="German - Deutsch">German - Deutsch</VSCodeOption>
|
||||
<VSCodeOption value="Hindi - हिन्दी">Hindi - हिन्दी</VSCodeOption>
|
||||
<VSCodeOption value="Hungarian - Magyar">Hungarian - Magyar</VSCodeOption>
|
||||
<VSCodeOption value="Italian - Italiano">Italian - Italiano</VSCodeOption>
|
||||
<VSCodeOption value="Japanese - 日本語">Japanese - 日本語</VSCodeOption>
|
||||
<VSCodeOption value="Korean - 한국어">Korean - 한국어</VSCodeOption>
|
||||
<VSCodeOption value="Polish - Polski">Polish - Polski</VSCodeOption>
|
||||
<VSCodeOption value="Portuguese - Português (Portugal)">Portuguese - Português (Portugal)</VSCodeOption>
|
||||
<VSCodeOption value="Russian - Русский">Russian - Русский</VSCodeOption>
|
||||
<VSCodeOption value="Simplified Chinese - 简体中文">Simplified Chinese - 简体中文</VSCodeOption>
|
||||
<VSCodeOption value="Spanish - Español">Spanish - Español</VSCodeOption>
|
||||
<VSCodeOption value="Traditional Chinese - 繁體中文">Traditional Chinese - 繁體中文</VSCodeOption>
|
||||
<VSCodeOption value="Turkish - Türkçe">Turkish - Türkçe</VSCodeOption>
|
||||
</VSCodeDropdown>
|
||||
<p className="text-xs text-[var(--vscode-descriptionForeground)] mt-1">
|
||||
The language that Cline should use for communication.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(PreferredLanguageSetting)
|
||||
@@ -1,5 +1,14 @@
|
||||
import { VSCodeButton, VSCodeCheckbox, VSCodeLink, VSCodeTextArea } from "@vscode/webview-ui-toolkit/react"
|
||||
import {
|
||||
VSCodeButton,
|
||||
VSCodeCheckbox,
|
||||
VSCodeDropdown,
|
||||
VSCodeLink,
|
||||
VSCodeOption,
|
||||
VSCodeTextArea,
|
||||
} from "@vscode/webview-ui-toolkit/react"
|
||||
import { memo, useCallback, useEffect, useState } from "react"
|
||||
import PreferredLanguageSetting from "./PreferredLanguageSetting" // Added import
|
||||
import { OpenAIReasoningEffort } from "@shared/ChatSettings"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { validateApiConfiguration, validateModelId } from "@/utils/validate"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
@@ -8,6 +17,7 @@ import ApiOptions from "./ApiOptions"
|
||||
import { TabButton } from "../mcp/configuration/McpConfigurationView"
|
||||
import { useEvent } from "react-use"
|
||||
import { ExtensionMessage } from "@shared/ExtensionMessage"
|
||||
import FeatureSettingsSection from "./FeatureSettingsSection"
|
||||
import BrowserSettingsSection from "./BrowserSettingsSection"
|
||||
import TerminalSettingsSection from "./TerminalSettingsSection"
|
||||
import { FEATURE_FLAGS } from "@shared/services/feature-flags/feature-flags"
|
||||
@@ -27,8 +37,11 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
telemetrySetting,
|
||||
setTelemetrySetting,
|
||||
chatSettings,
|
||||
setChatSettings,
|
||||
planActSeparateModelsSetting,
|
||||
setPlanActSeparateModelsSetting,
|
||||
enableCheckpointsSetting,
|
||||
mcpMarketplaceEnabled,
|
||||
} = useExtensionState()
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
|
||||
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
|
||||
@@ -67,6 +80,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
planActSeparateModelsSetting,
|
||||
customInstructionsSetting: customInstructions,
|
||||
telemetrySetting,
|
||||
enableCheckpointsSetting,
|
||||
mcpMarketplaceEnabled,
|
||||
apiConfiguration: apiConfigurationToSubmit,
|
||||
})
|
||||
|
||||
@@ -198,6 +213,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{chatSettings && <PreferredLanguageSetting chatSettings={chatSettings} setChatSettings={setChatSettings} />}
|
||||
|
||||
<div className="mb-[5px]">
|
||||
<VSCodeCheckbox
|
||||
className="mb-[5px]"
|
||||
@@ -238,21 +255,15 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Feature Settings Section */}
|
||||
<FeatureSettingsSection />
|
||||
|
||||
{/* Browser Settings Section */}
|
||||
<BrowserSettingsSection />
|
||||
|
||||
{/* Terminal Settings Section */}
|
||||
<TerminalSettingsSection />
|
||||
|
||||
<div className="mt-auto pr-2 flex justify-center">
|
||||
<SettingsButton
|
||||
onClick={() => vscode.postMessage({ type: "openExtensionSettings" })}
|
||||
className="mt-0 mr-0 mb-4 ml-0">
|
||||
<i className="codicon codicon-settings-gear" />
|
||||
Advanced Settings
|
||||
</SettingsButton>
|
||||
</div>
|
||||
|
||||
{IS_DEV && (
|
||||
<>
|
||||
<div className="mt-[10px] mb-1">Debug</div>
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import React, { useState, useEffect, useRef } from "react"
|
||||
import { TaskServiceClient } from "@/services/grpc-client"
|
||||
|
||||
interface Task {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
prompt: string
|
||||
}
|
||||
|
||||
const tasks: Task[] = [
|
||||
{
|
||||
id: "web-app",
|
||||
title: "Build a Web App",
|
||||
description: "Create a modern React app with Vite and Tailwind",
|
||||
prompt: "Create a landing page for an app where LLMs can swipe on each other. Make it in React with Vite and tailwind, and then test it using the browser tool.",
|
||||
},
|
||||
{
|
||||
id: "cli-tool",
|
||||
title: "Create a CLI Tool",
|
||||
description: "Build a Node.js CLI for markdown analysis",
|
||||
prompt: "Create a Node.js CLI tool that can analyze a directory of markdown files and generate a summary of their contents, including word count, reading time, and most common topics. Include a progress bar for processing files.",
|
||||
},
|
||||
{
|
||||
id: "file-automation",
|
||||
title: "Automate",
|
||||
description: "Extract and organize TODO comments",
|
||||
prompt: "Help me organize my project's documentation by creating a script that finds all TODO comments in the codebase, extracts them into a structured markdown file, and sorts them by priority based on comment content.",
|
||||
},
|
||||
]
|
||||
|
||||
export const SuggestedTasks: React.FC = () => {
|
||||
const [currentIndex, setCurrentIndex] = useState(0)
|
||||
const [isPaused, setIsPaused] = useState(false)
|
||||
const [direction, setDirection] = useState<"up" | "down">("down") // Track animation direction
|
||||
const pauseTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const [isUpHovered, setIsUpHovered] = useState(false)
|
||||
const [isDownHovered, setIsDownHovered] = useState(false)
|
||||
|
||||
// Handle task selection
|
||||
const handleTaskClick = async (prompt: string) => {
|
||||
await TaskServiceClient.newTask({ text: prompt, images: [] })
|
||||
}
|
||||
|
||||
// Function to handle arrow clicks and navigation
|
||||
const handleNavigation = (direction: "prev" | "next") => {
|
||||
// Pause auto-scrolling for 5 seconds
|
||||
setIsPaused(true)
|
||||
if (pauseTimeoutRef.current) {
|
||||
clearTimeout(pauseTimeoutRef.current)
|
||||
}
|
||||
pauseTimeoutRef.current = setTimeout(() => {
|
||||
setIsPaused(false)
|
||||
}, 2000)
|
||||
|
||||
// Set the animation direction
|
||||
setDirection(direction === "prev" ? "up" : "down")
|
||||
|
||||
// Update the current index
|
||||
if (direction === "next") {
|
||||
setCurrentIndex((prevIndex) => (prevIndex + 1) % tasks.length)
|
||||
} else {
|
||||
setCurrentIndex((prevIndex) => (prevIndex - 1 + tasks.length) % tasks.length)
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-advance to next task (unless paused)
|
||||
useEffect(() => {
|
||||
if (isPaused) return
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setDirection("down") // Default auto-advance direction is down
|
||||
setCurrentIndex((prevIndex) => (prevIndex + 1) % tasks.length)
|
||||
}, 3000) // Change task every 3 seconds
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [isPaused])
|
||||
|
||||
// Clean up pause timeout on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (pauseTimeoutRef.current) {
|
||||
clearTimeout(pauseTimeoutRef.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const currentTask = tasks[currentIndex]
|
||||
|
||||
return (
|
||||
<div className="px-6 py-2 select-none">
|
||||
{/* Container with fixed height to prevent layout shift */}
|
||||
<div className="relative h-[80px] sm:h-[100px] mb-1 overflow-hidden">
|
||||
{/* Fixed navigation arrows (outside of cards) */}
|
||||
<div className="absolute left-2 top-0 bottom-0 flex flex-col justify-center items-center gap-1 z-20">
|
||||
{/* Up arrow */}
|
||||
<div
|
||||
className="flex items-center justify-center w-5 h-5 rounded-full cursor-pointer transition-colors select-none"
|
||||
style={{
|
||||
backgroundColor: isUpHovered
|
||||
? "var(--vscode-list-hoverBackground, rgba(90, 93, 94, 0.31))"
|
||||
: "var(--vscode-editorWidget-background, rgba(60, 60, 60, 0.4))",
|
||||
}}
|
||||
onClick={() => handleNavigation("prev")}
|
||||
onMouseEnter={() => setIsUpHovered(true)}
|
||||
onMouseLeave={() => setIsUpHovered(false)}>
|
||||
<span
|
||||
className="codicon codicon-chevron-up"
|
||||
style={{
|
||||
fontSize: "14px",
|
||||
color: "var(--vscode-foreground, rgba(255, 255, 255, 0.9))",
|
||||
}}></span>
|
||||
</div>
|
||||
|
||||
{/* Down arrow */}
|
||||
<div
|
||||
className="flex items-center justify-center w-5 h-5 rounded-full cursor-pointer transition-colors select-none"
|
||||
style={{
|
||||
backgroundColor: isDownHovered
|
||||
? "var(--vscode-list-hoverBackground, rgba(90, 93, 94, 0.31))"
|
||||
: "var(--vscode-editorWidget-background, rgba(60, 60, 60, 0.4))",
|
||||
}}
|
||||
onClick={() => handleNavigation("next")}
|
||||
onMouseEnter={() => setIsDownHovered(true)}
|
||||
onMouseLeave={() => setIsDownHovered(false)}>
|
||||
<span
|
||||
className="codicon codicon-chevron-down"
|
||||
style={{
|
||||
fontSize: "14px",
|
||||
color: "var(--vscode-foreground, rgba(255, 255, 255, 0.9))",
|
||||
}}></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Task card with high contrast theme variables */}
|
||||
<div
|
||||
key={`task-${currentTask.id}`}
|
||||
onClick={() => handleTaskClick(currentTask.prompt)}
|
||||
className="absolute inset-0 flex flex-col px-3 py-2 rounded-lg cursor-pointer select-none
|
||||
border border-white/30
|
||||
shadow-lg shadow-black/10 hover:shadow-xl hover:shadow-black/20
|
||||
active:shadow-md
|
||||
transition-transform duration-500 ease-out"
|
||||
style={{
|
||||
backgroundColor: "var(--vscode-statusBarItem-prominentBackground, var(--vscode-button-background))",
|
||||
transform: "translateY(0)",
|
||||
transition: "transform 0.5s ease-out, background-color 0.3s ease",
|
||||
}}>
|
||||
{/* Task content (adjusted to make room for left arrows) */}
|
||||
<div className="relative flex flex-col justify-center flex-1 text-center pl-6">
|
||||
<h3 className="text-[0.7rem] sm:text-sm md:text-base font-semibold mb-1 sm:mb-2 text-white/95 group-hover:text-white select-none">
|
||||
{currentTask.title}
|
||||
</h3>
|
||||
<p className="text-[0.6rem] sm:text-xs md:text-sm text-white/90 line-clamp-2 break-words leading-tight mx-auto select-none">
|
||||
{currentTask.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Paper airplane icon (center-right) */}
|
||||
<div
|
||||
className="absolute right-2 sm:right-2.5 top-1/2 transform -translate-y-1/2 w-3 sm:w-3.5 h-3 sm:h-3.5 opacity-70 hover:opacity-100
|
||||
transition-opacity duration-300 ease-out">
|
||||
<span className="codicon codicon-send text-white/90" style={{ fontSize: "14px" }}></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import { McpMarketplaceCatalog, McpServer, McpViewTab } from "../../../src/share
|
||||
import { convertTextMateToHljs } from "../utils/textMateToHljs"
|
||||
import { vscode } from "../utils/vscode"
|
||||
import { DEFAULT_BROWSER_SETTINGS } from "@shared/BrowserSettings"
|
||||
import { DEFAULT_CHAT_SETTINGS } from "@shared/ChatSettings"
|
||||
import { ChatSettings, DEFAULT_CHAT_SETTINGS } from "@shared/ChatSettings"
|
||||
import { TelemetrySetting } from "@shared/TelemetrySetting"
|
||||
|
||||
interface ExtensionStateContextType extends ExtensionState {
|
||||
@@ -41,7 +41,10 @@ interface ExtensionStateContextType extends ExtensionState {
|
||||
setTelemetrySetting: (value: TelemetrySetting) => void
|
||||
setShowAnnouncement: (value: boolean) => void
|
||||
setPlanActSeparateModelsSetting: (value: boolean) => void
|
||||
setEnableCheckpointsSetting: (value: boolean) => void
|
||||
setMcpMarketplaceEnabled: (value: boolean) => void
|
||||
setShellIntegrationTimeout: (value: number) => void
|
||||
setChatSettings: (value: ChatSettings) => void
|
||||
setMcpServers: (value: McpServer[]) => void
|
||||
|
||||
// Navigation
|
||||
@@ -70,11 +73,14 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
telemetrySetting: "unset",
|
||||
vscMachineId: "",
|
||||
planActSeparateModelsSetting: true,
|
||||
enableCheckpointsSetting: true,
|
||||
globalClineRulesToggles: {},
|
||||
localClineRulesToggles: {},
|
||||
localCursorRulesToggles: {},
|
||||
localWindsurfRulesToggles: {},
|
||||
workflowToggles: {},
|
||||
shellIntegrationTimeout: 4000, // default timeout for shell integration
|
||||
isNewUser: false,
|
||||
})
|
||||
const [didHydrateState, setDidHydrateState] = useState(false)
|
||||
const [showWelcome, setShowWelcome] = useState(false)
|
||||
@@ -268,6 +274,8 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
localClineRulesToggles: state.localClineRulesToggles || {},
|
||||
localCursorRulesToggles: state.localCursorRulesToggles || {},
|
||||
localWindsurfRulesToggles: state.localWindsurfRulesToggles || {},
|
||||
workflowToggles: state.workflowToggles || {},
|
||||
enableCheckpointsSetting: state.enableCheckpointsSetting,
|
||||
setApiConfiguration: (value) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
@@ -288,6 +296,16 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
...prevState,
|
||||
planActSeparateModelsSetting: value,
|
||||
})),
|
||||
setEnableCheckpointsSetting: (value) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
enableCheckpointsSetting: value,
|
||||
})),
|
||||
setMcpMarketplaceEnabled: (value) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
mcpMarketplaceEnabled: value,
|
||||
})),
|
||||
setShowAnnouncement: (value) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
@@ -300,6 +318,22 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
})),
|
||||
setMcpServers: (mcpServers: McpServer[]) => setMcpServers(mcpServers),
|
||||
setShowMcp,
|
||||
setChatSettings: (value) => {
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
chatSettings: value,
|
||||
}))
|
||||
vscode.postMessage({
|
||||
type: "updateSettings",
|
||||
chatSettings: value,
|
||||
apiConfiguration: state.apiConfiguration,
|
||||
customInstructionsSetting: state.customInstructions,
|
||||
telemetrySetting: state.telemetrySetting,
|
||||
planActSeparateModelsSetting: state.planActSeparateModelsSetting,
|
||||
enableCheckpointsSetting: state.enableCheckpointsSetting,
|
||||
mcpMarketplaceEnabled: state.mcpMarketplaceEnabled,
|
||||
})
|
||||
},
|
||||
setMcpTab,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,31 +1,52 @@
|
||||
export interface SlashCommand {
|
||||
name: string
|
||||
description: string
|
||||
description?: string
|
||||
section?: "default" | "custom"
|
||||
}
|
||||
|
||||
export const SUPPORTED_SLASH_COMMANDS: SlashCommand[] = [
|
||||
export const DEFAULT_SLASH_COMMANDS: SlashCommand[] = [
|
||||
{
|
||||
name: "newtask",
|
||||
description: "Create a new task with context from the current task",
|
||||
section: "default",
|
||||
},
|
||||
{
|
||||
name: "smol",
|
||||
description: "Condenses your current context window",
|
||||
section: "default",
|
||||
},
|
||||
{
|
||||
name: "newrule",
|
||||
description: "Create a new Cline rule based on your conversation",
|
||||
section: "default",
|
||||
},
|
||||
{
|
||||
name: "reportbug",
|
||||
description: "Create a Github issue with Cline",
|
||||
section: "default",
|
||||
},
|
||||
]
|
||||
|
||||
export function getWorkflowCommands(workflowToggles: Record<string, boolean>): SlashCommand[] {
|
||||
return Object.entries(workflowToggles)
|
||||
.filter(([_, enabled]) => enabled)
|
||||
.map(([filePath, _]) => {
|
||||
// potentially remove the file extension if there is one, but this would then require
|
||||
// that we prevent users from having the same fname with different extensions
|
||||
const fileName = filePath.replace(/^.*[/\\]/, "")
|
||||
|
||||
return {
|
||||
name: fileName,
|
||||
section: "custom",
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Regex for detecting slash commands in text
|
||||
export const slashCommandRegex = /\/([a-zA-Z0-9_-]+)(\s|$)/
|
||||
// currently doesn't allow whitespace inside of the filename
|
||||
export const slashCommandRegex = /\/([a-zA-Z0-9_\.-]+)(\s|$)/
|
||||
export const slashCommandRegexGlobal = new RegExp(slashCommandRegex.source, "g")
|
||||
export const slashCommandDeleteRegex = /^\s*\/([a-zA-Z0-9_-]+)$/
|
||||
export const slashCommandDeleteRegex = /^\s*\/([a-zA-Z0-9_\.-]+)$/
|
||||
|
||||
/**
|
||||
* Removes a slash command at the cursor position
|
||||
@@ -81,13 +102,16 @@ export function shouldShowSlashCommandsMenu(text: string, cursorPosition: number
|
||||
/**
|
||||
* Gets filtered slash commands that match the current input
|
||||
*/
|
||||
export function getMatchingSlashCommands(query: string): SlashCommand[] {
|
||||
export function getMatchingSlashCommands(query: string, workflowToggles: Record<string, boolean> = {}): SlashCommand[] {
|
||||
const workflowCommands = getWorkflowCommands(workflowToggles)
|
||||
const allCommands = [...DEFAULT_SLASH_COMMANDS, ...workflowCommands]
|
||||
|
||||
if (!query) {
|
||||
return [...SUPPORTED_SLASH_COMMANDS]
|
||||
return allCommands
|
||||
}
|
||||
|
||||
// filter commands that start with the query (case sensitive)
|
||||
return SUPPORTED_SLASH_COMMANDS.filter((cmd) => cmd.name.startsWith(query))
|
||||
return allCommands.filter((cmd) => cmd.name.startsWith(query))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -110,19 +134,22 @@ export function insertSlashCommand(text: string, commandName: string): { newValu
|
||||
* Determines the validation state of a slash command
|
||||
* Returns partial if we have a partial match against valid commands, or full for full match
|
||||
*/
|
||||
export function validateSlashCommand(command: string): "full" | "partial" | null {
|
||||
export function validateSlashCommand(command: string, workflowToggles: Record<string, boolean> = {}): "full" | "partial" | null {
|
||||
if (!command) {
|
||||
return null
|
||||
}
|
||||
|
||||
const workflowCommands = getWorkflowCommands(workflowToggles)
|
||||
const allCommands = [...DEFAULT_SLASH_COMMANDS, ...workflowCommands]
|
||||
|
||||
// case sensitive matching
|
||||
const exactMatch = SUPPORTED_SLASH_COMMANDS.some((cmd) => cmd.name === command)
|
||||
const exactMatch = allCommands.some((cmd) => cmd.name === command)
|
||||
|
||||
if (exactMatch) {
|
||||
return "full"
|
||||
}
|
||||
|
||||
const partialMatch = SUPPORTED_SLASH_COMMANDS.some((cmd) => cmd.name.startsWith(command))
|
||||
const partialMatch = allCommands.some((cmd) => cmd.name.startsWith(command))
|
||||
|
||||
if (partialMatch) {
|
||||
return "partial"
|
||||
|
||||
Reference in New Issue
Block a user