mirror of
https://github.com/cline/cline.git
synced 2026-09-02 15:52:29 +08:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 449c094933 | |||
| 100c578776 | |||
| 6fa516fc54 | |||
| 19cc8bc9f8 | |||
| 08c04a3c67 | |||
| 41ae7326c0 | |||
| b0961f4538 | |||
| 26242f6378 | |||
| 13228ed46f | |||
| d162a4b420 | |||
| 1704684af8 | |||
| c63d9a13a5 | |||
| 65243adb24 | |||
| e35f7b4e21 | |||
| 82449dabd6 | |||
| 74ec823017 | |||
| 91e222fe37 | |||
| 14230e7221 | |||
| 4b697d8695 | |||
| deeda6e273 | |||
| 7e7844529f | |||
| 4196c14c9c | |||
| 5294e78dde | |||
| d97424fcab | |||
| 2b3c0bb633 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
updated drag and drop text to say "drop" instead of "drag"
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Minor UX improvement to drag and drop ux
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Remove linear pull request action
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix for git commit mentions in repos with no git commits
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Adding args to allow Cursor to open workspaces (for checkpoint testing/development)
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Add a timeout setting for the terminal connection, allowing users to adjust this if they are having timeout issues
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
add checkpoints after more messages
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Introduce UI library for future UI development
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
add newrule slash command
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
add cache ui for open router and cline provider
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
showing expanded task by default
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Refactor to not pass a message for showing the MCP View from the servers modal
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Migrate the addRemoteServer to protobus
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Lowering Gemini cache TTL time
|
||||
@@ -13,6 +13,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
codespell:
|
||||
if: false
|
||||
name: Check for spelling errors
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -23,3 +24,5 @@ jobs:
|
||||
uses: codespell-project/codespell-problem-matcher@v1
|
||||
- name: Codespell
|
||||
uses: codespell-project/actions-codespell@v2
|
||||
with:
|
||||
only_warn: 1
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
name: Create Linear Issue on Pull Request
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
create-linear-issue-on-pull-request:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check for existing Linear link
|
||||
id: check-linear
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
result-encoding: string
|
||||
script: |
|
||||
const pr = context.payload.pull_request;
|
||||
// 1) PR body
|
||||
if (/https?:\/\/linear\.app/.test(pr.body||"")) {
|
||||
return "true";
|
||||
}
|
||||
// 2) Any linked GitHub issues?
|
||||
const res = await github.graphql(
|
||||
`query($owner:String!,$repo:String!,$prNumber:Int!){
|
||||
repository(owner:$owner,name:$repo){
|
||||
pullRequest(number:$prNumber){
|
||||
closingIssuesReferences(first:10){
|
||||
nodes{number}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
{
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
prNumber: pr.number
|
||||
}
|
||||
);
|
||||
for (const {number} of res.repository.pullRequest.closingIssuesReferences.nodes) {
|
||||
const comments = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: number
|
||||
});
|
||||
if (comments.data.some(c=>/https?:\/\/linear\.app/.test(c.body))) {
|
||||
return "true";
|
||||
}
|
||||
}
|
||||
return "false";
|
||||
|
||||
- name: Find or create Linear issue via GraphQL
|
||||
if: steps.check-linear.outputs.result == 'false'
|
||||
id: linear
|
||||
uses: actions/github-script@v6
|
||||
env:
|
||||
LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
|
||||
with:
|
||||
result-encoding: string
|
||||
script: |
|
||||
const API = 'https://api.linear.app/graphql';
|
||||
const apiKey = process.env.LINEAR_API_KEY;
|
||||
|
||||
// Check if API key exists
|
||||
if (!apiKey) {
|
||||
core.setFailed('LINEAR_API_KEY is not set. Please add it to your repository secrets.');
|
||||
core.setOutput('error', 'true');
|
||||
core.setOutput('error-message', 'LINEAR_API_KEY is not set. Please add it to your repository secrets.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Helper to call Linear with error handling
|
||||
async function gql(q, v) {
|
||||
try {
|
||||
const r = await fetch(API, {
|
||||
method:'POST',
|
||||
headers:{
|
||||
'Content-Type':'application/json',
|
||||
'Authorization': apiKey
|
||||
},
|
||||
body: JSON.stringify({ query: q, variables: v })
|
||||
});
|
||||
|
||||
if (!r.ok) {
|
||||
throw new Error(`Linear API responded with status ${r.status}: ${await r.text()}`);
|
||||
}
|
||||
|
||||
const json = await r.json();
|
||||
|
||||
// Check for GraphQL errors
|
||||
if (json.errors && json.errors.length > 0) {
|
||||
const errorMessages = json.errors.map(e => e.message).join(', ');
|
||||
throw new Error(`Linear GraphQL errors: ${errorMessages}`);
|
||||
}
|
||||
|
||||
return json.data;
|
||||
} catch (error) {
|
||||
core.error(`Error calling Linear API: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// 1) Set team ID
|
||||
const teamId = "19b9c1b2-5f58-498c-b1bf-23ee8f52a677"
|
||||
|
||||
// 2) Look for existing issue by PR URL
|
||||
const pr = context.payload.pull_request;
|
||||
const searchData = await gql(
|
||||
`query($team:ID!,$q:String!){
|
||||
issues(filter: { team: { id: { eq: $team } } attachments: { some: { url: { eq: $q } } } }){nodes{id,url}}
|
||||
}`,
|
||||
{ team: teamId, q: pr.html_url }
|
||||
);
|
||||
let issue = searchData.issues.nodes[0];
|
||||
|
||||
// 3) Create if missing
|
||||
if (!issue) {
|
||||
const createData = await gql(
|
||||
`mutation($input:IssueCreateInput!){
|
||||
issueCreate(input:$input){issue{id,url}}
|
||||
}`,
|
||||
{
|
||||
input: {
|
||||
teamId,
|
||||
title: `[GITHUB] ${pr.title}`,
|
||||
description: `${pr.body||''}\n\n${pr.html_url}`,
|
||||
stateId: "4d9bcba2-6712-47e3-b577-6ec1ee023dc2",
|
||||
labelIds: ["504e7d60-5037-483f-a9b8-7e298bdf116f"]
|
||||
}
|
||||
}
|
||||
);
|
||||
issue = createData.issueCreate.issue;
|
||||
}
|
||||
|
||||
// Set output for next steps
|
||||
core.setOutput('linear-issue-url', issue.url);
|
||||
core.setOutput('error', 'false');
|
||||
} catch (error) {
|
||||
core.setOutput('error', 'true');
|
||||
core.setOutput('error-message', error.message);
|
||||
core.setFailed(`Failed to create or find Linear issue: ${error.message}`);
|
||||
}
|
||||
|
||||
- name: Comment PR with Linear link
|
||||
if: steps.check-linear.outputs.result == 'false'
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const pr = context.payload.pull_request;
|
||||
const url = `${{ steps.linear.outputs.linear-issue-url }}`;
|
||||
const body = `🔗 Linear issue created: ${url}`;
|
||||
// Fetch existing comments
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
...context.repo,
|
||||
issue_number: pr.number
|
||||
});
|
||||
const botComment = comments.find(c =>
|
||||
c.user.type === "Bot" && c.body.startsWith("🔗 Linear issue created:")
|
||||
);
|
||||
if (botComment) {
|
||||
await github.rest.issues.updateComment({
|
||||
...context.repo,
|
||||
comment_id: botComment.id,
|
||||
body
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
...context.repo,
|
||||
issue_number: pr.number,
|
||||
body
|
||||
});
|
||||
}
|
||||
@@ -70,8 +70,8 @@ jobs:
|
||||
run: npm run format
|
||||
|
||||
# Build the extension before running tests
|
||||
- name: Build Extension
|
||||
run: npm run compile
|
||||
- name: Build Tests and Extension
|
||||
run: npm run pretest
|
||||
|
||||
- name: Unit Tests
|
||||
run: npm run test:unit
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
id: extension_coverage
|
||||
continue-on-error: true
|
||||
run: |
|
||||
xvfb-run -a npm run test:coverage > extension_coverage.txt 2>&1 || true
|
||||
xvfb-run -a npm run test:coverage > extension_coverage.txt 2>&1
|
||||
PYTHONPATH=.github/scripts python -m coverage_check extract-coverage extension_coverage.txt --type=extension --github-output --verbose
|
||||
|
||||
# Run webview tests with coverage
|
||||
@@ -106,6 +106,18 @@ jobs:
|
||||
webview-ui/webview_coverage.txt
|
||||
retention-period: workflow # Artifacts are automatically deleted when the workflow completes
|
||||
|
||||
# Set the check as failed if any of the tests failed
|
||||
- name: Check for test failures
|
||||
run: |
|
||||
# Check if any of the test steps failed
|
||||
# https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs#steps-context
|
||||
if [ "${{ steps.extension_coverage.outcome }}" != "success" ] || [ "${{ steps.webview_coverage.outcome }}" != "success" ]; then
|
||||
echo "Tests failed."
|
||||
cat extension_coverage.txt
|
||||
cat webview-ui/webview_coverage.txt
|
||||
exit 1
|
||||
fi
|
||||
|
||||
coverage:
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -6,6 +6,10 @@ export default defineConfig({
|
||||
mocha: {
|
||||
ui: "bdd",
|
||||
timeout: 20000, // Maximum time (in ms) that a test can run before failing
|
||||
/** Set up alias path resolution during tests
|
||||
* @See {@link file://./test-setup.js}
|
||||
*/
|
||||
require: ["./test-setup.js"],
|
||||
},
|
||||
workspaceFolder: "test-workspace",
|
||||
version: "stable",
|
||||
|
||||
Vendored
+1
-2
@@ -9,10 +9,9 @@
|
||||
"name": "Run Extension",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
|
||||
"args": ["--extensionDevelopmentPath=${workspaceFolder}", "--disable-workspace-trust", "${workspaceFolder}"],
|
||||
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||
"preLaunchTask": "${defaultBuildTask}",
|
||||
"postDebugTask": "stop",
|
||||
"env": {
|
||||
"IS_DEV": "true",
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}"
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 902 B |
Binary file not shown.
|
After Width: | Height: | Size: 666 B |
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"$schema": "https://mintlify.com/docs.json",
|
||||
"theme": "linden",
|
||||
"name": "Cline",
|
||||
"description": "AI-powered coding assistant for VSCode",
|
||||
"colors": {
|
||||
"primary": "#9D4EDD",
|
||||
"light": "#F0E6FF",
|
||||
"dark": "#000000"
|
||||
},
|
||||
"logo": {
|
||||
"light": "/assets/robot_panel_light.png",
|
||||
"dark": "/assets/robot_panel_dark.png"
|
||||
},
|
||||
"favicon": {
|
||||
"light": "/assets/robot_panel_light.png",
|
||||
"dark": "/assets/robot_panel_dark.png"
|
||||
},
|
||||
"background": {
|
||||
"color": {
|
||||
"light": "#F0E6FF",
|
||||
"dark": "#000000"
|
||||
},
|
||||
"decoration": "gradient"
|
||||
},
|
||||
"styling": {
|
||||
"eyebrows": "breadcrumbs",
|
||||
"codeblocks": "system"
|
||||
},
|
||||
"appearance": {
|
||||
"default": "system",
|
||||
"strict": false
|
||||
},
|
||||
"fonts": {
|
||||
"family": "Roboto",
|
||||
"weight": 400
|
||||
},
|
||||
"navbar": {
|
||||
"links": [
|
||||
{
|
||||
"label": "GitHub",
|
||||
"href": "https://github.com/cline/cline"
|
||||
},
|
||||
{
|
||||
"label": "Discord",
|
||||
"href": "https://discord.gg/cline"
|
||||
}
|
||||
],
|
||||
"primary": {
|
||||
"type": "button",
|
||||
"label": "Install Cline",
|
||||
"href": "https://cline.bot/install?utm_source=website&utm_medium=header"
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"pages": ["introduction"]
|
||||
},
|
||||
"footer": {
|
||||
"socials": {
|
||||
"x": "https://x.com/cline",
|
||||
"github": "https://github.com/cline/cline",
|
||||
"discord": "https://discord.gg/cline"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"prompt": "Search Cline documentation..."
|
||||
},
|
||||
"contextual": {
|
||||
"options": ["copy"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Hello World"
|
||||
description: "This is the introduction to the documentation"
|
||||
---
|
||||
@@ -19,6 +19,7 @@ const aliasResolverPlugin = {
|
||||
"@services": path.resolve(__dirname, "src/services"),
|
||||
"@shared": path.resolve(__dirname, "src/shared"),
|
||||
"@utils": path.resolve(__dirname, "src/utils"),
|
||||
"@packages": path.resolve(__dirname, "src/packages"),
|
||||
}
|
||||
|
||||
// For each alias entry, create a resolver
|
||||
|
||||
Generated
+7559
-2
File diff suppressed because it is too large
Load Diff
+4
-2
@@ -292,7 +292,7 @@
|
||||
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
|
||||
"package": "npm run build:webview && npm run check-types && npm run lint && node esbuild.js --production",
|
||||
"protos": "node proto/build-proto.js && prettier src/shared/proto --write && prettier src/core/controller --write",
|
||||
"compile-tests": "tsc -p ./tsconfig.test.json --outDir out",
|
||||
"compile-tests": "node ./scripts/build-tests.js",
|
||||
"watch-tests": "tsc -p . -w --outDir out",
|
||||
"pretest": "npm run compile-tests && npm run compile && npm run lint",
|
||||
"check-types": "tsc --noEmit",
|
||||
@@ -312,7 +312,8 @@
|
||||
"publish:marketplace:prerelease": "vsce publish --pre-release && ovsx publish --pre-release",
|
||||
"prepare": "husky",
|
||||
"changeset": "changeset",
|
||||
"version-packages": "changeset version"
|
||||
"version-packages": "changeset version",
|
||||
"docs:preview": "cd docs && mintlify dev"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@changesets/cli": "^2.27.12",
|
||||
@@ -338,6 +339,7 @@
|
||||
"eslint": "^8.57.0",
|
||||
"grpc-tools": "^1.13.0",
|
||||
"husky": "^9.1.7",
|
||||
"mintlify": "^4.0.515",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.3.3",
|
||||
"protoc-gen-ts": "^0.8.7",
|
||||
|
||||
@@ -9,6 +9,7 @@ import "common.proto";
|
||||
service McpService {
|
||||
rpc toggleMcpServer(ToggleMcpServerRequest) returns (McpServers);
|
||||
rpc updateMcpTimeout(UpdateMcpTimeoutRequest) returns (McpServers);
|
||||
rpc addRemoteMcpServer(AddRemoteMcpServerRequest) returns (McpServers);
|
||||
}
|
||||
|
||||
message ToggleMcpServerRequest {
|
||||
@@ -23,6 +24,12 @@ message UpdateMcpTimeoutRequest {
|
||||
int32 timeout = 3;
|
||||
}
|
||||
|
||||
message AddRemoteMcpServerRequest {
|
||||
Metadata metadata = 1;
|
||||
string server_name = 2;
|
||||
string server_url = 3;
|
||||
}
|
||||
|
||||
message McpTool {
|
||||
string name = 1;
|
||||
optional string description = 2;
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
const { execSync } = require("child_process")
|
||||
const esbuild = require("esbuild")
|
||||
|
||||
const watch = process.argv.includes("--watch")
|
||||
|
||||
/**
|
||||
* @type {import('esbuild').Plugin}
|
||||
*/
|
||||
const esbuildProblemMatcherPlugin = {
|
||||
name: "esbuild-problem-matcher",
|
||||
|
||||
setup(build) {
|
||||
build.onStart(() => {
|
||||
console.log("[watch] build started")
|
||||
})
|
||||
build.onEnd((result) => {
|
||||
result.errors.forEach(({ text, location }) => {
|
||||
console.error(`✘ [ERROR] ${text}`)
|
||||
console.error(` ${location.file}:${location.line}:${location.column}:`)
|
||||
})
|
||||
console.log("[watch] build finished")
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
const srcConfig = {
|
||||
bundle: true,
|
||||
minify: false,
|
||||
sourcemap: true,
|
||||
sourcesContent: true,
|
||||
logLevel: "silent",
|
||||
entryPoints: ["src/packages/**/*.ts"],
|
||||
outdir: "out/packages",
|
||||
format: "cjs",
|
||||
platform: "node",
|
||||
define: {
|
||||
"process.env.IS_DEV": "true",
|
||||
"process.env.IS_TEST": "true",
|
||||
},
|
||||
external: ["vscode"],
|
||||
plugins: [esbuildProblemMatcherPlugin],
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const srcCtx = await esbuild.context(srcConfig)
|
||||
|
||||
if (watch) {
|
||||
await srcCtx.watch()
|
||||
} else {
|
||||
await srcCtx.rebuild()
|
||||
|
||||
await srcCtx.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
execSync("tsc -p ./tsconfig.test.json --outDir out", { encoding: "utf-8" })
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -74,6 +74,8 @@ export class ClineHandler implements ApiHandler {
|
||||
if (!didOutputUsage && chunk.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
@@ -105,6 +107,9 @@ export class ClineHandler implements ApiHandler {
|
||||
const generation = response.data
|
||||
return {
|
||||
type: "usage",
|
||||
// at this time there's no support for gatting cached_tokens from generation endpoint
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
inputTokens: generation?.native_tokens_prompt || 0,
|
||||
outputTokens: generation?.native_tokens_completion || 0,
|
||||
totalCost: generation?.total_cost || 0,
|
||||
|
||||
@@ -7,8 +7,8 @@ import { ApiHandlerOptions, geminiDefaultModelId, GeminiModelId, geminiModels, M
|
||||
import { convertAnthropicMessageToGemini } from "../transform/gemini-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
// Define a default TTL for the cache (e.g., 1 hour in seconds)
|
||||
const DEFAULT_CACHE_TTL_SECONDS = 3600
|
||||
// Define a default TTL for the cache (e.g., 15 minutes in seconds)
|
||||
const DEFAULT_CACHE_TTL_SECONDS = 900
|
||||
|
||||
export class GeminiHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
|
||||
@@ -76,6 +76,8 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
if (!didOutputUsage && chunk.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
@@ -103,8 +105,9 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
// console.log("OpenRouter generation details:", generation)
|
||||
return {
|
||||
type: "usage",
|
||||
// cacheWriteTokens: 0,
|
||||
// cacheReadTokens: 0,
|
||||
// at this time there's no support for gatting cached_tokens from generation endpoint
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
// openrouter generation endpoint fails often
|
||||
inputTokens: generation?.native_tokens_prompt || 0,
|
||||
outputTokens: generation?.native_tokens_completion || 0,
|
||||
|
||||
@@ -25,6 +25,7 @@ export const toolUseNames = [
|
||||
"attempt_completion",
|
||||
"new_task",
|
||||
"condense",
|
||||
"new_rule",
|
||||
] as const
|
||||
|
||||
// Converts array of tool call names into a union type ("execute_command" | "read_file" | ...)
|
||||
|
||||
@@ -55,7 +55,10 @@ export function parseAssistantMessage(assistantMessage: string) {
|
||||
|
||||
// special case for write_to_file where file contents could contain the closing tag, in which case the param would have closed and we end up with the rest of the file contents here. To work around this, we get the string between the starting content tag and the LAST content tag.
|
||||
const contentParamName: ToolParamName = "content"
|
||||
if (currentToolUse.name === "write_to_file" && accumulator.endsWith(`</${contentParamName}>`)) {
|
||||
if (
|
||||
(currentToolUse.name === "write_to_file" || currentToolUse.name === "new_rule") &&
|
||||
accumulator.endsWith(`</${contentParamName}>`)
|
||||
) {
|
||||
const toolContent = accumulator.slice(currentToolUseStartIndex)
|
||||
const contentStartTag = `<${contentParamName}>`
|
||||
const contentEndTag = `</${contentParamName}>`
|
||||
|
||||
@@ -76,26 +76,6 @@ describe("ModelContextTracker", () => {
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw an error when controller is dereferenced", async () => {
|
||||
// Create a new tracker with a controller that will be garbage collected
|
||||
const weakTracker = new ModelContextTracker(mockContext, taskId)
|
||||
|
||||
// Force the WeakRef to return null by overriding the deref method
|
||||
const weakRef = { deref: sandbox.stub().returns(null) }
|
||||
sandbox.stub(WeakRef.prototype, "deref").callsFake(() => weakRef.deref())
|
||||
|
||||
try {
|
||||
// Try to call the method - this should throw
|
||||
await weakTracker.recordModelUsage("any-provider", "any-model", "any-mode")
|
||||
|
||||
// If we get here, the test should fail
|
||||
expect.fail("Expected an error to be thrown")
|
||||
} catch (error) {
|
||||
// Verify the error message
|
||||
expect(error.message).to.equal("Unable to access extension context")
|
||||
}
|
||||
})
|
||||
|
||||
it("should append model usage to existing entries", async () => {
|
||||
// Add an existing model usage entry
|
||||
const existingTimestamp = 1617200000000
|
||||
|
||||
@@ -7,6 +7,45 @@ import { ClineRulesToggles } from "@shared/cline-rules"
|
||||
import { getGlobalState, getWorkspaceState, updateGlobalState, updateWorkspaceState } from "@core/storage/state"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
/**
|
||||
* 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)) {
|
||||
|
||||
@@ -136,8 +136,14 @@ export class Controller {
|
||||
|
||||
async initTask(task?: string, images?: string[], historyItem?: HistoryItem) {
|
||||
await this.clearTask() // ensures that an existing task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one
|
||||
const { apiConfiguration, customInstructions, autoApprovalSettings, browserSettings, chatSettings } =
|
||||
await getAllExtensionState(this.context)
|
||||
const {
|
||||
apiConfiguration,
|
||||
customInstructions,
|
||||
autoApprovalSettings,
|
||||
browserSettings,
|
||||
chatSettings,
|
||||
shellIntegrationTimeout,
|
||||
} = await getAllExtensionState(this.context)
|
||||
|
||||
if (autoApprovalSettings) {
|
||||
const updatedAutoApprovalSettings = {
|
||||
@@ -159,6 +165,7 @@ export class Controller {
|
||||
autoApprovalSettings,
|
||||
browserSettings,
|
||||
chatSettings,
|
||||
shellIntegrationTimeout,
|
||||
customInstructions,
|
||||
task,
|
||||
images,
|
||||
@@ -186,28 +193,6 @@ export class Controller {
|
||||
*/
|
||||
async handleWebviewMessage(message: WebviewMessage) {
|
||||
switch (message.type) {
|
||||
case "addRemoteServer": {
|
||||
try {
|
||||
await this.mcpHub?.addRemoteServer(message.serverName!, message.serverUrl!)
|
||||
await this.postMessageToWebview({
|
||||
type: "addRemoteServerResult",
|
||||
addRemoteServerResult: {
|
||||
success: true,
|
||||
serverName: message.serverName!,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
await this.postMessageToWebview({
|
||||
type: "addRemoteServerResult",
|
||||
addRemoteServerResult: {
|
||||
success: false,
|
||||
serverName: message.serverName!,
|
||||
error: error.message,
|
||||
},
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
case "authStateChanged":
|
||||
await this.setUserInfo(message.user || undefined)
|
||||
await this.postStateToWebview()
|
||||
@@ -457,10 +442,6 @@ export class Controller {
|
||||
await this.fetchUserCreditsData()
|
||||
break
|
||||
}
|
||||
case "showMcpView": {
|
||||
await this.postMessageToWebview({ type: "action", action: "mcpButtonClicked", tab: message.tab || undefined })
|
||||
break
|
||||
}
|
||||
case "openMcpSettings": {
|
||||
const mcpSettingsFilePath = await this.mcpHub?.getMcpSettingsFilePath()
|
||||
if (mcpSettingsFilePath) {
|
||||
@@ -810,6 +791,21 @@ export class Controller {
|
||||
}
|
||||
break
|
||||
}
|
||||
case "updateTerminalConnectionTimeout": {
|
||||
if (message.shellIntegrationTimeout !== undefined) {
|
||||
const timeout = message.shellIntegrationTimeout
|
||||
|
||||
if (typeof timeout === "number" && !isNaN(timeout) && timeout > 0) {
|
||||
await updateGlobalState(this.context, "shellIntegrationTimeout", timeout)
|
||||
await this.postStateToWebview()
|
||||
} else {
|
||||
console.warn(
|
||||
`Invalid shell integration timeout value received: ${timeout}. ` + `Expected a positive number.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
// Add more switch case statements here as more webview message commands
|
||||
// are created within the webview context (i.e. inside media/main.js)
|
||||
}
|
||||
@@ -1795,6 +1791,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
|
||||
telemetrySetting,
|
||||
planActSeparateModelsSetting,
|
||||
globalClineRulesToggles,
|
||||
shellIntegrationTimeout,
|
||||
} = await getAllExtensionState(this.context)
|
||||
|
||||
const localClineRulesToggles =
|
||||
@@ -1824,6 +1821,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
|
||||
vscMachineId: vscode.env.machineId,
|
||||
globalClineRulesToggles: globalClineRulesToggles || {},
|
||||
localClineRulesToggles: localClineRulesToggles || {},
|
||||
shellIntegrationTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { convertMcpServersToProtoMcpServers } from "@/shared/proto-conversions/mcp/mcp-server-conversion"
|
||||
import type { AddRemoteMcpServerRequest, McpServers } from "../../../shared/proto/mcp"
|
||||
import type { Controller } from "../index"
|
||||
|
||||
/**
|
||||
* Adds a new remote MCP server via gRPC
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing server name and URL
|
||||
* @returns An array of McpServer objects
|
||||
*/
|
||||
export async function addRemoteMcpServer(controller: Controller, request: AddRemoteMcpServerRequest): Promise<McpServers> {
|
||||
try {
|
||||
// Validate required fields
|
||||
if (!request.serverName) {
|
||||
throw new Error("Server name is required")
|
||||
}
|
||||
if (!request.serverUrl) {
|
||||
throw new Error("Server URL is required")
|
||||
}
|
||||
|
||||
// Call the McpHub method to add the remote server
|
||||
const servers = await controller.mcpHub?.addRemoteServer(request.serverName, request.serverUrl)
|
||||
|
||||
const protoServers = convertMcpServersToProtoMcpServers(servers)
|
||||
|
||||
return { mcpServers: protoServers }
|
||||
} catch (error) {
|
||||
console.error(`Failed to add remote MCP server ${request.serverName}:`, error)
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,14 @@
|
||||
|
||||
// Import all method implementations
|
||||
import { registerMethod } from "./index"
|
||||
import { addRemoteMcpServer } from "./addRemoteMcpServer"
|
||||
import { toggleMcpServer } from "./toggleMcpServer"
|
||||
import { updateMcpTimeout } from "./updateMcpTimeout"
|
||||
|
||||
// Register all mcp service methods
|
||||
export function registerAllMethods(): void {
|
||||
// Register each method with the registry
|
||||
registerMethod("addRemoteMcpServer", addRemoteMcpServer)
|
||||
registerMethod("toggleMcpServer", toggleMcpServer)
|
||||
registerMethod("updateMcpTimeout", updateMcpTimeout)
|
||||
}
|
||||
|
||||
@@ -87,3 +87,61 @@ Example:
|
||||
|
||||
</explicit_instructions>\n
|
||||
`
|
||||
|
||||
export const newRuleToolResponse = () =>
|
||||
`<explicit_instructions type="new_rule">
|
||||
The user has explicitly asked you to help them create a new Cline rule file inside the .clinerules top-level directory based on the conversation up to this point in time. The user may have provided instructions or additional information for you to consider when creating the new Cline rule.
|
||||
When creating a new Cline rule file, you should NOT overwrite or alter an existing Cline rule file. To create the Cline rule file you MUST use the new_rule tool. The new_rule tool can be used in either of the PLAN or ACT modes.
|
||||
|
||||
The new_rule tool is defined below:
|
||||
|
||||
Description:
|
||||
Your task is to create a new Cline rule file which includes guidelines on how to approach developing code in tandem with the user, which can be either project specific or cover more global rules. This includes but is not limited to: desired conversational style, favorite project dependencies, coding styles, naming conventions, architectural choices, ui/ux preferences, etc.
|
||||
The Cline rule file must be formatted as markdown and be a '.md' file. The name of the file you generate must be as succinct as possible and be encompassing the main overarching concept of the rules you added to the file (e.g., 'memory-bank.md' or 'project-overview.md').
|
||||
|
||||
Parameters:
|
||||
- Path: (required) The path of the file to write to (relative to the current working directory). This will be the Cline rule file you create, and it must be placed inside the .clinerules top-level directory (create this if it doesn't exist). The filename created CANNOT be "default-clineignore.md". For filenames, use hyphens ("-") instead of underscores ("_") to separate words.
|
||||
- Content: (required) The content to write to the file. ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. The content for the Cline rule file MUST be created according to the following instructions:
|
||||
1. Format the Cline rule file to have distinct guideline sections, each with their own markdown heading, starting with "## Brief overview". Under each of these headings, include bullet points fully fleshing out the details, with examples and/or trigger cases ONLY when applicable.
|
||||
2. These guidelines can be specific to the task(s) or project worked on thus far, or cover more high-level concepts. Guidelines can include coding conventions, general design patterns, preferred tech stack including favorite libraries and language, communication style with Cline (verbose vs concise), prompting strategies, naming conventions, testing strategies, comment verbosity, time spent on architecting prior to development, and other preferences.
|
||||
3. When creating guidelines, you should not invent preferences or make assumptions based on what you think a typical user might want. These should be specific to the conversation you had with the user. Your guidelines / rules should not be overly verbose.
|
||||
4. Your guidelines should NOT be a recollection of the conversation up to this point in time, meaning you should NOT be including arbitrary details of the conversation.
|
||||
|
||||
Usage:
|
||||
<new_rule>
|
||||
<path>.clinerules/{file name}.md</path>
|
||||
<content>Cline rule file content here</content>
|
||||
</new_rule>
|
||||
|
||||
Example:
|
||||
<new_rule>
|
||||
<path>.clinerules/project-preferences.md</path>
|
||||
<content>
|
||||
## Brief overview
|
||||
[Brief description of the rules, including if this set of guidelines is project-specific or global]
|
||||
|
||||
## Communication style
|
||||
- [Description, rule, preference, instruction]
|
||||
- [...]
|
||||
|
||||
## Development workflow
|
||||
- [Description, rule, preference, instruction]
|
||||
- [...]
|
||||
|
||||
## Coding best practices
|
||||
- [Description, rule, preference, instruction]
|
||||
- [...]
|
||||
|
||||
## Project context
|
||||
- [Description, rule, preference, instruction]
|
||||
- [...]
|
||||
|
||||
## Other guidelines
|
||||
- [Description, rule, preference, instruction]
|
||||
- [...]
|
||||
</content>
|
||||
</new_rule>
|
||||
|
||||
Below is the user's input when they indicated that they wanted to create a new Cline rule file.
|
||||
</explicit_instructions>\n
|
||||
`
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { newTaskToolResponse, condenseToolResponse } from "../prompts/commands"
|
||||
import { newTaskToolResponse, condenseToolResponse, newRuleToolResponse } from "../prompts/commands"
|
||||
|
||||
/**
|
||||
* 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): string {
|
||||
const SUPPORTED_COMMANDS = ["newtask", "smol", "compact"]
|
||||
export function parseSlashCommands(text: string): { processedText: string; needsClinerulesFileCheck: boolean } {
|
||||
const SUPPORTED_COMMANDS = ["newtask", "smol", "compact", "newrule"]
|
||||
|
||||
const commandReplacements: Record<string, string> = {
|
||||
newtask: newTaskToolResponse(),
|
||||
smol: condenseToolResponse(),
|
||||
compact: condenseToolResponse(),
|
||||
newrule: newRuleToolResponse(),
|
||||
}
|
||||
|
||||
// this currently allows matching prepended whitespace prior to /slash-command
|
||||
@@ -47,11 +48,11 @@ export function parseSlashCommands(text: string): string {
|
||||
const textWithoutSlashCommand = text.substring(0, slashCommandStartIndex) + text.substring(slashCommandEndIndex)
|
||||
const processedText = commandReplacements[commandName] + textWithoutSlashCommand
|
||||
|
||||
return processedText
|
||||
return { processedText: processedText, needsClinerulesFileCheck: commandName === "newrule" ? true : false }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if no supported commands are found, return the original text
|
||||
return text
|
||||
return { processedText: text, needsClinerulesFileCheck: false }
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { fileExistsAtPath } from "@utils/fs"
|
||||
import { ClineMessage } from "@shared/ExtensionMessage"
|
||||
import { TaskMetadata } from "@core/context/context-tracking/ContextTrackerTypes"
|
||||
import os from "os"
|
||||
import { execa } from "execa"
|
||||
import { execa } from "@packages/execa"
|
||||
|
||||
export const GlobalFileNames = {
|
||||
apiConversationHistory: "api_conversation_history.json",
|
||||
|
||||
@@ -76,5 +76,6 @@ export type GlobalStateKey =
|
||||
| "planActSeparateModelsSetting"
|
||||
| "favoritedModelIds"
|
||||
| "requestTimeoutMs"
|
||||
| "shellIntegrationTimeout"
|
||||
|
||||
export type LocalStateKey = "localClineRulesToggles"
|
||||
|
||||
@@ -126,6 +126,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
favoritedModelIds,
|
||||
globalClineRulesToggles,
|
||||
requestTimeoutMs,
|
||||
shellIntegrationTimeout,
|
||||
] = await Promise.all([
|
||||
getGlobalState(context, "apiProvider") as Promise<ApiProvider | undefined>,
|
||||
getGlobalState(context, "apiModelId") as Promise<string | undefined>,
|
||||
@@ -200,6 +201,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
getGlobalState(context, "favoritedModelIds") as Promise<string[] | undefined>,
|
||||
getGlobalState(context, "globalClineRulesToggles") as Promise<ClineRulesToggles | undefined>,
|
||||
getGlobalState(context, "requestTimeoutMs") as Promise<number | undefined>,
|
||||
getGlobalState(context, "shellIntegrationTimeout") as Promise<number | undefined>,
|
||||
])
|
||||
|
||||
let apiProvider: ApiProvider
|
||||
@@ -319,6 +321,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
mcpMarketplaceEnabled,
|
||||
telemetrySetting: telemetrySetting || "unset",
|
||||
planActSeparateModelsSetting,
|
||||
shellIntegrationTimeout: shellIntegrationTimeout || 4000,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+129
-52
@@ -86,6 +86,7 @@ import {
|
||||
getGlobalClineRules,
|
||||
getLocalClineRules,
|
||||
refreshClineRulesToggles,
|
||||
ensureLocalClinerulesDirExists,
|
||||
} from "@core/context/instructions/user-instructions/cline-rules"
|
||||
import { getGlobalState } from "@core/storage/state"
|
||||
import { parseSlashCommands } from "@core/slash-commands"
|
||||
@@ -172,6 +173,7 @@ export class Task {
|
||||
autoApprovalSettings: AutoApprovalSettings,
|
||||
browserSettings: BrowserSettings,
|
||||
chatSettings: ChatSettings,
|
||||
shellIntegrationTimeout: number,
|
||||
customInstructions?: string,
|
||||
task?: string,
|
||||
images?: string[],
|
||||
@@ -190,6 +192,7 @@ export class Task {
|
||||
console.error("Failed to initialize ClineIgnoreController:", error)
|
||||
})
|
||||
this.terminalManager = new TerminalManager()
|
||||
this.terminalManager.setShellIntegrationTimeout(shellIntegrationTimeout)
|
||||
this.urlContentFetcher = new UrlContentFetcher(context)
|
||||
this.browserSession = new BrowserSession(context, browserSettings)
|
||||
this.contextManager = new ContextManager()
|
||||
@@ -926,6 +929,7 @@ export class Task {
|
||||
let responseImages: string[] | undefined
|
||||
if (response === "messageResponse") {
|
||||
await this.say("user_feedback", text, images)
|
||||
await this.saveCheckpoint()
|
||||
responseText = text
|
||||
responseImages = images
|
||||
}
|
||||
@@ -1325,6 +1329,7 @@ export class Task {
|
||||
|
||||
if (userFeedback) {
|
||||
await this.say("user_feedback", userFeedback.text, userFeedback.images)
|
||||
await this.saveCheckpoint()
|
||||
return [
|
||||
true,
|
||||
formatResponse.toolResult(
|
||||
@@ -1361,6 +1366,7 @@ export class Task {
|
||||
this.autoApprovalSettings.actions.readFiles,
|
||||
this.autoApprovalSettings.actions.readFilesExternally ?? false,
|
||||
]
|
||||
case "new_rule":
|
||||
case "write_to_file":
|
||||
case "replace_in_file":
|
||||
return [
|
||||
@@ -1686,6 +1692,8 @@ export class Task {
|
||||
return `[${block.name} for creating a new task]`
|
||||
case "condense":
|
||||
return `[${block.name}]`
|
||||
case "new_rule":
|
||||
return `[${block.name} for '${block.params.path}']`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1759,6 +1767,7 @@ export class Task {
|
||||
if (text || images?.length) {
|
||||
pushAdditionalToolFeedback(text, images)
|
||||
await this.say("user_feedback", text, images)
|
||||
await this.saveCheckpoint()
|
||||
}
|
||||
this.didRejectTool = true // Prevent further tool uses in this message
|
||||
return false
|
||||
@@ -1767,6 +1776,7 @@ export class Task {
|
||||
if (text || images?.length) {
|
||||
pushAdditionalToolFeedback(text, images)
|
||||
await this.say("user_feedback", text, images)
|
||||
await this.saveCheckpoint()
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -1825,6 +1835,7 @@ export class Task {
|
||||
}
|
||||
|
||||
switch (block.name) {
|
||||
case "new_rule":
|
||||
case "write_to_file":
|
||||
case "replace_in_file": {
|
||||
const relPath: string | undefined = block.params.path
|
||||
@@ -1840,7 +1851,7 @@ export class Task {
|
||||
if (!accessAllowed) {
|
||||
await this.say("clineignore_error", relPath)
|
||||
pushToolResult(formatResponse.toolError(formatResponse.clineIgnoreError(relPath)))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
|
||||
@@ -1896,6 +1907,7 @@ export class Task {
|
||||
)
|
||||
await this.diffViewProvider.revertChanges()
|
||||
await this.diffViewProvider.reset()
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
} else if (content) {
|
||||
@@ -1953,21 +1965,28 @@ export class Task {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError(block.name, "path"))
|
||||
await this.diffViewProvider.reset()
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
if (block.name === "replace_in_file" && !diff) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("replace_in_file", "diff"))
|
||||
await this.diffViewProvider.reset()
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
if (block.name === "write_to_file" && !content) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("write_to_file", "content"))
|
||||
await this.diffViewProvider.reset()
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
if (block.name === "new_rule" && !content) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("new_rule", "content"))
|
||||
await this.diffViewProvider.reset()
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
|
||||
@@ -2026,6 +2045,7 @@ export class Task {
|
||||
if (text || images?.length) {
|
||||
pushAdditionalToolFeedback(text, images)
|
||||
await this.say("user_feedback", text, images)
|
||||
await this.saveCheckpoint()
|
||||
}
|
||||
this.didRejectTool = true
|
||||
didApprove = false
|
||||
@@ -2035,12 +2055,14 @@ export class Task {
|
||||
if (text || images?.length) {
|
||||
pushAdditionalToolFeedback(text, images)
|
||||
await this.say("user_feedback", text, images)
|
||||
await this.saveCheckpoint()
|
||||
}
|
||||
telemetryService.captureToolUsage(this.taskId, block.name, false, true)
|
||||
}
|
||||
|
||||
if (!didApprove) {
|
||||
await this.diffViewProvider.revertChanges()
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2101,7 +2123,7 @@ export class Task {
|
||||
await handleError("writing file", error)
|
||||
await this.diffViewProvider.revertChanges()
|
||||
await this.diffViewProvider.reset()
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2130,7 +2152,7 @@ export class Task {
|
||||
if (!relPath) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("read_file", "path"))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
|
||||
@@ -2138,7 +2160,7 @@ export class Task {
|
||||
if (!accessAllowed) {
|
||||
await this.say("clineignore_error", relPath)
|
||||
pushToolResult(formatResponse.toolError(formatResponse.clineIgnoreError(relPath)))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
|
||||
@@ -2161,6 +2183,7 @@ export class Task {
|
||||
this.removeLastPartialMessageIfExistsWithType("say", "tool")
|
||||
const didApprove = await askApproval("tool", completeMessage)
|
||||
if (!didApprove) {
|
||||
await this.saveCheckpoint()
|
||||
telemetryService.captureToolUsage(this.taskId, block.name, false, false)
|
||||
break
|
||||
}
|
||||
@@ -2173,12 +2196,12 @@ export class Task {
|
||||
await this.fileContextTracker.trackFileContext(relPath, "read_tool")
|
||||
|
||||
pushToolResult(content)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("reading file", error)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2209,7 +2232,7 @@ export class Task {
|
||||
if (!relDirPath) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("list_files", "path"))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
@@ -2242,17 +2265,18 @@ export class Task {
|
||||
const didApprove = await askApproval("tool", completeMessage)
|
||||
if (!didApprove) {
|
||||
telemetryService.captureToolUsage(this.taskId, block.name, false, false)
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
telemetryService.captureToolUsage(this.taskId, block.name, false, true)
|
||||
}
|
||||
pushToolResult(result)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("listing files", error)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2281,7 +2305,7 @@ export class Task {
|
||||
if (!relDirPath) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("list_code_definition_names", "path"))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
|
||||
@@ -2311,17 +2335,18 @@ export class Task {
|
||||
const didApprove = await askApproval("tool", completeMessage)
|
||||
if (!didApprove) {
|
||||
telemetryService.captureToolUsage(this.taskId, block.name, false, false)
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
telemetryService.captureToolUsage(this.taskId, block.name, false, true)
|
||||
}
|
||||
pushToolResult(result)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("parsing source code definitions", error)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2354,13 +2379,13 @@ export class Task {
|
||||
if (!relDirPath) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("search_files", "path"))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
if (!regex) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("search_files", "regex"))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
@@ -2392,17 +2417,18 @@ export class Task {
|
||||
const didApprove = await askApproval("tool", completeMessage)
|
||||
if (!didApprove) {
|
||||
telemetryService.captureToolUsage(this.taskId, block.name, false, false)
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
telemetryService.captureToolUsage(this.taskId, block.name, false, true)
|
||||
}
|
||||
pushToolResult(results)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("searching files", error)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2418,6 +2444,7 @@ export class Task {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("browser_action", "action"))
|
||||
await this.browserSession.closeBrowser()
|
||||
await this.saveCheckpoint()
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -2461,7 +2488,7 @@ export class Task {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("browser_action", "url"))
|
||||
await this.browserSession.closeBrowser()
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
@@ -2477,6 +2504,7 @@ export class Task {
|
||||
this.removeLastPartialMessageIfExistsWithType("say", "browser_action_launch")
|
||||
const didApprove = await askApproval("browser_action_launch", url)
|
||||
if (!didApprove) {
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2502,7 +2530,7 @@ export class Task {
|
||||
await this.sayAndCreateMissingParamError("browser_action", "coordinate"),
|
||||
)
|
||||
await this.browserSession.closeBrowser()
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break // can't be within an inner switch
|
||||
}
|
||||
}
|
||||
@@ -2511,7 +2539,7 @@ export class Task {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("browser_action", "text"))
|
||||
await this.browserSession.closeBrowser()
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2560,7 +2588,7 @@ export class Task {
|
||||
browserActionResult.screenshot ? [browserActionResult.screenshot] : [],
|
||||
),
|
||||
)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
case "close":
|
||||
pushToolResult(
|
||||
@@ -2568,7 +2596,7 @@ export class Task {
|
||||
`The browser has been closed. You may now proceed to using other tools.`,
|
||||
),
|
||||
)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
|
||||
@@ -2577,7 +2605,7 @@ export class Task {
|
||||
} catch (error) {
|
||||
await this.browserSession.closeBrowser() // if any error occurs, the browser session is terminated
|
||||
await handleError("executing browser action", error)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2605,7 +2633,7 @@ export class Task {
|
||||
if (!command) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("execute_command", "command"))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
if (!requiresApprovalRaw) {
|
||||
@@ -2613,7 +2641,7 @@ export class Task {
|
||||
pushToolResult(
|
||||
await this.sayAndCreateMissingParamError("execute_command", "requires_approval"),
|
||||
)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
@@ -2629,7 +2657,7 @@ export class Task {
|
||||
pushToolResult(
|
||||
formatResponse.toolError(formatResponse.clineIgnoreError(ignoredFileAttemptedToAccess)),
|
||||
)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
|
||||
@@ -2661,6 +2689,7 @@ export class Task {
|
||||
`${this.shouldAutoApproveTool(block.name) && requiresApprovalPerLLM ? COMMAND_REQ_APP_STRING : ""}`, // ugly hack until we refactor combineCommandSequences
|
||||
)
|
||||
if (!didApprove) {
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2696,7 +2725,7 @@ export class Task {
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("executing command", error)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2726,13 +2755,13 @@ export class Task {
|
||||
if (!server_name) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("use_mcp_tool", "server_name"))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
if (!tool_name) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("use_mcp_tool", "tool_name"))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
// arguments are optional, but if they are provided they must be valid JSON
|
||||
@@ -2756,7 +2785,7 @@ export class Task {
|
||||
formatResponse.invalidMcpToolArgumentError(server_name, tool_name),
|
||||
),
|
||||
)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2783,6 +2812,7 @@ export class Task {
|
||||
this.removeLastPartialMessageIfExistsWithType("say", "use_mcp_server")
|
||||
const didApprove = await askApproval("use_mcp_server", completeMessage)
|
||||
if (!didApprove) {
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2834,7 +2864,7 @@ export class Task {
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("executing MCP tool", error)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2862,13 +2892,13 @@ export class Task {
|
||||
if (!server_name) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("access_mcp_resource", "server_name"))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
if (!uri) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("access_mcp_resource", "uri"))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
@@ -2889,6 +2919,7 @@ export class Task {
|
||||
this.removeLastPartialMessageIfExistsWithType("say", "use_mcp_server")
|
||||
const didApprove = await askApproval("use_mcp_server", completeMessage)
|
||||
if (!didApprove) {
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2908,12 +2939,12 @@ export class Task {
|
||||
.join("\n\n") || "(Empty response)"
|
||||
await this.say("mcp_server_response", resourceResultPretty)
|
||||
pushToolResult(formatResponse.toolResult(resourceResultPretty))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("accessing MCP resource", error)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2932,7 +2963,7 @@ export class Task {
|
||||
if (!question) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("ask_followup_question", "question"))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
@@ -2969,12 +3000,12 @@ export class Task {
|
||||
}
|
||||
|
||||
pushToolResult(formatResponse.toolResult(`<answer>\n${text}\n</answer>`, images))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("asking question", error)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2988,6 +3019,7 @@ export class Task {
|
||||
if (!context) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("new_task", "context"))
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
@@ -3016,10 +3048,12 @@ export class Task {
|
||||
formatResponse.toolResult(`The user has created a new task with the provided context.`),
|
||||
)
|
||||
}
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("creating new task", error)
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -3033,6 +3067,7 @@ export class Task {
|
||||
if (!context) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("condense", "context"))
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
@@ -3075,10 +3110,12 @@ export class Task {
|
||||
await ensureTaskDirectoryExists(this.getContext(), this.taskId),
|
||||
)
|
||||
}
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("condensing context window", error)
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -3139,6 +3176,7 @@ export class Task {
|
||||
if (text || images?.length) {
|
||||
telemetryService.captureOptionsIgnored(this.taskId, options.length, "plan")
|
||||
await this.say("user_feedback", text ?? "", images)
|
||||
await this.saveCheckpoint()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3285,12 +3323,14 @@ export class Task {
|
||||
// complete command message
|
||||
const didApprove = await askApproval("command", command)
|
||||
if (!didApprove) {
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
const [userRejected, execCommandResult] = await this.executeCommandTool(command!)
|
||||
if (userRejected) {
|
||||
this.didRejectTool = true
|
||||
pushToolResult(execCommandResult)
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
// user didn't reject, but the command may have output
|
||||
@@ -3309,6 +3349,7 @@ export class Task {
|
||||
break
|
||||
}
|
||||
await this.say("user_feedback", text ?? "", images)
|
||||
await this.saveCheckpoint()
|
||||
|
||||
const toolResults: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[] = []
|
||||
if (commandResult) {
|
||||
@@ -3337,7 +3378,7 @@ export class Task {
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("attempting completion", error)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -3439,9 +3480,6 @@ export class Task {
|
||||
|
||||
// Save checkpoint if this is the first API request
|
||||
const isFirstRequest = this.clineMessages.filter((m) => m.say === "api_req_started").length === 0
|
||||
if (isFirstRequest) {
|
||||
await this.say("checkpoint_created") // no hash since we need to wait for CheckpointTracker to be initialized
|
||||
}
|
||||
|
||||
// getting verbose details is an expensive operation, it uses globby to top-down build file structure of project which for large projects can take a few seconds
|
||||
// for the best UX we show a placeholder api_req_started message with a loading spinner as this happens
|
||||
@@ -3452,6 +3490,10 @@ 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 &&
|
||||
@@ -3482,7 +3524,16 @@ export class Task {
|
||||
}
|
||||
}
|
||||
|
||||
const [parsedUserContent, environmentDetails] = await this.loadContext(userContent, includeFileDetails)
|
||||
const [parsedUserContent, environmentDetails, clinerulesError] = await this.loadContext(userContent, includeFileDetails)
|
||||
|
||||
// error handling if the user uses the /newrule command & their .clinerules is a file, for file read operations didnt work properly
|
||||
if (clinerulesError === true) {
|
||||
await this.say(
|
||||
"error",
|
||||
"Issue with processing the /newrule command. Double check that, if '.clinerules' already exists, it's a directory and not a file. Otherwise there was an issue referencing this file/directory.",
|
||||
)
|
||||
}
|
||||
|
||||
userContent = parsedUserContent
|
||||
// add environment details as its own text block, separate from tool results
|
||||
userContent.push({ type: "text", text: environmentDetails })
|
||||
@@ -3767,11 +3818,14 @@ export class Task {
|
||||
}
|
||||
}
|
||||
|
||||
async loadContext(userContent: UserContent, includeFileDetails: boolean = false) {
|
||||
return await Promise.all([
|
||||
async loadContext(userContent: UserContent, includeFileDetails: boolean = false): Promise<[UserContent, string, boolean]> {
|
||||
// Track if we need to check clinerulesFile
|
||||
let needsClinerulesFileCheck = false
|
||||
|
||||
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)
|
||||
Promise.all(
|
||||
return await Promise.all(
|
||||
userContent.map(async (block) => {
|
||||
if (block.type === "text") {
|
||||
// We need to ensure any user generated content is wrapped in one of these tags so that we know to parse mentions
|
||||
@@ -3782,22 +3836,45 @@ export class Task {
|
||||
block.text.includes("<task>") ||
|
||||
block.text.includes("<user_message>")
|
||||
) {
|
||||
let parsedText = await parseMentions(block.text, cwd, this.urlContentFetcher, this.fileContextTracker)
|
||||
const parsedText = await parseMentions(
|
||||
block.text,
|
||||
cwd,
|
||||
this.urlContentFetcher,
|
||||
this.fileContextTracker,
|
||||
)
|
||||
|
||||
// when parsing slash commands, we still want to allow the user to provide their desired context
|
||||
parsedText = parseSlashCommands(parsedText)
|
||||
const { processedText, needsClinerulesFileCheck: needsCheck } = parseSlashCommands(parsedText)
|
||||
|
||||
if (needsCheck) {
|
||||
needsClinerulesFileCheck = true
|
||||
}
|
||||
|
||||
return {
|
||||
...block,
|
||||
text: parsedText,
|
||||
text: processedText,
|
||||
}
|
||||
}
|
||||
}
|
||||
return block
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Run initial promises in parallel
|
||||
const [processedUserContent, environmentDetails] = await Promise.all([
|
||||
processUserContent(),
|
||||
this.getEnvironmentDetails(includeFileDetails),
|
||||
])
|
||||
|
||||
// After processing content, check clinerulesData if needed
|
||||
let clinerulesError = false
|
||||
if (needsClinerulesFileCheck) {
|
||||
clinerulesError = await ensureLocalClinerulesDirExists(cwd)
|
||||
}
|
||||
|
||||
// Return all results
|
||||
return [processedUserContent, environmentDetails, clinerulesError]
|
||||
}
|
||||
|
||||
async getEnvironmentDetails(includeFileDetails: boolean = false) {
|
||||
|
||||
@@ -39,7 +39,7 @@ const terminalManager = new TerminalManager(context);
|
||||
const process = terminalManager.runCommand('npm install', '/path/to/project');
|
||||
|
||||
process.on('line', (line) => {
|
||||
console.log(line);
|
||||
console.log(line);
|
||||
});
|
||||
|
||||
// To wait for the process to complete naturally:
|
||||
@@ -93,6 +93,7 @@ export class TerminalManager {
|
||||
private terminalIds: Set<number> = new Set()
|
||||
private processes: Map<number, TerminalProcess> = new Map()
|
||||
private disposables: vscode.Disposable[] = []
|
||||
private shellIntegrationTimeout: number = 4000
|
||||
|
||||
constructor() {
|
||||
let disposable: vscode.Disposable | undefined
|
||||
@@ -144,13 +145,30 @@ export class TerminalManager {
|
||||
process.run(terminalInfo.terminal, command)
|
||||
} else {
|
||||
// docs recommend waiting 3s for shell integration to activate
|
||||
pWaitFor(() => terminalInfo.terminal.shellIntegration !== undefined, { timeout: 4000 }).finally(() => {
|
||||
const existingProcess = this.processes.get(terminalInfo.id)
|
||||
if (existingProcess && existingProcess.waitForShellIntegration) {
|
||||
existingProcess.waitForShellIntegration = false
|
||||
existingProcess.run(terminalInfo.terminal, command)
|
||||
}
|
||||
console.log(
|
||||
`[TerminalManager Test] Waiting for shell integration for terminal ${terminalInfo.id} with timeout ${this.shellIntegrationTimeout}ms`,
|
||||
)
|
||||
pWaitFor(() => terminalInfo.terminal.shellIntegration !== undefined, {
|
||||
timeout: this.shellIntegrationTimeout,
|
||||
})
|
||||
.then(() => {
|
||||
console.log(
|
||||
`[TerminalManager Test] Shell integration activated for terminal ${terminalInfo.id} within timeout.`,
|
||||
)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.warn(
|
||||
`[TerminalManager Test] Shell integration timed out or failed for terminal ${terminalInfo.id}: ${err.message}`,
|
||||
)
|
||||
})
|
||||
.finally(() => {
|
||||
console.log(`[TerminalManager Test] Proceeding with command execution for terminal ${terminalInfo.id}.`)
|
||||
const existingProcess = this.processes.get(terminalInfo.id)
|
||||
if (existingProcess && existingProcess.waitForShellIntegration) {
|
||||
existingProcess.waitForShellIntegration = false
|
||||
existingProcess.run(terminalInfo.terminal, command)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return mergePromise(process, promise)
|
||||
@@ -219,4 +237,8 @@ export class TerminalManager {
|
||||
this.disposables.forEach((disposable) => disposable.dispose())
|
||||
this.disposables = []
|
||||
}
|
||||
|
||||
setShellIntegrationTimeout(timeout: number): void {
|
||||
this.shellIntegrationTimeout = timeout
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { execa } from "execa"
|
||||
+22
-30
@@ -521,6 +521,21 @@ export class McpHub {
|
||||
this.isConnecting = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets sorted MCP servers based on the order defined in settings
|
||||
* @param serverOrder Array of server names in the order they appear in settings
|
||||
* @returns Array of McpServer objects sorted according to settings order
|
||||
*/
|
||||
private getSortedMcpServers(serverOrder: string[]): McpServer[] {
|
||||
return [...this.connections]
|
||||
.sort((a, b) => {
|
||||
const indexA = serverOrder.indexOf(a.server.name)
|
||||
const indexB = serverOrder.indexOf(b.server.name)
|
||||
return indexA - indexB
|
||||
})
|
||||
.map((connection) => connection.server)
|
||||
}
|
||||
|
||||
private async notifyWebviewOfServerChanges(): Promise<void> {
|
||||
// servers should always be sorted in the order they are defined in the settings file
|
||||
const settingsPath = await this.getMcpSettingsFilePath()
|
||||
@@ -529,13 +544,7 @@ export class McpHub {
|
||||
const serverOrder = Object.keys(config.mcpServers || {})
|
||||
await this.postMessageToWebview({
|
||||
type: "mcpServers",
|
||||
mcpServers: [...this.connections]
|
||||
.sort((a, b) => {
|
||||
const indexA = serverOrder.indexOf(a.server.name)
|
||||
const indexB = serverOrder.indexOf(b.server.name)
|
||||
return indexA - indexB
|
||||
})
|
||||
.map((connection) => connection.server),
|
||||
mcpServers: this.getSortedMcpServers(serverOrder),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -566,16 +575,7 @@ export class McpHub {
|
||||
}
|
||||
|
||||
const serverOrder = Object.keys(config.mcpServers || {})
|
||||
|
||||
const mcpServers = [...this.connections]
|
||||
.sort((a, b) => {
|
||||
const indexA = serverOrder.indexOf(a.server.name)
|
||||
const indexB = serverOrder.indexOf(b.server.name)
|
||||
return indexA - indexB
|
||||
})
|
||||
.map((connection) => connection.server)
|
||||
|
||||
return mcpServers
|
||||
return this.getSortedMcpServers(serverOrder)
|
||||
}
|
||||
console.error(`Server "${serverName}" not found in MCP configuration`)
|
||||
throw new Error(`Server "${serverName}" not found in MCP configuration`)
|
||||
@@ -691,7 +691,7 @@ export class McpHub {
|
||||
}
|
||||
}
|
||||
|
||||
public async addRemoteServer(serverName: string, serverUrl: string) {
|
||||
public async addRemoteServer(serverName: string, serverUrl: string): Promise<McpServer[]> {
|
||||
try {
|
||||
const settings = await this.readAndValidateMcpSettingsFile()
|
||||
if (!settings) {
|
||||
@@ -728,12 +728,12 @@ export class McpHub {
|
||||
JSON.stringify({ mcpServers: { ...settings.mcpServers, [serverName]: serverConfig } }, null, 2),
|
||||
)
|
||||
|
||||
await this.updateServerConnections(settings.mcpServers)
|
||||
await this.updateServerConnectionsRPC(settings.mcpServers)
|
||||
|
||||
vscode.window.showInformationMessage(`Added ${serverName} MCP server`)
|
||||
const serverOrder = Object.keys(settings.mcpServers || {})
|
||||
return this.getSortedMcpServers(serverOrder)
|
||||
} catch (error) {
|
||||
console.error("Failed to add remote MCP server:", error)
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -791,15 +791,7 @@ export class McpHub {
|
||||
await this.updateServerConnectionsRPC(config.mcpServers)
|
||||
|
||||
const serverOrder = Object.keys(config.mcpServers || {})
|
||||
const updatedMcpServers = [...this.connections]
|
||||
.sort((a, b) => {
|
||||
const indexA = serverOrder.indexOf(a.server.name)
|
||||
const indexB = serverOrder.indexOf(b.server.name)
|
||||
return indexA - indexB
|
||||
})
|
||||
.map((connection) => connection.server)
|
||||
|
||||
return updatedMcpServers
|
||||
return this.getSortedMcpServers(serverOrder)
|
||||
} catch (error) {
|
||||
console.error("Failed to update server timeout:", error)
|
||||
if (error instanceof Error) {
|
||||
|
||||
@@ -37,7 +37,6 @@ export interface ExtensionMessage {
|
||||
| "openGraphData"
|
||||
| "isImageUrlResult"
|
||||
| "didUpdateSettings"
|
||||
| "addRemoteServerResult"
|
||||
| "userCreditsBalance"
|
||||
| "userCreditsUsage"
|
||||
| "userCreditsPayments"
|
||||
@@ -103,11 +102,6 @@ export interface ExtensionMessage {
|
||||
type: "file" | "folder"
|
||||
label?: string
|
||||
}>
|
||||
addRemoteServerResult?: {
|
||||
success: boolean
|
||||
serverName: string
|
||||
error?: string
|
||||
}
|
||||
tab?: McpViewTab
|
||||
grpc_response?: {
|
||||
message?: any // JSON serialized protobuf message
|
||||
@@ -138,6 +132,7 @@ export interface ExtensionState {
|
||||
shouldShowAnnouncement: boolean
|
||||
taskHistory: HistoryItem[]
|
||||
telemetrySetting: TelemetrySetting
|
||||
shellIntegrationTimeout: number
|
||||
uriScheme?: string
|
||||
userInfo?: {
|
||||
displayName: string | null
|
||||
|
||||
@@ -9,7 +9,6 @@ import { McpViewTab } from "./mcp"
|
||||
|
||||
export interface WebviewMessage {
|
||||
type:
|
||||
| "addRemoteServer"
|
||||
| "apiConfiguration"
|
||||
| "webviewDidLaunch"
|
||||
| "newTask"
|
||||
@@ -51,7 +50,6 @@ export interface WebviewMessage {
|
||||
| "downloadMcp"
|
||||
| "silentlyRefreshMcpMarketplace"
|
||||
| "searchCommits"
|
||||
| "showMcpView"
|
||||
| "fetchLatestMcpServersFromHub"
|
||||
| "telemetrySetting"
|
||||
| "openSettings"
|
||||
@@ -73,6 +71,7 @@ export interface WebviewMessage {
|
||||
| "toggleClineRule"
|
||||
| "deleteClineRule"
|
||||
| "copyToClipboard"
|
||||
| "updateTerminalConnectionTimeout"
|
||||
|
||||
// | "relaunchChromeDebugMode"
|
||||
text?: string
|
||||
@@ -123,6 +122,7 @@ export interface WebviewMessage {
|
||||
filename?: string
|
||||
|
||||
offset?: number
|
||||
shellIntegrationTimeout?: number
|
||||
}
|
||||
|
||||
export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse"
|
||||
|
||||
@@ -65,6 +65,12 @@ export interface UpdateMcpTimeoutRequest {
|
||||
timeout: number
|
||||
}
|
||||
|
||||
export interface AddRemoteMcpServerRequest {
|
||||
metadata?: Metadata | undefined
|
||||
serverName: string
|
||||
serverUrl: string
|
||||
}
|
||||
|
||||
export interface McpTool {
|
||||
name: string
|
||||
description?: string | undefined
|
||||
@@ -288,6 +294,99 @@ export const UpdateMcpTimeoutRequest: MessageFns<UpdateMcpTimeoutRequest> = {
|
||||
},
|
||||
}
|
||||
|
||||
function createBaseAddRemoteMcpServerRequest(): AddRemoteMcpServerRequest {
|
||||
return { metadata: undefined, serverName: "", serverUrl: "" }
|
||||
}
|
||||
|
||||
export const AddRemoteMcpServerRequest: MessageFns<AddRemoteMcpServerRequest> = {
|
||||
encode(message: AddRemoteMcpServerRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.metadata !== undefined) {
|
||||
Metadata.encode(message.metadata, writer.uint32(10).fork()).join()
|
||||
}
|
||||
if (message.serverName !== "") {
|
||||
writer.uint32(18).string(message.serverName)
|
||||
}
|
||||
if (message.serverUrl !== "") {
|
||||
writer.uint32(26).string(message.serverUrl)
|
||||
}
|
||||
return writer
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): AddRemoteMcpServerRequest {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
|
||||
let end = length === undefined ? reader.len : reader.pos + length
|
||||
const message = createBaseAddRemoteMcpServerRequest()
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32()
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break
|
||||
}
|
||||
|
||||
message.metadata = Metadata.decode(reader, reader.uint32())
|
||||
continue
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 18) {
|
||||
break
|
||||
}
|
||||
|
||||
message.serverName = reader.string()
|
||||
continue
|
||||
}
|
||||
case 3: {
|
||||
if (tag !== 26) {
|
||||
break
|
||||
}
|
||||
|
||||
message.serverUrl = reader.string()
|
||||
continue
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break
|
||||
}
|
||||
reader.skip(tag & 7)
|
||||
}
|
||||
return message
|
||||
},
|
||||
|
||||
fromJSON(object: any): AddRemoteMcpServerRequest {
|
||||
return {
|
||||
metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined,
|
||||
serverName: isSet(object.serverName) ? globalThis.String(object.serverName) : "",
|
||||
serverUrl: isSet(object.serverUrl) ? globalThis.String(object.serverUrl) : "",
|
||||
}
|
||||
},
|
||||
|
||||
toJSON(message: AddRemoteMcpServerRequest): unknown {
|
||||
const obj: any = {}
|
||||
if (message.metadata !== undefined) {
|
||||
obj.metadata = Metadata.toJSON(message.metadata)
|
||||
}
|
||||
if (message.serverName !== "") {
|
||||
obj.serverName = message.serverName
|
||||
}
|
||||
if (message.serverUrl !== "") {
|
||||
obj.serverUrl = message.serverUrl
|
||||
}
|
||||
return obj
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<AddRemoteMcpServerRequest>, I>>(base?: I): AddRemoteMcpServerRequest {
|
||||
return AddRemoteMcpServerRequest.fromPartial(base ?? ({} as any))
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<AddRemoteMcpServerRequest>, I>>(object: I): AddRemoteMcpServerRequest {
|
||||
const message = createBaseAddRemoteMcpServerRequest()
|
||||
message.metadata =
|
||||
object.metadata !== undefined && object.metadata !== null ? Metadata.fromPartial(object.metadata) : undefined
|
||||
message.serverName = object.serverName ?? ""
|
||||
message.serverUrl = object.serverUrl ?? ""
|
||||
return message
|
||||
},
|
||||
}
|
||||
|
||||
function createBaseMcpTool(): McpTool {
|
||||
return { name: "", description: undefined, inputSchema: undefined, autoApprove: undefined }
|
||||
}
|
||||
@@ -897,6 +996,14 @@ export const McpServiceDefinition = {
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
addRemoteMcpServer: {
|
||||
name: "addRemoteMcpServer",
|
||||
requestType: AddRemoteMcpServerRequest,
|
||||
requestStream: false,
|
||||
responseType: McpServers,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
} as const
|
||||
|
||||
|
||||
+30
-2
@@ -30,6 +30,15 @@ async function checkGitInstalled(): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
async function checkGitRepoHasCommits(cwd: string): Promise<boolean> {
|
||||
try {
|
||||
await execAsync("git rev-parse HEAD", { cwd })
|
||||
return true
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function searchCommits(query: string, cwd: string): Promise<GitCommit[]> {
|
||||
try {
|
||||
const isInstalled = await checkGitInstalled()
|
||||
@@ -44,6 +53,12 @@ export async function searchCommits(query: string, cwd: string): Promise<GitComm
|
||||
return []
|
||||
}
|
||||
|
||||
// Check if repo has any commits
|
||||
if (!(await checkGitRepoHasCommits(cwd))) {
|
||||
// No commits yet in the repository
|
||||
return []
|
||||
}
|
||||
|
||||
// Search commits by hash or message, limiting to 10 results
|
||||
const { stdout } = await execAsync(
|
||||
`git log -n 10 --format="%H%n%h%n%s%n%an%n%ad" --date=short ` + `--grep="${query}" --regexp-ignore-case`,
|
||||
@@ -100,6 +115,11 @@ export async function getCommitInfo(hash: string, cwd: string): Promise<string>
|
||||
return "Not a git repository"
|
||||
}
|
||||
|
||||
// Check if repo has any commits
|
||||
if (!(await checkGitRepoHasCommits(cwd))) {
|
||||
return "Repository has no commits yet"
|
||||
}
|
||||
|
||||
// Get commit info, stats, and diff separately
|
||||
const { stdout: info } = await execAsync(`git show --format="%H%n%h%n%s%n%an%n%ad%n%b" --no-patch ${hash}`, {
|
||||
cwd,
|
||||
@@ -147,8 +167,16 @@ export async function getWorkingState(cwd: string): Promise<string> {
|
||||
return "No changes in working directory"
|
||||
}
|
||||
|
||||
// Get all changes (both staged and unstaged) compared to HEAD
|
||||
const { stdout: diff } = await execAsync("git diff HEAD", { cwd })
|
||||
// Check if repo has any commits before trying to diff against HEAD
|
||||
let diff = ""
|
||||
if (await checkGitRepoHasCommits(cwd)) {
|
||||
// Only run git diff if there are commits
|
||||
const { stdout: diffOutput } = await execAsync("git diff HEAD", { cwd })
|
||||
diff = diffOutput
|
||||
} else {
|
||||
// No commits yet, use status output only
|
||||
return `Working directory changes (new repository):\n\n${status}`
|
||||
}
|
||||
const output = `Working directory changes:\n\n${status}\n\n${diff}`.trim()
|
||||
return truncateOutput(output)
|
||||
} catch (error) {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
const tsConfigPaths = require("tsconfig-paths")
|
||||
const fs = require("fs")
|
||||
|
||||
const tsConfig = JSON.parse(fs.readFileSync("./tsconfig.json", "utf-8"))
|
||||
|
||||
/**
|
||||
* The aliases point towards the `src` directory.
|
||||
* However, `tsc` doesn't compile paths by itself
|
||||
* (https://www.typescriptlang.org/docs/handbook/modules/reference.html#paths-does-not-affect-emit)
|
||||
* So we need to use tsconfig-paths to resolve the aliases when running tests,
|
||||
* but pointing to `out` instead.
|
||||
*/
|
||||
const outPaths = {}
|
||||
Object.keys(tsConfig.compilerOptions.paths).forEach((key) => {
|
||||
const value = tsConfig.compilerOptions.paths[key]
|
||||
outPaths[key] = value.map((path) => path.replace("src", "out"))
|
||||
})
|
||||
|
||||
tsConfigPaths.register({
|
||||
baseUrl: ".",
|
||||
paths: outPaths,
|
||||
})
|
||||
+2
-1
@@ -27,7 +27,8 @@
|
||||
"@integrations/*": ["src/integrations/*"],
|
||||
"@services/*": ["src/services/*"],
|
||||
"@shared/*": ["src/shared/*"],
|
||||
"@utils/*": ["src/utils/*"]
|
||||
"@utils/*": ["src/utils/*"],
|
||||
"@packages/*": ["src/packages/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*", "scripts/**/*"],
|
||||
|
||||
Generated
+4295
-162
File diff suppressed because it is too large
Load Diff
@@ -15,11 +15,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "^0.27.4",
|
||||
"@heroui/react": "^2.8.0-beta.2",
|
||||
"@vscode/webview-ui-toolkit": "^1.4.0",
|
||||
"debounce": "^2.1.1",
|
||||
"dompurify": "^3.2.4",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"firebase": "^11.3.0",
|
||||
"framer-motion": "^12.7.4",
|
||||
"fuse.js": "^7.0.0",
|
||||
"fzf": "^0.5.2",
|
||||
"mermaid": "^11.4.1",
|
||||
@@ -42,7 +44,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.17.0",
|
||||
"@tailwindcss/vite": "^4.0.12",
|
||||
"@tailwindcss/vite": "^4.1.4",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
@@ -60,10 +62,10 @@
|
||||
"eslint-plugin-react-refresh": "^0.4.16",
|
||||
"globals": "^15.14.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"tailwindcss": "^4.0.12",
|
||||
"tailwindcss": "^4.1.4",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.18.2",
|
||||
"vite": "^6.2.6",
|
||||
"vite": "^6.3.4",
|
||||
"vitest": "^3.0.5"
|
||||
}
|
||||
}
|
||||
|
||||
+60
-55
@@ -6,64 +6,71 @@ import HistoryView from "./components/history/HistoryView"
|
||||
import SettingsView from "./components/settings/SettingsView"
|
||||
import WelcomeView from "./components/welcome/WelcomeView"
|
||||
import AccountView from "./components/account/AccountView"
|
||||
import { ExtensionStateContextProvider, useExtensionState } from "./context/ExtensionStateContext"
|
||||
import { FirebaseAuthProvider } from "./context/FirebaseAuthContext"
|
||||
import { useExtensionState } from "./context/ExtensionStateContext"
|
||||
import { vscode } from "./utils/vscode"
|
||||
import McpView from "./components/mcp/configuration/McpConfigurationView"
|
||||
import { McpViewTab } from "@shared/mcp"
|
||||
import { Providers } from "./Providers"
|
||||
|
||||
const AppContent = () => {
|
||||
const { didHydrateState, showWelcome, shouldShowAnnouncement, telemetrySetting, vscMachineId } = useExtensionState()
|
||||
const { didHydrateState, showWelcome, shouldShowAnnouncement, showMcp, mcpTab } = useExtensionState()
|
||||
const [showSettings, setShowSettings] = useState(false)
|
||||
const hideSettings = useCallback(() => setShowSettings(false), [])
|
||||
const [showHistory, setShowHistory] = useState(false)
|
||||
const [showMcp, setShowMcp] = useState(false)
|
||||
const [showAccount, setShowAccount] = useState(false)
|
||||
const [showAnnouncement, setShowAnnouncement] = useState(false)
|
||||
const [mcpTab, setMcpTab] = useState<McpViewTab | undefined>(undefined)
|
||||
|
||||
const handleMessage = useCallback((e: MessageEvent) => {
|
||||
const message: ExtensionMessage = e.data
|
||||
switch (message.type) {
|
||||
case "action":
|
||||
switch (message.action!) {
|
||||
case "settingsButtonClicked":
|
||||
setShowSettings(true)
|
||||
setShowHistory(false)
|
||||
setShowMcp(false)
|
||||
setShowAccount(false)
|
||||
break
|
||||
case "historyButtonClicked":
|
||||
setShowSettings(false)
|
||||
setShowHistory(true)
|
||||
setShowMcp(false)
|
||||
setShowAccount(false)
|
||||
break
|
||||
case "mcpButtonClicked":
|
||||
setShowSettings(false)
|
||||
setShowHistory(false)
|
||||
if (message.tab) {
|
||||
setMcpTab(message.tab)
|
||||
}
|
||||
setShowMcp(true)
|
||||
setShowAccount(false)
|
||||
break
|
||||
case "accountButtonClicked":
|
||||
setShowSettings(false)
|
||||
setShowHistory(false)
|
||||
setShowMcp(false)
|
||||
setShowAccount(true)
|
||||
break
|
||||
case "chatButtonClicked":
|
||||
setShowSettings(false)
|
||||
setShowHistory(false)
|
||||
setShowMcp(false)
|
||||
setShowAccount(false)
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
}, [])
|
||||
const { setShowMcp, setMcpTab } = useExtensionState()
|
||||
|
||||
const closeMcpView = useCallback(() => {
|
||||
setShowMcp(false)
|
||||
setMcpTab(undefined)
|
||||
}, [setShowMcp, setMcpTab])
|
||||
|
||||
const handleMessage = useCallback(
|
||||
(e: MessageEvent) => {
|
||||
const message: ExtensionMessage = e.data
|
||||
switch (message.type) {
|
||||
case "action":
|
||||
switch (message.action!) {
|
||||
case "settingsButtonClicked":
|
||||
setShowSettings(true)
|
||||
setShowHistory(false)
|
||||
closeMcpView()
|
||||
setShowAccount(false)
|
||||
break
|
||||
case "historyButtonClicked":
|
||||
setShowSettings(false)
|
||||
setShowHistory(true)
|
||||
closeMcpView()
|
||||
setShowAccount(false)
|
||||
break
|
||||
case "mcpButtonClicked":
|
||||
setShowSettings(false)
|
||||
setShowHistory(false)
|
||||
if (message.tab) {
|
||||
setMcpTab(message.tab)
|
||||
}
|
||||
setShowMcp(true)
|
||||
setShowAccount(false)
|
||||
break
|
||||
case "accountButtonClicked":
|
||||
setShowSettings(false)
|
||||
setShowHistory(false)
|
||||
closeMcpView()
|
||||
setShowAccount(true)
|
||||
break
|
||||
case "chatButtonClicked":
|
||||
setShowSettings(false)
|
||||
setShowHistory(false)
|
||||
closeMcpView()
|
||||
setShowAccount(false)
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
},
|
||||
[setShowMcp, setMcpTab, closeMcpView],
|
||||
)
|
||||
|
||||
useEvent("message", handleMessage)
|
||||
|
||||
@@ -95,13 +102,13 @@ const AppContent = () => {
|
||||
<>
|
||||
{showSettings && <SettingsView onDone={hideSettings} />}
|
||||
{showHistory && <HistoryView onDone={() => setShowHistory(false)} />}
|
||||
{showMcp && <McpView initialTab={mcpTab} onDone={() => setShowMcp(false)} />}
|
||||
{showMcp && <McpView initialTab={mcpTab} onDone={closeMcpView} />}
|
||||
{showAccount && <AccountView onDone={() => setShowAccount(false)} />}
|
||||
{/* Do not conditionally load ChatView, it's expensive and there's state we don't want to lose (user input, disableInput, askResponse promise, etc.) */}
|
||||
<ChatView
|
||||
showHistoryView={() => {
|
||||
setShowSettings(false)
|
||||
setShowMcp(false)
|
||||
closeMcpView()
|
||||
setShowAccount(false)
|
||||
setShowHistory(true)
|
||||
}}
|
||||
@@ -119,11 +126,9 @@ const AppContent = () => {
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<ExtensionStateContextProvider>
|
||||
<FirebaseAuthProvider>
|
||||
<AppContent />
|
||||
</FirebaseAuthProvider>
|
||||
</ExtensionStateContextProvider>
|
||||
<Providers>
|
||||
<AppContent />
|
||||
</Providers>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { ReactNode } from "react"
|
||||
|
||||
import { ExtensionStateContextProvider } from "./context/ExtensionStateContext"
|
||||
import { FirebaseAuthProvider } from "./context/FirebaseAuthContext"
|
||||
import { HeroUIProvider } from "@heroui/react"
|
||||
|
||||
export function Providers({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<ExtensionStateContextProvider>
|
||||
<FirebaseAuthProvider>
|
||||
<HeroUIProvider>{children}</HeroUIProvider>
|
||||
</FirebaseAuthProvider>
|
||||
</ExtensionStateContextProvider>
|
||||
)
|
||||
}
|
||||
@@ -237,6 +237,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
) => {
|
||||
const { filePaths, chatSettings, apiConfiguration, openRouterModels, platform } = useExtensionState()
|
||||
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false)
|
||||
const [isDraggingOver, setIsDraggingOver] = useState(false)
|
||||
const [gitCommits, setGitCommits] = useState<GitCommit[]>([])
|
||||
|
||||
const [showSlashCommandsMenu, setShowSlashCommandsMenu] = useState(false)
|
||||
@@ -266,6 +267,10 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
const [menuPosition, setMenuPosition] = useState(0)
|
||||
const [shownTooltipMode, setShownTooltipMode] = useState<ChatSettings["mode"] | null>(null)
|
||||
const [pendingInsertions, setPendingInsertions] = useState<string[]>([])
|
||||
const [showShiftDragTip, setShowShiftDragTip] = useState(false)
|
||||
const shiftHoldTimerRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const [showUnsupportedFileError, setShowUnsupportedFileError] = useState(false)
|
||||
const unsupportedFileTimerRef = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
const [fileSearchResults, setFileSearchResults] = useState<SearchResult[]>([])
|
||||
const [searchLoading, setSearchLoading] = useState(false)
|
||||
@@ -1016,6 +1021,84 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
}
|
||||
}, [showModelSelector])
|
||||
|
||||
// Effect for Shift key hold detection
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Shift" && !event.repeat) {
|
||||
// Start timer only if Shift is pressed and not already held down
|
||||
if (shiftHoldTimerRef.current === null) {
|
||||
shiftHoldTimerRef.current = setTimeout(() => {
|
||||
setShowShiftDragTip(true)
|
||||
}, 250) // 250ms delay
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyUp = (event: KeyboardEvent) => {
|
||||
if (event.key === "Shift") {
|
||||
// Clear timer and hide tip when Shift is released
|
||||
if (shiftHoldTimerRef.current !== null) {
|
||||
clearTimeout(shiftHoldTimerRef.current)
|
||||
shiftHoldTimerRef.current = null
|
||||
}
|
||||
setShowShiftDragTip(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Add listeners
|
||||
window.addEventListener("keydown", handleKeyDown)
|
||||
window.addEventListener("keyup", handleKeyUp)
|
||||
|
||||
// Cleanup listeners on component unmount
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown)
|
||||
window.removeEventListener("keyup", handleKeyUp)
|
||||
// Clear any running timer on unmount
|
||||
if (shiftHoldTimerRef.current !== null) {
|
||||
clearTimeout(shiftHoldTimerRef.current)
|
||||
}
|
||||
}
|
||||
}, []) // Empty dependency array ensures this runs only once on mount/unmount
|
||||
|
||||
// Function to show error message for unsupported files for drag and drop
|
||||
const showUnsupportedFileErrorMessage = () => {
|
||||
// Show error message for unsupported files
|
||||
setShowUnsupportedFileError(true)
|
||||
|
||||
// Clear any existing timer
|
||||
if (unsupportedFileTimerRef.current) {
|
||||
clearTimeout(unsupportedFileTimerRef.current)
|
||||
}
|
||||
|
||||
// Set timer to hide error after 3 seconds
|
||||
unsupportedFileTimerRef.current = setTimeout(() => {
|
||||
setShowUnsupportedFileError(false)
|
||||
unsupportedFileTimerRef.current = null
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
const handleDragEnter = (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
setIsDraggingOver(true)
|
||||
|
||||
// Check if files are being dragged
|
||||
if (e.dataTransfer.types.includes("Files")) {
|
||||
// Check if any of the files are not images
|
||||
const items = Array.from(e.dataTransfer.items)
|
||||
const hasNonImageFile = items.some((item) => {
|
||||
if (item.kind === "file") {
|
||||
const type = item.type.split("/")[0]
|
||||
return type !== "image"
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
if (hasNonImageFile) {
|
||||
showUnsupportedFileErrorMessage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the drag over event to allow dropping.
|
||||
* Prevents the default behavior to enable drop.
|
||||
@@ -1024,8 +1107,37 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
*/
|
||||
const onDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
// Ensure state remains true if dragging continues over the element
|
||||
if (!isDraggingOver) {
|
||||
setIsDraggingOver(true)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDragLeave = (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
// Check if the related target is still within the drop zone; prevents flickering
|
||||
const dropZone = e.currentTarget as HTMLElement
|
||||
if (!dropZone.contains(e.relatedTarget as Node)) {
|
||||
setIsDraggingOver(false)
|
||||
// Don't clear the error message here, let it time out naturally
|
||||
}
|
||||
}
|
||||
|
||||
// Effect to detect when drag operation ends outside the component
|
||||
useEffect(() => {
|
||||
const handleGlobalDragEnd = () => {
|
||||
// This will be triggered when the drag operation ends anywhere
|
||||
setIsDraggingOver(false)
|
||||
// Don't clear error message, let it time out naturally
|
||||
}
|
||||
|
||||
document.addEventListener("dragend", handleGlobalDragEnd)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("dragend", handleGlobalDragEnd)
|
||||
}
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* Handles the drop event for files and text.
|
||||
* Processes dropped images and text, updating the state accordingly.
|
||||
@@ -1034,6 +1146,14 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
*/
|
||||
const onDrop = async (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
setIsDraggingOver(false) // Reset state on drop
|
||||
|
||||
// Clear any error message when something is actually dropped
|
||||
setShowUnsupportedFileError(false)
|
||||
if (unsupportedFileTimerRef.current) {
|
||||
clearTimeout(unsupportedFileTimerRef.current)
|
||||
unsupportedFileTimerRef.current = null
|
||||
}
|
||||
|
||||
// --- 1. VSCode Explorer Drop Handling ---
|
||||
let uris: string[] = []
|
||||
@@ -1153,9 +1273,37 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
opacity: textAreaDisabled ? 0.5 : 1,
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
// Drag-over styles moved to DynamicTextArea
|
||||
transition: "background-color 0.1s ease-in-out, border 0.1s ease-in-out",
|
||||
}}
|
||||
onDrop={onDrop}
|
||||
onDragOver={onDragOver}>
|
||||
onDragOver={onDragOver}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}>
|
||||
{showUnsupportedFileError && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: "10px 15px",
|
||||
backgroundColor: "rgba(var(--vscode-errorForeground-rgb), 0.1)",
|
||||
border: "2px solid var(--vscode-errorForeground)",
|
||||
borderRadius: 2,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
zIndex: 10,
|
||||
pointerEvents: "none",
|
||||
}}>
|
||||
<span
|
||||
style={{
|
||||
color: "var(--vscode-errorForeground)",
|
||||
fontWeight: "bold",
|
||||
fontSize: "12px",
|
||||
}}>
|
||||
Only image files are supported
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{showSlashCommandsMenu && (
|
||||
<div ref={slashCommandsMenuContainerRef}>
|
||||
<SlashCommandMenu
|
||||
@@ -1250,7 +1398,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
}
|
||||
onHeightChange?.(height)
|
||||
}}
|
||||
placeholder={placeholderText}
|
||||
placeholder={showUnsupportedFileError ? "" : placeholderText}
|
||||
maxRows={10}
|
||||
autoFocus={true}
|
||||
style={{
|
||||
@@ -1282,9 +1430,13 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
cursor: textAreaDisabled ? "not-allowed" : undefined,
|
||||
flex: 1,
|
||||
zIndex: 1,
|
||||
outline: isTextAreaFocused
|
||||
? `1px solid ${chatSettings.mode === "plan" ? PLAN_MODE_COLOR : "var(--vscode-focusBorder)"}`
|
||||
: "none",
|
||||
outline:
|
||||
isDraggingOver && !showUnsupportedFileError // Only show drag outline if not showing error
|
||||
? "2px dashed var(--vscode-focusBorder)"
|
||||
: isTextAreaFocused
|
||||
? `1px solid ${chatSettings.mode === "plan" ? PLAN_MODE_COLOR : "var(--vscode-focusBorder)"}`
|
||||
: "none",
|
||||
outlineOffset: isDraggingOver && !showUnsupportedFileError ? "1px" : "0px", // Add offset for drag-over outline
|
||||
}}
|
||||
onScroll={() => updateHighlights()}
|
||||
/>
|
||||
@@ -1346,83 +1498,129 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
</div>
|
||||
|
||||
<ControlsContainer>
|
||||
<ButtonGroup>
|
||||
<Tooltip tipText="Add Context" style={{ left: 0 }}>
|
||||
<VSCodeButton
|
||||
data-testid="context-button"
|
||||
appearance="icon"
|
||||
aria-label="Add Context"
|
||||
disabled={textAreaDisabled}
|
||||
onClick={handleContextButtonClick}
|
||||
style={{ padding: "0px 0px", height: "20px" }}>
|
||||
<ButtonContainer>
|
||||
<span className="flex items-center" style={{ fontSize: "13px", marginBottom: 1 }}>
|
||||
@
|
||||
</span>
|
||||
{/* {showButtonText && <span style={{ fontSize: "10px" }}>Context</span>} */}
|
||||
</ButtonContainer>
|
||||
</VSCodeButton>
|
||||
</Tooltip>
|
||||
{/* Always render both components, but control visibility with CSS */}
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
height: "28px", // Fixed height to prevent container shrinking
|
||||
}}>
|
||||
{/* ButtonGroup - always in DOM but visibility controlled */}
|
||||
<ButtonGroup
|
||||
style={{
|
||||
opacity: showShiftDragTip ? 0 : 1,
|
||||
pointerEvents: showShiftDragTip ? "none" : "auto",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
transition: "opacity 0.3s ease-in-out",
|
||||
transitionDelay: showShiftDragTip ? "0s" : "0.2s",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
zIndex: showShiftDragTip ? 0 : 1,
|
||||
}}>
|
||||
<Tooltip tipText="Add Context" style={{ left: 0 }}>
|
||||
<VSCodeButton
|
||||
data-testid="context-button"
|
||||
appearance="icon"
|
||||
aria-label="Add Context"
|
||||
disabled={textAreaDisabled}
|
||||
onClick={handleContextButtonClick}
|
||||
style={{ padding: "0px 0px", height: "20px" }}>
|
||||
<ButtonContainer>
|
||||
<span className="flex items-center" style={{ fontSize: "13px", marginBottom: 1 }}>
|
||||
@
|
||||
</span>
|
||||
</ButtonContainer>
|
||||
</VSCodeButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip tipText="Add Images">
|
||||
<VSCodeButton
|
||||
data-testid="images-button"
|
||||
appearance="icon"
|
||||
aria-label="Add Images"
|
||||
disabled={shouldDisableImages}
|
||||
onClick={() => {
|
||||
if (!shouldDisableImages) {
|
||||
onSelectImages()
|
||||
}
|
||||
}}
|
||||
style={{ padding: "0px 0px", height: "20px" }}>
|
||||
<ButtonContainer>
|
||||
<span
|
||||
className="codicon codicon-device-camera flex items-center"
|
||||
style={{ fontSize: "14px", marginBottom: -3 }}
|
||||
/>
|
||||
{/* {showButtonText && <span style={{ fontSize: "10px" }}>Images</span>} */}
|
||||
</ButtonContainer>
|
||||
</VSCodeButton>
|
||||
</Tooltip>
|
||||
<ServersToggleModal />
|
||||
<ClineRulesToggleModal />
|
||||
<ModelContainer ref={modelSelectorRef}>
|
||||
<ModelButtonWrapper ref={buttonRef}>
|
||||
<ModelDisplayButton
|
||||
role="button"
|
||||
isActive={showModelSelector}
|
||||
disabled={false}
|
||||
title="Select Model / API Provider"
|
||||
onClick={handleModelButtonClick}
|
||||
// onKeyDown={(e) => {
|
||||
// if (e.key === "Enter" || e.key === " ") {
|
||||
// e.preventDefault()
|
||||
// handleModelButtonClick()
|
||||
// }
|
||||
// }}
|
||||
tabIndex={0}>
|
||||
<ModelButtonContent>{modelDisplayName}</ModelButtonContent>
|
||||
</ModelDisplayButton>
|
||||
</ModelButtonWrapper>
|
||||
{showModelSelector && (
|
||||
<ModelSelectorTooltip
|
||||
arrowPosition={arrowPosition}
|
||||
menuPosition={menuPosition}
|
||||
style={{
|
||||
bottom: `calc(100vh - ${menuPosition}px + 6px)`,
|
||||
}}>
|
||||
<ApiOptions
|
||||
showModelOptions={true}
|
||||
apiErrorMessage={undefined}
|
||||
modelIdErrorMessage={undefined}
|
||||
isPopup={true}
|
||||
saveImmediately={true} // Ensure popup saves immediately
|
||||
/>
|
||||
</ModelSelectorTooltip>
|
||||
)}
|
||||
</ModelContainer>
|
||||
</ButtonGroup>
|
||||
<Tooltip tipText="Add Images">
|
||||
<VSCodeButton
|
||||
data-testid="images-button"
|
||||
appearance="icon"
|
||||
aria-label="Add Images"
|
||||
disabled={shouldDisableImages}
|
||||
onClick={() => {
|
||||
if (!shouldDisableImages) {
|
||||
onSelectImages()
|
||||
}
|
||||
}}
|
||||
style={{ padding: "0px 0px", height: "20px" }}>
|
||||
<ButtonContainer>
|
||||
<span
|
||||
className="codicon codicon-device-camera flex items-center"
|
||||
style={{ fontSize: "14px", marginBottom: -3 }}
|
||||
/>
|
||||
</ButtonContainer>
|
||||
</VSCodeButton>
|
||||
</Tooltip>
|
||||
<ServersToggleModal />
|
||||
<ClineRulesToggleModal />
|
||||
<ModelContainer ref={modelSelectorRef}>
|
||||
<ModelButtonWrapper ref={buttonRef}>
|
||||
<ModelDisplayButton
|
||||
role="button"
|
||||
isActive={showModelSelector}
|
||||
disabled={false}
|
||||
title="Select Model / API Provider"
|
||||
onClick={handleModelButtonClick}
|
||||
tabIndex={0}>
|
||||
<ModelButtonContent>{modelDisplayName}</ModelButtonContent>
|
||||
</ModelDisplayButton>
|
||||
</ModelButtonWrapper>
|
||||
{showModelSelector && (
|
||||
<ModelSelectorTooltip
|
||||
arrowPosition={arrowPosition}
|
||||
menuPosition={menuPosition}
|
||||
style={{
|
||||
bottom: `calc(100vh - ${menuPosition}px + 6px)`,
|
||||
}}>
|
||||
<ApiOptions
|
||||
showModelOptions={true}
|
||||
apiErrorMessage={undefined}
|
||||
modelIdErrorMessage={undefined}
|
||||
isPopup={true}
|
||||
saveImmediately={true} // Ensure popup saves immediately
|
||||
/>
|
||||
</ModelSelectorTooltip>
|
||||
)}
|
||||
</ModelContainer>
|
||||
</ButtonGroup>
|
||||
|
||||
{/* Shift Tip - always in DOM but visibility controlled */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "flex-start", // Left align horizontally
|
||||
alignItems: "center", // Center vertically
|
||||
height: "100%", // Fill the container height
|
||||
padding: "4px 0", // Add padding to match button group height
|
||||
boxSizing: "border-box", // Include padding in height calculation
|
||||
opacity: showShiftDragTip ? 1 : 0,
|
||||
pointerEvents: showShiftDragTip ? "auto" : "none",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
transition: "opacity 0.3s ease-in-out",
|
||||
transitionDelay: showShiftDragTip ? "0.2s" : "0s",
|
||||
width: "100%",
|
||||
zIndex: showShiftDragTip ? 1 : 0,
|
||||
}}>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "10px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
whiteSpace: "nowrap",
|
||||
}}>
|
||||
Hold Shift to Drop Files
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* Tooltip for Plan/Act toggle remains outside the conditional rendering */}
|
||||
<Tooltip
|
||||
style={{ zIndex: 1000 }}
|
||||
visible={shownTooltipMode !== null}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useRef, useState, useEffect } from "react"
|
||||
import { useClickAway, useWindowSize } from "react-use"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { useNavigator } from "@/hooks/useNavigator"
|
||||
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
|
||||
import ServersToggleList from "@/components/mcp/configuration/tabs/installed/ServersToggleList"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
@@ -9,6 +10,7 @@ import Tooltip from "@/components/common/Tooltip"
|
||||
|
||||
const ServersToggleModal: React.FC = () => {
|
||||
const { mcpServers } = useExtensionState()
|
||||
const { navigateToMcp } = useNavigator()
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
const buttonRef = useRef<HTMLDivElement>(null)
|
||||
const modalRef = useRef<HTMLDivElement>(null)
|
||||
@@ -81,11 +83,8 @@ const ServersToggleModal: React.FC = () => {
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
onClick={() => {
|
||||
vscode.postMessage({
|
||||
type: "showMcpView",
|
||||
tab: "installed",
|
||||
})
|
||||
setIsVisible(false)
|
||||
navigateToMcp("installed")
|
||||
}}>
|
||||
<span className="codicon codicon-gear text-[10px]"></span>
|
||||
</VSCodeButton>
|
||||
|
||||
@@ -35,7 +35,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
||||
onClose,
|
||||
}) => {
|
||||
const { apiConfiguration, currentTaskItem, checkpointTrackerErrorMessage } = useExtensionState()
|
||||
const [isTaskExpanded, setIsTaskExpanded] = useState(false)
|
||||
const [isTaskExpanded, setIsTaskExpanded] = useState(true)
|
||||
const [isTextExpanded, setIsTextExpanded] = useState(false)
|
||||
const [showSeeMore, setShowSeeMore] = useState(false)
|
||||
const textContainerRef = useRef<HTMLDivElement>(null)
|
||||
@@ -138,6 +138,10 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
||||
const shouldShowPromptCacheInfo =
|
||||
doesModelSupportPromptCache && apiConfiguration?.apiProvider !== "openrouter" && apiConfiguration?.apiProvider !== "cline"
|
||||
|
||||
const shouldShowPromptCacheInfoClineOR =
|
||||
doesModelSupportPromptCache &&
|
||||
(apiConfiguration?.apiProvider === "openrouter" || apiConfiguration?.apiProvider === "cline")
|
||||
|
||||
const ContextWindowComponent = (
|
||||
<>
|
||||
{isTaskExpanded && contextWindow && (
|
||||
@@ -406,6 +410,33 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{shouldShowPromptCacheInfoClineOR && cacheReads !== undefined && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "4px",
|
||||
flexWrap: "wrap",
|
||||
}}>
|
||||
<span style={{ fontWeight: "bold" }}>Cache:</span>
|
||||
<span
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "3px",
|
||||
}}>
|
||||
<i
|
||||
className="codicon codicon-arrow-right"
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
fontWeight: "bold",
|
||||
marginBottom: 0,
|
||||
}}
|
||||
/>
|
||||
{formatLargeNumber(cacheReads || 0)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{shouldShowPromptCacheInfo &&
|
||||
(cacheReads !== undefined ||
|
||||
cacheWrites !== undefined ||
|
||||
|
||||
+28
-45
@@ -1,50 +1,20 @@
|
||||
import { useCallback, useRef, useState } from "react"
|
||||
import { useRef, useState } from "react"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { VSCodeButton, VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useEvent } from "react-use"
|
||||
import { LINKS } from "@/constants"
|
||||
import { McpServiceClient } from "@/services/grpc-client"
|
||||
import { convertProtoMcpServersToMcpServers } from "@shared/proto-conversions/mcp/mcp-server-conversion"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
|
||||
const AddRemoteServerForm = ({ onServerAdded }: { onServerAdded: () => void }) => {
|
||||
const [serverName, setServerName] = useState("")
|
||||
const [serverUrl, setServerUrl] = useState("")
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [error, setError] = useState("")
|
||||
const [showConnectingMessage, setShowConnectingMessage] = useState(false)
|
||||
const { setMcpServers } = useExtensionState()
|
||||
|
||||
// Store submitted values to check if the server was added
|
||||
const submittedValues = useRef<{ name: string } | null>(null)
|
||||
|
||||
const handleMessage = useCallback(
|
||||
(event: MessageEvent) => {
|
||||
const message = event.data
|
||||
|
||||
if (
|
||||
message.type === "addRemoteServerResult" &&
|
||||
isSubmitting &&
|
||||
submittedValues.current &&
|
||||
message.addRemoteServerResult?.serverName === submittedValues.current.name
|
||||
) {
|
||||
if (message.addRemoteServerResult.success) {
|
||||
// Handle success
|
||||
setIsSubmitting(false)
|
||||
setServerName("")
|
||||
setServerUrl("")
|
||||
submittedValues.current = null
|
||||
onServerAdded()
|
||||
setShowConnectingMessage(false)
|
||||
} else {
|
||||
// Handle error
|
||||
setIsSubmitting(false)
|
||||
setError(message.addRemoteServerResult.error || "Failed to add server")
|
||||
setShowConnectingMessage(false)
|
||||
}
|
||||
}
|
||||
},
|
||||
[isSubmitting, onServerAdded],
|
||||
)
|
||||
|
||||
useEvent("message", handleMessage)
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!serverName.trim()) {
|
||||
@@ -65,16 +35,29 @@ const AddRemoteServerForm = ({ onServerAdded }: { onServerAdded: () => void }) =
|
||||
}
|
||||
|
||||
setError("")
|
||||
|
||||
submittedValues.current = { name: serverName.trim() }
|
||||
|
||||
setIsSubmitting(true)
|
||||
setShowConnectingMessage(true)
|
||||
vscode.postMessage({
|
||||
type: "addRemoteServer",
|
||||
serverName: serverName.trim(),
|
||||
serverUrl: serverUrl.trim(),
|
||||
})
|
||||
|
||||
try {
|
||||
const servers = await McpServiceClient.addRemoteMcpServer({
|
||||
serverName: serverName.trim(),
|
||||
serverUrl: serverUrl.trim(),
|
||||
})
|
||||
|
||||
setIsSubmitting(false)
|
||||
|
||||
const mcpServers = convertProtoMcpServersToMcpServers(servers)
|
||||
setMcpServers(mcpServers)
|
||||
|
||||
setServerName("")
|
||||
setServerUrl("")
|
||||
onServerAdded()
|
||||
setShowConnectingMessage(false)
|
||||
} catch (error) {
|
||||
setIsSubmitting(false)
|
||||
setError(error instanceof Error ? error.message : "Failed to add server")
|
||||
setShowConnectingMessage(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -9,6 +9,7 @@ import { TabButton } from "../mcp/configuration/McpConfigurationView"
|
||||
import { useEvent } from "react-use"
|
||||
import { ExtensionMessage } from "@shared/ExtensionMessage"
|
||||
import BrowserSettingsSection from "./BrowserSettingsSection"
|
||||
import TerminalSettingsSection from "./TerminalSettingsSection"
|
||||
|
||||
const { IS_DEV } = process.env
|
||||
|
||||
@@ -240,6 +241,9 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
{/* Browser Settings Section */}
|
||||
<BrowserSettingsSection />
|
||||
|
||||
{/* Terminal Settings Section */}
|
||||
<TerminalSettingsSection />
|
||||
|
||||
<div className="mt-auto pr-2 flex justify-center">
|
||||
<SettingsButton
|
||||
onClick={() => vscode.postMessage({ type: "openExtensionSettings" })}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import React, { useState } from "react"
|
||||
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
|
||||
export const TerminalSettingsSection: React.FC = () => {
|
||||
const { shellIntegrationTimeout, setShellIntegrationTimeout } = useExtensionState()
|
||||
const [inputValue, setInputValue] = useState((shellIntegrationTimeout / 1000).toString())
|
||||
const [inputError, setInputError] = useState<string | null>(null)
|
||||
|
||||
const handleTimeoutChange = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
const value = target.value
|
||||
|
||||
setInputValue(value)
|
||||
|
||||
const seconds = parseFloat(value)
|
||||
if (isNaN(seconds) || seconds <= 0) {
|
||||
setInputError("Please enter a positive number")
|
||||
return
|
||||
}
|
||||
|
||||
setInputError(null)
|
||||
const timeout = Math.round(seconds * 1000) // Convert to milliseconds
|
||||
|
||||
// Update local state
|
||||
setShellIntegrationTimeout(timeout)
|
||||
|
||||
// Send to extension
|
||||
vscode.postMessage({
|
||||
type: "updateTerminalConnectionTimeout",
|
||||
shellIntegrationTimeout: timeout,
|
||||
})
|
||||
}
|
||||
|
||||
const handleInputBlur = () => {
|
||||
// If there was an error, reset the input to the current valid value
|
||||
if (inputError) {
|
||||
setInputValue((shellIntegrationTimeout / 1000).toString())
|
||||
setInputError(null)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
id="terminal-settings-section"
|
||||
style={{ marginBottom: 20, borderTop: "1px solid var(--vscode-panel-border)", paddingTop: 15 }}>
|
||||
<h3 style={{ color: "var(--vscode-foreground)", margin: "0 0 10px 0", fontSize: "14px" }}>Terminal Settings</h3>
|
||||
<div style={{ marginBottom: 15 }}>
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<label style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>
|
||||
Shell integration timeout (seconds)
|
||||
</label>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<VSCodeTextField
|
||||
style={{ width: "100%" }}
|
||||
value={inputValue}
|
||||
placeholder="Enter timeout in seconds"
|
||||
onChange={(event) => handleTimeoutChange(event as Event)}
|
||||
onBlur={handleInputBlur}
|
||||
/>
|
||||
</div>
|
||||
{inputError && (
|
||||
<div style={{ color: "var(--vscode-errorForeground)", fontSize: "12px", marginTop: 5 }}>{inputError}</div>
|
||||
)}
|
||||
</div>
|
||||
<p style={{ fontSize: "12px", color: "var(--vscode-descriptionForeground)", margin: 0 }}>
|
||||
Set how long Cline waits for shell integration to activate before executing commands. Increase this value if
|
||||
you experience terminal connection timeouts.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TerminalSettingsSection
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
requestyDefaultModelInfo,
|
||||
} from "../../../src/shared/api"
|
||||
import { findLastIndex } from "@shared/array"
|
||||
import { McpMarketplaceCatalog, McpServer } from "../../../src/shared/mcp"
|
||||
import { McpMarketplaceCatalog, McpServer, McpViewTab } from "../../../src/shared/mcp"
|
||||
import { convertTextMateToHljs } from "../utils/textMateToHljs"
|
||||
import { vscode } from "../utils/vscode"
|
||||
import { DEFAULT_BROWSER_SETTINGS } from "@shared/BrowserSettings"
|
||||
@@ -29,12 +29,22 @@ interface ExtensionStateContextType extends ExtensionState {
|
||||
mcpMarketplaceCatalog: McpMarketplaceCatalog
|
||||
filePaths: string[]
|
||||
totalTasksSize: number | null
|
||||
// View state
|
||||
showMcp: boolean
|
||||
mcpTab?: McpViewTab
|
||||
|
||||
// Setters
|
||||
setApiConfiguration: (config: ApiConfiguration) => void
|
||||
setCustomInstructions: (value?: string) => void
|
||||
setTelemetrySetting: (value: TelemetrySetting) => void
|
||||
setShowAnnouncement: (value: boolean) => void
|
||||
setPlanActSeparateModelsSetting: (value: boolean) => void
|
||||
setShellIntegrationTimeout: (value: number) => void
|
||||
setMcpServers: (value: McpServer[]) => void
|
||||
|
||||
// Navigation
|
||||
setShowMcp: (value: boolean) => void
|
||||
setMcpTab: (tab?: McpViewTab) => void
|
||||
}
|
||||
|
||||
const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
|
||||
@@ -42,6 +52,10 @@ const ExtensionStateContext = createContext<ExtensionStateContextType | undefine
|
||||
export const ExtensionStateContextProvider: React.FC<{
|
||||
children: React.ReactNode
|
||||
}> = ({ children }) => {
|
||||
// UI view state
|
||||
const [showMcp, setShowMcp] = useState(false)
|
||||
const [mcpTab, setMcpTab] = useState<McpViewTab | undefined>(undefined)
|
||||
|
||||
const [state, setState] = useState<ExtensionState>({
|
||||
version: "",
|
||||
clineMessages: [],
|
||||
@@ -56,6 +70,7 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
planActSeparateModelsSetting: true,
|
||||
globalClineRulesToggles: {},
|
||||
localClineRulesToggles: {},
|
||||
shellIntegrationTimeout: 4000, // default timeout for shell integration
|
||||
})
|
||||
const [didHydrateState, setDidHydrateState] = useState(false)
|
||||
const [showWelcome, setShowWelcome] = useState(false)
|
||||
@@ -200,6 +215,8 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
mcpMarketplaceCatalog,
|
||||
filePaths,
|
||||
totalTasksSize,
|
||||
showMcp,
|
||||
mcpTab,
|
||||
globalClineRulesToggles: state.globalClineRulesToggles || {},
|
||||
localClineRulesToggles: state.localClineRulesToggles || {},
|
||||
setApiConfiguration: (value) =>
|
||||
@@ -227,7 +244,14 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
...prevState,
|
||||
shouldShowAnnouncement: value,
|
||||
})),
|
||||
setShellIntegrationTimeout: (value) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
shellIntegrationTimeout: value,
|
||||
})),
|
||||
setMcpServers: (mcpServers: McpServer[]) => setMcpServers(mcpServers),
|
||||
setShowMcp,
|
||||
setMcpTab,
|
||||
}
|
||||
|
||||
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useExtensionState } from "../context/ExtensionStateContext"
|
||||
import { McpViewTab } from "@shared/mcp"
|
||||
|
||||
/**
|
||||
* Hook for navigating between different views in the application.
|
||||
*/
|
||||
export const useNavigator = () => {
|
||||
const { setShowMcp, setMcpTab } = useExtensionState()
|
||||
|
||||
/**
|
||||
* Navigate to the MCP view
|
||||
* @param tab Optional tab to show in the MCP view
|
||||
*/
|
||||
const navigateToMcp = (tab?: McpViewTab) => {
|
||||
if (tab) {
|
||||
setMcpTab(tab)
|
||||
}
|
||||
setShowMcp(true)
|
||||
}
|
||||
|
||||
return {
|
||||
navigateToMcp,
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@
|
||||
/* @import "tailwindcss/preflight.css" layer(base); */
|
||||
@import "tailwindcss/utilities.css" layer(utilities);
|
||||
|
||||
@config "../tailwind.config.js";
|
||||
|
||||
textarea:focus {
|
||||
outline: 1.5px solid var(--vscode-focusBorder, #007fd4);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,10 @@ export const SUPPORTED_SLASH_COMMANDS: SlashCommand[] = [
|
||||
name: "smol",
|
||||
description: "Condenses your current context window",
|
||||
},
|
||||
{
|
||||
name: "newrule",
|
||||
description: "Create a new Cline rule based on your conversation",
|
||||
},
|
||||
]
|
||||
|
||||
// Regex for detecting slash commands in text
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
const { heroui } = require("@heroui/react")
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ["./src/**/*.{js,ts,jsx,tsx,mdx}", "./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
darkMode: "class",
|
||||
plugins: [
|
||||
heroui({
|
||||
defaultTheme: "vscode",
|
||||
themes: {
|
||||
vscode: {
|
||||
colors: {
|
||||
background: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
}
|
||||
Reference in New Issue
Block a user