diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0587c47dcc..191977c516 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,6 +6,12 @@ on: branches: - main +# Set default permissions for all jobs +permissions: + contents: read # Needed to check out code + checks: write # Needed to report test results + pull-requests: write # Needed to add comments/annotations to PRs + jobs: test: runs-on: ubuntu-latest @@ -51,6 +57,5 @@ jobs: - name: Prettier / Format Check run: npm run format - - name: Tests + - name: Extension Tests run: xvfb-run -a npm run test - if: runner.os == 'Linux' diff --git a/.gitignore b/.gitignore index 89382bd731..e6899656c2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,10 @@ out dist node_modules +tmp .vscode-test/ *.vsix .DS_Store + +pnpm-lock.yaml \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000000..f7f6972ae7 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,17 @@ +echo "Running pre-commit checks..." + +# Run ESLint +echo "Running ESLint..." +npm run lint || { + echo "❌ ESLint check failed. Please fix the errors and try committing again." + exit 1 +} + +# Run Prettier +echo "Running Prettier..." +npm run format || { + echo "❌ Prettier check failed. Run 'npm run format:fix' to automatically fix formatting issues." + exit 1 +} + +echo "✅ All checks passed!" diff --git a/.prettierignore b/.prettierignore index 71a9e12197..30f96297f0 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,5 +1,5 @@ dist/ node_modules webview-ui/build/ -CHANGELOG.md -package-lock.json \ No newline at end of file +*.md +package-lock.json diff --git a/.vscode-test.mjs b/.vscode-test.mjs index da0108114e..c1a69e22df 100644 --- a/.vscode-test.mjs +++ b/.vscode-test.mjs @@ -1,8 +1,14 @@ import { defineConfig } from "@vscode/test-cli" +import path from "path" export default defineConfig({ - files: "out/**/*.test.js", + files: "{out/test/**/*.test.js,src/test/suite/**/*.test.js}", mocha: { + ui: "bdd", timeout: 20000, // Maximum time (in ms) that a test can run before failing }, + workspaceFolder: "test-workspace", + version: "stable", + extensionDevelopmentPath: path.resolve("./"), + launchArgs: ["--disable-extensions"], }) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85989ce8e2..2b37724af2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Change Log +## [3.2.5] + +- Use yellow textfield outline in Plan mode to better distinguish from Act mode + +## [3.2.3] + +- Add DeepSeek-R1 (deepseek-reasoner) model support with proper parameter handling (thanks @slavakurilyak!) + ## [3.2.0] - Add Plan/Act mode toggle to let you plan tasks with Cline before letting him get to work diff --git a/package-lock.json b/package-lock.json index 064d9fac18..67d8666191 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "claude-dev", - "version": "3.2.0", + "version": "3.2.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "claude-dev", - "version": "3.2.0", + "version": "3.2.5", "license": "Apache-2.0", "dependencies": { "@anthropic-ai/bedrock-sdk": "^0.10.2", @@ -29,6 +29,7 @@ "diff": "^5.2.0", "execa": "^9.5.2", "fast-deep-equal": "^3.1.3", + "firebase": "^11.2.0", "get-folder-size": "^5.0.0", "globby": "^14.0.2", "isbinaryfile": "^5.0.2", @@ -49,6 +50,7 @@ "zod": "^3.23.8" }, "devDependencies": { + "@types/chai": "^5.0.1", "@types/diff": "^5.2.1", "@types/mocha": "^10.0.7", "@types/node": "20.x", @@ -58,8 +60,10 @@ "@typescript-eslint/parser": "^7.11.0", "@vscode/test-cli": "^0.0.9", "@vscode/test-electron": "^2.4.0", + "chai": "^4.3.10", "esbuild": "^0.21.5", "eslint": "^8.57.0", + "husky": "^9.1.7", "npm-run-all": "^4.1.5", "prettier": "^3.3.3", "should": "^13.2.3", @@ -2280,6 +2284,713 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@firebase/analytics": { + "version": "0.10.11", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.11.tgz", + "integrity": "sha512-zwuPiRE0+hgcS95JZbJ6DFQN4xYFO8IyGxpeePTV51YJMwCf3lkBa6FnZ/iXIqDKcBPMgMuuEZozI0BJWaLEYg==", + "dependencies": { + "@firebase/component": "0.6.12", + "@firebase/installations": "0.6.12", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.10.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/analytics-compat": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.17.tgz", + "integrity": "sha512-SJNVOeTvzdqZQvXFzj7yAirXnYcLDxh57wBFROfeowq/kRN1AqOw1tG6U4OiFOEhqi7s3xLze/LMkZatk2IEww==", + "dependencies": { + "@firebase/analytics": "0.10.11", + "@firebase/analytics-types": "0.8.3", + "@firebase/component": "0.6.12", + "@firebase/util": "1.10.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/analytics-compat/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/@firebase/analytics-types": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.3.tgz", + "integrity": "sha512-VrIp/d8iq2g501qO46uGz3hjbDb8xzYMrbu8Tp0ovzIzrvJZ2fvmj649gTjge/b7cCCcjT0H37g1gVtlNhnkbg==" + }, + "node_modules/@firebase/analytics/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/@firebase/app": { + "version": "0.10.18", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.10.18.tgz", + "integrity": "sha512-VuqEwD/QRisKd/zsFsqgvSAx34mZ3WEF47i97FD6Vw4GWAhdjepYf0Hmi6K0b4QMSgWcv/x0C30Slm5NjjERXg==", + "dependencies": { + "@firebase/component": "0.6.12", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.10.3", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@firebase/app-check": { + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.8.11.tgz", + "integrity": "sha512-42zIfRI08/7bQqczAy7sY2JqZYEv3a1eNa4fLFdtJ54vNevbBIRSEA3fZgRqWFNHalh5ohsBXdrYgFqaRIuCcQ==", + "dependencies": { + "@firebase/component": "0.6.12", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.10.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/app-check-compat": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.3.18.tgz", + "integrity": "sha512-qjozwnwYmAIdrsVGrJk+hnF1WBois54IhZR6gO0wtZQoTvWL/GtiA2F31TIgAhF0ayUiZhztOv1RfC7YyrZGDQ==", + "dependencies": { + "@firebase/app-check": "0.8.11", + "@firebase/app-check-types": "0.5.3", + "@firebase/component": "0.6.12", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.10.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/app-check-compat/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz", + "integrity": "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==" + }, + "node_modules/@firebase/app-check-types": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.3.tgz", + "integrity": "sha512-hyl5rKSj0QmwPdsAxrI5x1otDlByQ7bvNvVt8G/XPO2CSwE++rmSVf3VEhaeOR4J8ZFaF0Z0NDSmLejPweZ3ng==" + }, + "node_modules/@firebase/app-check/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/@firebase/app-compat": { + "version": "0.2.48", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.48.tgz", + "integrity": "sha512-wVNU1foBIaJncUmiALyRxhHHHC3ZPMLIETTAk+2PG87eP9B/IDBsYUiTpHyboDPEI8CgBPat/zN2v+Snkz6lBw==", + "dependencies": { + "@firebase/app": "0.10.18", + "@firebase/component": "0.6.12", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.10.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@firebase/app-compat/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/@firebase/app-types": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", + "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==" + }, + "node_modules/@firebase/app/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/@firebase/auth": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.8.2.tgz", + "integrity": "sha512-q+071y2LWe0bVnjqaX3BscqZwzdP0GKN2YBKapLq4bV88MPfCtWwGKmDhNDEDUmioOjudGXkUY5cvvKqk3mlUg==", + "dependencies": { + "@firebase/component": "0.6.12", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.10.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@react-native-async-storage/async-storage": "^1.18.1" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@firebase/auth-compat": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.5.17.tgz", + "integrity": "sha512-Shi6rqLqzU9KLXnUCmlLvVByq1kiG3oe7Wpbf5m1CgS7NiRx2pSSn0HLaRRozdkaizNzMGGj+3oHmNYQ7kU6xA==", + "dependencies": { + "@firebase/auth": "1.8.2", + "@firebase/auth-types": "0.12.3", + "@firebase/component": "0.6.12", + "@firebase/util": "1.10.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/auth-compat/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz", + "integrity": "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==" + }, + "node_modules/@firebase/auth-types": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.12.3.tgz", + "integrity": "sha512-Zq9zI0o5hqXDtKg6yDkSnvMCMuLU6qAVS51PANQx+ZZX5xnzyNLEBO3GZgBUPsV5qIMFhjhqmLDxUqCbnAYy2A==", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/auth/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/@firebase/component": { + "version": "0.6.12", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.12.tgz", + "integrity": "sha512-YnxqjtohLbnb7raXt2YuA44cC1wA9GiehM/cmxrsoxKlFxBLy2V0OkRSj9gpngAE0UoJ421Wlav9ycO7lTPAUw==", + "dependencies": { + "@firebase/util": "1.10.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@firebase/component/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/@firebase/data-connect": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.2.0.tgz", + "integrity": "sha512-7OrZtQoLSk2fiGijhIdUnTSqEFti3h1EMhw9nNiSZ6jJGduw4Pz6jrVvxjpZJtGH/JiljbMkBnPBS2h8CTRKEw==", + "dependencies": { + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.6.12", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.10.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/data-connect/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/@firebase/database": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.11.tgz", + "integrity": "sha512-gLrw/XeioswWUXgpVKCPAzzoOuvYNqK5fRUeiJTzO7Mlp9P6ylFEyPJlRBl1djqYye641r3MX6AmIeMXwjgwuQ==", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.6.12", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.10.3", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.0.2.tgz", + "integrity": "sha512-5zvdnMsfDHvrQAVM6jBS7CkBpu+z3YbpFdhxRsrK1FP45IEfxlzpeuEUb17D/tpM10vfq4Ok0x5akIBaCv7gfA==", + "dependencies": { + "@firebase/component": "0.6.12", + "@firebase/database": "1.0.11", + "@firebase/database-types": "1.0.8", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.10.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@firebase/database-compat/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/@firebase/database-types": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.8.tgz", + "integrity": "sha512-6lPWIGeufhUq1heofZULyVvWFhD01TUrkkB9vyhmksjZ4XF7NaivQp9rICMk7QNhqwa+uDCaj4j+Q8qqcSVZ9g==", + "dependencies": { + "@firebase/app-types": "0.9.3", + "@firebase/util": "1.10.3" + } + }, + "node_modules/@firebase/database/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/@firebase/firestore": { + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.7.6.tgz", + "integrity": "sha512-aVDboR+upR/44qZDLR4tnZ9pepSOFBbDJnwk7eWzmTyQq2nZAVG+HIhrqpQawmUVcDRkuJv2K2UT2+oqR8F8TA==", + "dependencies": { + "@firebase/component": "0.6.12", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.10.3", + "@firebase/webchannel-wrapper": "1.0.3", + "@grpc/grpc-js": "~1.9.0", + "@grpc/proto-loader": "^0.7.8", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/firestore-compat": { + "version": "0.3.41", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.3.41.tgz", + "integrity": "sha512-J/PgWKEt0yugETOE7lOabT16hsV21cLzSxERD7ZhaiwBQkBTSf0Mx9RhjZRT0Ttqe4weM90HGZFyUBqYA73fVA==", + "dependencies": { + "@firebase/component": "0.6.12", + "@firebase/firestore": "4.7.6", + "@firebase/firestore-types": "3.0.3", + "@firebase/util": "1.10.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/firestore-compat/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/@firebase/firestore-types": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-3.0.3.tgz", + "integrity": "sha512-hD2jGdiWRxB/eZWF89xcK9gF8wvENDJkzpVFb4aGkzfEaKxVRD1kjz1t1Wj8VZEp2LCB53Yx1zD8mrhQu87R6Q==", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/firestore/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/@firebase/functions": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.12.1.tgz", + "integrity": "sha512-QucRiFrvMMmIGTRhL7ZK2IeBnAWP7lAmfFREMpEtX47GjVqDqGxdFs+Mg7XBzxSc9UjDO4Rxf+aE9xJHU6bGwg==", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.6.12", + "@firebase/messaging-interop-types": "0.2.3", + "@firebase/util": "1.10.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/functions-compat": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.3.18.tgz", + "integrity": "sha512-N7+RN5GVus2ORB8cqfSNhfSn4iaYws6F8uCCfn4mtjC7zYS/KH6muzNAhZUdUqlv5YazbVmvxlAoYYF39i8Qzg==", + "dependencies": { + "@firebase/component": "0.6.12", + "@firebase/functions": "0.12.1", + "@firebase/functions-types": "0.6.3", + "@firebase/util": "1.10.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/functions-compat/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/@firebase/functions-types": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.3.tgz", + "integrity": "sha512-EZoDKQLUHFKNx6VLipQwrSMh01A1SaL3Wg6Hpi//x6/fJ6Ee4hrAeswK99I5Ht8roiniKHw4iO0B1Oxj5I4plg==" + }, + "node_modules/@firebase/functions/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/@firebase/installations": { + "version": "0.6.12", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.12.tgz", + "integrity": "sha512-ES/WpuAV2k2YtBTvdaknEo7IY8vaGjIjS3zhnHSAIvY9KwTR8XZFXOJoZ3nSkjN1A5R4MtEh+07drnzPDg9vaw==", + "dependencies": { + "@firebase/component": "0.6.12", + "@firebase/util": "1.10.3", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/installations-compat": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.12.tgz", + "integrity": "sha512-RhcGknkxmFu92F6Jb3rXxv6a4sytPjJGifRZj8MSURPuv2Xu+/AispCXEfY1ZraobhEHTG5HLGsP6R4l9qB5aA==", + "dependencies": { + "@firebase/component": "0.6.12", + "@firebase/installations": "0.6.12", + "@firebase/installations-types": "0.5.3", + "@firebase/util": "1.10.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/installations-compat/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/@firebase/installations-types": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.3.tgz", + "integrity": "sha512-2FJI7gkLqIE0iYsNQ1P751lO3hER+Umykel+TkLwHj6plzWVxqvfclPUZhcKFVQObqloEBTmpi2Ozn7EkCABAA==", + "peerDependencies": { + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/installations/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/@firebase/logger": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.4.tgz", + "integrity": "sha512-mH0PEh1zoXGnaR8gD1DeGeNZtWFKbnz9hDO91dIml3iou1gpOnLqXQ2dJfB71dj6dpmUjcQ6phY3ZZJbjErr9g==", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@firebase/logger/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/@firebase/messaging": { + "version": "0.12.16", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.16.tgz", + "integrity": "sha512-VJ8sCEIeP3+XkfbJA7410WhYGHdloYFZXoHe/vt+vNVDGw8JQPTQSVTRvjrUprEf5I4Tbcnpr2H34lS6zhCHSA==", + "dependencies": { + "@firebase/component": "0.6.12", + "@firebase/installations": "0.6.12", + "@firebase/messaging-interop-types": "0.2.3", + "@firebase/util": "1.10.3", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/messaging-compat": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.16.tgz", + "integrity": "sha512-9HZZ88Ig3zQ0ok/Pwt4gQcNsOhoEy8hDHoGsV1am6ulgMuGuDVD2gl11Lere2ksL+msM12Lddi2x/7TCqmODZw==", + "dependencies": { + "@firebase/component": "0.6.12", + "@firebase/messaging": "0.12.16", + "@firebase/util": "1.10.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/messaging-compat/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/@firebase/messaging-interop-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.3.tgz", + "integrity": "sha512-xfzFaJpzcmtDjycpDeCUj0Ge10ATFi/VHVIvEEjDNc3hodVBQADZ7BWQU7CuFpjSHE+eLuBI13z5F/9xOoGX8Q==" + }, + "node_modules/@firebase/messaging/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/@firebase/performance": { + "version": "0.6.12", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.6.12.tgz", + "integrity": "sha512-8mYL4z2jRlKXAi2hjk4G7o2sQLnJCCuTbyvti/xmHf5ZvOIGB01BZec0aDuBIXO+H1MLF62dbye/k91Fr+yc8g==", + "dependencies": { + "@firebase/component": "0.6.12", + "@firebase/installations": "0.6.12", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.10.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/performance-compat": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.12.tgz", + "integrity": "sha512-DyCbDTIwtBTGsEiQxTz/TD23a0na2nrDozceQ5kVkszyFYvliB0YK/9el0wAGIG91SqgTG9pxHtYErzfZc0VWw==", + "dependencies": { + "@firebase/component": "0.6.12", + "@firebase/logger": "0.4.4", + "@firebase/performance": "0.6.12", + "@firebase/performance-types": "0.2.3", + "@firebase/util": "1.10.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/performance-compat/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/@firebase/performance-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.3.tgz", + "integrity": "sha512-IgkyTz6QZVPAq8GSkLYJvwSLr3LS9+V6vNPQr0x4YozZJiLF5jYixj0amDtATf1X0EtYHqoPO48a9ija8GocxQ==" + }, + "node_modules/@firebase/performance/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/@firebase/remote-config": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.5.0.tgz", + "integrity": "sha512-weiEbpBp5PBJTHUWR4GwI7ZacaAg68BKha5QnZ8Go65W4oQjEWqCW/rfskABI/OkrGijlL3CUmCB/SA6mVo0qA==", + "dependencies": { + "@firebase/component": "0.6.12", + "@firebase/installations": "0.6.12", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.10.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/remote-config-compat": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.12.tgz", + "integrity": "sha512-91jLWPtubIuPBngg9SzwvNCWzhMLcyBccmt7TNZP+y1cuYFNOWWHKUXQ3IrxCLB7WwLqQaEu7fTDAjHsTyBsSw==", + "dependencies": { + "@firebase/component": "0.6.12", + "@firebase/logger": "0.4.4", + "@firebase/remote-config": "0.5.0", + "@firebase/remote-config-types": "0.4.0", + "@firebase/util": "1.10.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/remote-config-compat/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/@firebase/remote-config-types": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.4.0.tgz", + "integrity": "sha512-7p3mRE/ldCNYt8fmWMQ/MSGRmXYlJ15Rvs9Rk17t8p0WwZDbeK7eRmoI1tvCPaDzn9Oqh+yD6Lw+sGLsLg4kKg==" + }, + "node_modules/@firebase/remote-config/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/@firebase/storage": { + "version": "0.13.5", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.13.5.tgz", + "integrity": "sha512-sB/7HNuW0N9tITyD0RxVLNCROuCXkml5i/iPqjwOGKC0xiUfpCOjBE+bb0ABMoN1qYZfqk0y9IuI2TdomjmkNw==", + "dependencies": { + "@firebase/component": "0.6.12", + "@firebase/util": "1.10.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/storage-compat": { + "version": "0.3.15", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.3.15.tgz", + "integrity": "sha512-Z9afjrK2O9o1ZHWCpprCGZ1BTc3BbvpZvi6tkSteC8H3W/fMM6x+RoSunlzD3hEVV5bkbwdJIqNClLMchvyoPA==", + "dependencies": { + "@firebase/component": "0.6.12", + "@firebase/storage": "0.13.5", + "@firebase/storage-types": "0.8.3", + "@firebase/util": "1.10.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/storage-compat/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/@firebase/storage-types": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.3.tgz", + "integrity": "sha512-+Muk7g9uwngTpd8xn9OdF/D48uiQ7I1Fae7ULsWPuKoCH3HU7bfFPhxtJYzyhjdniowhuDpQcfPmuNRAqZEfvg==", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/storage/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/@firebase/util": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.10.3.tgz", + "integrity": "sha512-wfoF5LTy0m2ufUapV0ZnpcGQvuavTbJ5Qr1Ze9OJGL70cSMvhDyjS4w2121XdA3lGZSTOsDOyGhpoDtYwck85A==", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@firebase/util/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/@firebase/vertexai": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@firebase/vertexai/-/vertexai-1.0.3.tgz", + "integrity": "sha512-SQHg/RPb3LwQs/xiLcvAZYz9NXyDSZUIIwvgsKh6e4wdULAfyPCZIu6Y2ZYIhZLfk9Q44cKZ+++7RPTaqQJdYA==", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/component": "0.6.12", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.10.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/vertexai/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/@firebase/webchannel-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.3.tgz", + "integrity": "sha512-2xCRM9q9FlzGZCdgDMJwc0gyUkWFtkosy7Xxr6sFgQwn+wMNIWd7xIvYNauU1r64B5L5rsGKy/n9TKJ0aAFeqQ==" + }, "node_modules/@google/generative-ai": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.18.0.tgz", @@ -2289,6 +3000,35 @@ "node": ">=18.0.0" } }, + "node_modules/@grpc/grpc-js": { + "version": "1.9.15", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz", + "integrity": "sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==", + "dependencies": { + "@grpc/proto-loader": "^0.7.8", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", + "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -2493,6 +3233,60 @@ "node": ">=14" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, "node_modules/@puppeteer/browsers": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.4.0.tgz", @@ -4187,11 +4981,26 @@ "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", "license": "MIT" }, + "node_modules/@types/chai": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.0.1.tgz", + "integrity": "sha512-5T8ajsg3M/FOncpLYW7sdOcD6yf4+722sze/tc4KQV0P8Z2rAr3SAuHCIkYmYpt8VbcQlnz8SxlOlPQYefe4cA==", + "dev": true, + "dependencies": { + "@types/deep-eql": "*" + } + }, "node_modules/@types/clone-deep": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/clone-deep/-/clone-deep-4.0.4.tgz", "integrity": "sha512-vXh6JuuaAha6sqEbJueYdh5zNBPPgG1OYumuz2UvLvriN6ABHDSW8ludREGWJb1MLIzbwZn4q4zUbUCerJTJfA==" }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true + }, "node_modules/@types/diff": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/@types/diff/-/diff-5.2.1.tgz", @@ -4792,6 +5601,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/ast-types": { "version": "0.13.4", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", @@ -5141,6 +5959,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/chai": { + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", + "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.0.8" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -5171,6 +6007,18 @@ "node": ">=8" } }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, "node_modules/cheerio": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", @@ -5565,6 +6413,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -6500,6 +7360,17 @@ "reusify": "^1.0.4" } }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", @@ -6578,6 +7449,41 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/firebase": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-11.2.0.tgz", + "integrity": "sha512-ztwPhBLAZMVNZjBeQzzTM4rk2rsRXmdFYcnvjAXh+StbiFVshHKaPO9VRGMUzF48du4Mkz6jN1wkmYCuUJPxLA==", + "dependencies": { + "@firebase/analytics": "0.10.11", + "@firebase/analytics-compat": "0.2.17", + "@firebase/app": "0.10.18", + "@firebase/app-check": "0.8.11", + "@firebase/app-check-compat": "0.3.18", + "@firebase/app-compat": "0.2.48", + "@firebase/app-types": "0.9.3", + "@firebase/auth": "1.8.2", + "@firebase/auth-compat": "0.5.17", + "@firebase/data-connect": "0.2.0", + "@firebase/database": "1.0.11", + "@firebase/database-compat": "2.0.2", + "@firebase/firestore": "4.7.6", + "@firebase/firestore-compat": "0.3.41", + "@firebase/functions": "0.12.1", + "@firebase/functions-compat": "0.3.18", + "@firebase/installations": "0.6.12", + "@firebase/installations-compat": "0.2.12", + "@firebase/messaging": "0.12.16", + "@firebase/messaging-compat": "0.2.16", + "@firebase/performance": "0.6.12", + "@firebase/performance-compat": "0.2.12", + "@firebase/remote-config": "0.5.0", + "@firebase/remote-config-compat": "0.2.12", + "@firebase/storage": "0.13.5", + "@firebase/storage-compat": "0.3.15", + "@firebase/util": "1.10.3", + "@firebase/vertexai": "1.0.3" + } + }, "node_modules/flat": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", @@ -6872,6 +7778,15 @@ "node": ">=18.11.0" } }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/get-intrinsic": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", @@ -7275,6 +8190,11 @@ "node": ">= 0.8" } }, + "node_modules/http-parser-js": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.9.tgz", + "integrity": "sha512-n1XsPy3rXVxlqxVioEWdC+0+M+SQw0DpJynwtOPo1X+ZlvdzTLtDBIJJlDQTnwZIFJrZSzSGmIOUdP8tu+SgLw==" + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -7319,6 +8239,21 @@ "ms": "^2.0.0" } }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -7331,6 +8266,11 @@ "node": ">=0.10.0" } }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==" + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -8075,6 +9015,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -8099,6 +9044,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/long": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.4.tgz", + "integrity": "sha512-qtzLbJE8hq7VabR3mISmVGtoXP8KGc2Z/AT8OuqlYD7JTR3oqrgwdjnk07wpj1twXxYmgDXgoKVWUG/fReSzHg==" + }, "node_modules/lop": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/lop/-/lop-0.4.1.tgz", @@ -8110,6 +9060,15 @@ "underscore": "^1.13.1" } }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.1" + } + }, "node_modules/lru-cache": { "version": "10.3.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.3.0.tgz", @@ -9346,6 +10305,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/pdf-parse": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-1.1.1.tgz", @@ -9475,6 +10443,29 @@ "node": ">=0.4.0" } }, + "node_modules/protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/proxy-agent": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", @@ -10725,6 +11716,15 @@ "node": ">= 0.8.0" } }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -11006,6 +12006,27 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "license": "BSD-2-Clause" }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/whatwg-encoding": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", diff --git a/package.json b/package.json index 075ee7e765..d88d82305c 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "claude-dev", "displayName": "Cline", "description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.", - "version": "3.2.0", + "version": "3.2.5", "icon": "assets/icons/icon.png", "galleryBanner": { "color": "#617A91", @@ -141,6 +141,11 @@ } }, "description": "Settings for VSCode Language Model API" + }, + "cline.mcp.enabled": { + "type": "boolean", + "default": true, + "description": "Include MCP server functionality in AI prompts. When disabled, the AI will not be aware of MCP capabilities. This saves context window tokens." } } } @@ -164,9 +169,11 @@ "start:webview": "cd webview-ui && npm run start", "build:webview": "cd webview-ui && npm run build", "test:webview": "cd webview-ui && npm run test", - "publish:marketplace": "vsce publish && ovsx publish" + "publish:marketplace": "vsce publish && ovsx publish", + "prepare": "husky" }, "devDependencies": { + "@types/chai": "^5.0.1", "@types/diff": "^5.2.1", "@types/mocha": "^10.0.7", "@types/node": "20.x", @@ -176,8 +183,10 @@ "@typescript-eslint/parser": "^7.11.0", "@vscode/test-cli": "^0.0.9", "@vscode/test-electron": "^2.4.0", + "chai": "^4.3.10", "esbuild": "^0.21.5", "eslint": "^8.57.0", + "husky": "^9.1.7", "npm-run-all": "^4.1.5", "prettier": "^3.3.3", "should": "^13.2.3", @@ -204,6 +213,7 @@ "diff": "^5.2.0", "execa": "^9.5.2", "fast-deep-equal": "^3.1.3", + "firebase": "^11.2.0", "get-folder-size": "^5.0.0", "globby": "^14.0.2", "isbinaryfile": "^5.0.2", diff --git a/src/api/providers/deepseek.ts b/src/api/providers/deepseek.ts index a903ce2dd9..d68dc49bed 100644 --- a/src/api/providers/deepseek.ts +++ b/src/api/providers/deepseek.ts @@ -18,13 +18,15 @@ export class DeepSeekHandler implements ApiHandler { } async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { + const model = this.getModel() const stream = await this.client.chat.completions.create({ - model: this.getModel().id, - max_completion_tokens: this.getModel().info.maxTokens, - temperature: 0, + model: model.id, + max_completion_tokens: model.info.maxTokens, messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)], stream: true, stream_options: { include_usage: true }, + // Only set temperature for non-reasoner models + ...(model.id === "deepseek-reasoner" ? {} : { temperature: 0 }), }) for await (const chunk of stream) { diff --git a/src/core/Cline.ts b/src/core/Cline.ts index ef6d613c00..947a5fae6f 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -3290,12 +3290,11 @@ export class Cline { if (this.chatSettings.mode === "plan") { details += "\nPLAN MODE" details += - "\nIn this mode you should focus on information gathering and architecting a solution. If you haven't done so already, it's a good idea to start by reading files to get context and then asking questions." + "\nIn this mode you should focus on information gathering, asking questions, and architecting a solution. Once you have a plan, use the plan_mode_response tool to engage in a conversational back and forth with the user. Do not use the plan_mode_response tool until you've gathered all the information you need e.g. with read_file or ask_followup_question." details += - '\n(Remember: You now have access to the plan_mode_response tool, which allows you to engage in a more conversational back and forth with the user rather than jumping into executing the task. If it seems the user wants you to use tools only available in ACT MODE, you should ask the user to "toggle to Act mode" - they will have to manually do this themselves with the Plan/Act toggle button below. You do not have the ability to switch to ACT MODE yourself, and must wait for the user to do it themselves once they are satisfied with the plan.)' + '\n(Remember: If it seems the user wants you to use tools only available in Act Mode, you should ask the user to "toggle to Act mode" (use those words) - they will have to manually do this themselves with the Plan/Act toggle button below. You do not have the ability to switch to Act Mode yourself, and must wait for the user to do it themselves once they are satisfied with the plan.)' } else { details += "\nACT MODE" - details += "\n(Remember: You cannot use the plan_mode_response tool.)" } return `\n${details.trim()}\n` diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index 302de8f96e..af3d9b9f46 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -177,6 +177,9 @@ Usage: : "" } +${ + mcpHub.isMcpEnabled() + ? ` ## use_mcp_tool Description: Request to use a tool provided by a connected MCP server. Each MCP server can provide multiple tools with different capabilities. Tools have defined input schemas that specify required and optional parameters. Parameters: @@ -205,6 +208,9 @@ Usage: server name here resource URI here +` + : "" +} ## ask_followup_question Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth. @@ -230,9 +236,9 @@ Your final result description here ## plan_mode_response -Description: Respond to the user's inquiry with a clear answer in an effort to plan a solution to the user's task. This tool should be used when you need to provide a response to a question or statement from the user about how you plan to accomplish the task. This tool is only available in PLAN MODE. The environment_details will specify the current mode, if it is not PLAN MODE then you should not use this tool. Depending on the user's message, you may ask questions to get clarification about the user's request, architect a solution to the task, and to brainstorm ideas with the user. For example, if the user's task is to create a website, you may start by asking some clarifying questions, then present a detailed plan for how you will accomplish the task given the context, and perhaps engage in a back and forth to finalize the details before the user switches you to ACT MODE to implement the solution. +Description: Respond to the user's inquiry in an effort to plan a solution to the user's task. This tool should be used when you need to provide a response to a question or statement from the user about how you plan to accomplish the task. This tool is only available in PLAN MODE. The environment_details will specify the current mode, if it is not PLAN MODE then you should not use this tool. Depending on the user's message, you may ask questions to get clarification about the user's request, architect a solution to the task, and to brainstorm ideas with the user. For example, if the user's task is to create a website, you may start by asking some clarifying questions, then present a detailed plan for how you will accomplish the task given the context, and perhaps engage in a back and forth to finalize the details before the user switches you to ACT MODE to implement the solution. Parameters: -- response: (required) The response to provide to the user. +- response: (required) The response to provide to the user. Do not try to use tools in this parameter, this is simply a chat response. Usage: Your response here @@ -247,27 +253,7 @@ Usage: false -## Example 2: Requesting to use an MCP tool - - -weather-server -get_forecast - -{ - "city": "San Francisco", - "days": 5 -} - - - -## Example 3: Requesting to access an MCP resource - - -weather-server -weather://san-francisco/current - - -## Example 4: Requesting to create a new file +## Example 2: Requesting to create a new file src/frontend-config.json @@ -289,7 +275,7 @@ Usage: -## Example 6: Requesting to make targeted edits to a file +## Example 3: Requesting to make targeted edits to a file src/components/App.tsx @@ -323,6 +309,31 @@ return ( >>>>>>> REPLACE +${ + mcpHub.isMcpEnabled() + ? ` + +## Example 4: Requesting to use an MCP tool + + +weather-server +get_forecast + +{ + "city": "San Francisco", + "days": 5 +} + + + +## Example 5: Requesting to access an MCP resource + + +weather-server +weather://san-francisco/current +` + : "" +} # Tool Use Guidelines @@ -345,6 +356,9 @@ It is crucial to proceed step-by-step, waiting for the user's message after each By waiting for and carefully considering the user's response after each tool use, you can react accordingly and make informed decisions about how to proceed with the task. This iterative process helps ensure the overall success and accuracy of your work. +${ + mcpHub.isMcpEnabled() + ? ` ==== MCP SERVERS @@ -737,12 +751,13 @@ IMPORTANT: Regardless of what else you see in the MCP settings file, you must de ## Editing MCP Servers -The user may ask to add tools or resources that may make sense to add to an existing MCP server (listed under 'Connected MCP Servers' above: ${ - mcpHub - .getServers() - .map((server) => server.name) - .join(", ") || "(None running currently)" -}, e.g. if it would use the same API. This would be possible if you can locate the MCP server repository on the user's system by looking at the server arguments for a filepath. You might then use list_files and read_file to explore the files in the repository, and use replace_in_file to make changes to the files. +The user may ask to add tools or resources that may make sense to add to an existing MCP server (listed under 'Connected MCP Servers' below: ${ + mcpHub + .getServers() + .filter((server) => server.status === "connected") + .map((server) => server.name) + .join(", ") || "(None running currently)" + }, e.g. if it would use the same API. This would be possible if you can locate the MCP server repository on the user's system by looking at the server arguments for a filepath. You might then use list_files and read_file to explore the files in the repository, and use replace_in_file to make changes to the files. However some MCP servers may be running from installed packages rather than a local repository, in which case it may make more sense to create a new MCP server. @@ -751,7 +766,9 @@ However some MCP servers may be running from installed packages rather than a lo The user may not always request the use or creation of MCP servers. Instead, they might provide tasks that can be completed with existing tools. While using the MCP SDK to extend your capabilities can be useful, it's important to understand that this is just one specialized type of task you can accomplish. You should only implement MCP servers when the user explicitly requests it (e.g., "add a tool that..."). Remember: The MCP documentation and example provided above are to help you understand and work with existing MCP servers or create new ones when requested by the user. You already have access to tools and capabilities that can be used to accomplish a wide range of tasks. - +` + : "" +} ==== EDITING FILES @@ -834,10 +851,10 @@ ACT MODE V.S. PLAN MODE In each user message, the environment_details will specify the current mode. There are two modes: - ACT MODE: In this mode, you have access to all tools EXCEPT the plan_mode_response tool. - - In act mode, you use tools to accomplish the user's task. Once you've completed the user's task, you use the attempt_completion tool to present the result of the task to the user. + - In ACT MODE, you use tools to accomplish the user's task. Once you've completed the user's task, you use the attempt_completion tool to present the result of the task to the user. - PLAN MODE: In this special mode, you have access to the plan_mode_response tool. - - In plan mode, the goal is to gather information and get context to create a detailed plan for accomplishing the task, which the user will review and approve before you switch back to ACT MODE to implement the solution. - - In plan mode, you should use the plan_mode_response tool to deliver your response, rather than using tags to analyze when to respond. Do not talk about using plan_mode_response - just use it directly to share your thoughts and provide helpful answers. + - In PLAN MODE, the goal is to gather information and get context to create a detailed plan for accomplishing the task, which the user will review and approve before they switch you to ACT MODE to implement the solution. + - In PLAN MODE, when you need to converse with the user or present a plan, you should use the plan_mode_response tool to deliver your response directly, rather than using tags to analyze when to respond. Do not talk about using plan_mode_response - just use it directly to share your thoughts and provide helpful answers. ## What is PLAN MODE? @@ -863,7 +880,13 @@ CAPABILITIES ? "\n- You can use the browser_action tool to interact with websites (including html files and locally running development servers) through a Puppeteer-controlled browser when you feel it is necessary in accomplishing the user's task. This tool is particularly useful for web development tasks as it allows you to launch a browser, navigate to pages, interact with elements through clicks and keyboard input, and capture the results through screenshots and console logs. This tool may be useful at key stages of web development tasks-such as after implementing new features, making substantial changes, when troubleshooting issues, or to verify the result of your work. You can analyze the provided screenshots to ensure correct rendering or identify errors, and review console logs for runtime issues.\n - For example, if asked to add a component to a react website, you might create the necessary files, use execute_command to run the site locally, then use browser_action to launch the browser, navigate to the local server, and verify the component renders & functions correctly before closing the browser." : "" } +${ + mcpHub.isMcpEnabled() + ? ` - You have access to MCP servers that may provide additional tools and resources. Each server may provide different capabilities that you can use to accomplish tasks more effectively. +` + : "" +} ==== @@ -884,7 +907,7 @@ RULES - The user may provide a file's contents directly in their message, in which case you shouldn't use the read_file tool to get the file contents again since you already have it. - Your goal is to try to accomplish the user's task, NOT engage in a back and forth conversation.${ supportsComputerUse - ? '\n- The user may ask generic non-development tasks, such as "what\'s the latest news" or "look up the weather in San Diego", in which case you might use the browser_action tool to complete the task if it makes sense to do so, rather than trying to create a website or using curl to answer the question. However, if an available MCP server tool or resource can be used instead, you should prefer to use it over browser_action.' + ? `\n- The user may ask generic non-development tasks, such as "what\'s the latest news" or "look up the weather in San Diego", in which case you might use the browser_action tool to complete the task if it makes sense to do so, rather than trying to create a website or using curl to answer the question.${mcpHub.isMcpEnabled() ? "However, if an available MCP server tool or resource can be used instead, you should prefer to use it over browser_action." : ""}` : "" } - NEVER end attempt_completion result with a question or request to engage in further conversation! Formulate the end of your result in a way that is final and does not require further input from the user. @@ -892,7 +915,6 @@ RULES - When presented with images, utilize your vision capabilities to thoroughly examine them and extract meaningful information. Incorporate these insights into your thought process as you accomplish the user's task. - At the end of each user message, you will automatically receive environment_details. This information is not written by the user themselves, but is auto-generated to provide potentially relevant context about the project structure and environment. While this information can be valuable for understanding the project context, do not treat it as a direct part of the user's request or response. Use it to inform your actions and decisions, but don't assume the user is explicitly asking about or referring to this information unless they clearly do so in their message. When using environment_details, explain your actions clearly to ensure the user understands, as they may not be aware of these details. - Before executing commands, check the "Actively Running Terminals" section in environment_details. If present, consider how these active processes might impact your task. For example, if a local development server is already running, you wouldn't need to start it again. If no active terminals are listed, proceed with command execution as normal. -- MCP operations should be used one at a time, similar to other tool usage. Wait for confirmation of success before proceeding with additional operations. - When using the replace_in_file tool, you must include complete lines in your SEARCH blocks, not partial lines. The system requires exact line matches and cannot match partial lines. For example, if you want to match a line containing "const x = 5;", your SEARCH block must include the entire line, not just "x = 5" or other fragments. - When using the replace_in_file tool, if you use multiple SEARCH/REPLACE blocks, list them in the order they appear in the file. For example if you need to make changes to both line 10 and line 50, first include the SEARCH/REPLACE block for line 10, followed by the SEARCH/REPLACE block for line 50. - It is critical you wait for the user's response after each tool use, in order to confirm the success of the tool use. For example, if asked to make a todo app, you would create a file, wait for the user's response it was created successfully, then create another file if needed, wait for the user's response it was created successfully, etc.${ @@ -900,6 +922,13 @@ RULES ? " Then if you want to test your work, you might use browser_action to launch the site, wait for the user's response confirming the site was launched along with a screenshot, then perhaps e.g., click a button to test functionality if needed, wait for the user's response confirming the button was clicked along with a screenshot of the new state, before finally closing the browser." : "" } +${ + mcpHub.isMcpEnabled() + ? ` +- MCP operations should be used one at a time, similar to other tool usage. Wait for confirmation of success before proceeding with additional operations. +` + : "" +} ==== diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index d8d5b99ce8..bf92fc99ce 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -2,6 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk" import axios from "axios" import fs from "fs/promises" import os from "os" +import crypto from "crypto" import pWaitFor from "p-wait-for" import * as path from "path" import * as vscode from "vscode" @@ -12,6 +13,7 @@ import { selectImages } from "../../integrations/misc/process-images" import { getTheme } from "../../integrations/theme/getTheme" import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker" import { McpHub } from "../../services/mcp/McpHub" +import { FirebaseAuthManager, UserInfo } from "../../services/auth/FirebaseAuthManager" import { ApiProvider, ModelInfo } from "../../shared/api" import { findLast } from "../../shared/array" import { ExtensionMessage, ExtensionState } from "../../shared/ExtensionMessage" @@ -43,6 +45,8 @@ type SecretKey = | "openAiNativeApiKey" | "deepSeekApiKey" | "mistralApiKey" + | "authToken" + | "authNonce" type GlobalStateKey = | "apiProvider" | "apiModelId" @@ -68,6 +72,7 @@ type GlobalStateKey = | "chatSettings" | "vsCodeLmModelSelector" | "localeLanguage" + | "userInfo" export const GlobalFileNames = { apiConversationHistory: "api_conversation_history.json", @@ -86,7 +91,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { private cline?: Cline private workspaceTracker?: WorkspaceTracker mcpHub?: McpHub - private latestAnnouncementId = "jan-20a-2025" // update to some unique identifier when we add a new announcement + private authManager: FirebaseAuthManager + private latestAnnouncementId = "jan-20-2025" // update to some unique identifier when we add a new announcement constructor( readonly context: vscode.ExtensionContext, @@ -96,6 +102,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { ClineProvider.activeInstances.add(this) this.workspaceTracker = new WorkspaceTracker(this) this.mcpHub = new McpHub(this) + this.authManager = new FirebaseAuthManager(this) } /* @@ -121,10 +128,29 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.workspaceTracker = undefined this.mcpHub?.dispose() this.mcpHub = undefined + this.authManager.dispose() this.outputChannel.appendLine("Disposed all disposables") ClineProvider.activeInstances.delete(this) } + // Auth methods + async handleSignOut() { + try { + await this.authManager.signOut() + vscode.window.showInformationMessage("Successfully logged out of Cline") + } catch (error) { + vscode.window.showErrorMessage("Logout failed") + } + } + + async setAuthToken(token?: string) { + await this.storeSecret("authToken", token) + } + + async setUserInfo(info?: { displayName: string | null; email: string | null; photoURL: string | null }) { + await this.updateGlobalState("userInfo", info) + } + public static getVisibleInstance(): ClineProvider | undefined { return findLast(Array.from(this.activeInstances), (instance) => instance.view?.visible === true) } @@ -314,7 +340,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { - + Cline @@ -595,6 +621,27 @@ export class ClineProvider implements vscode.WebviewViewProvider { case "getLatestState": await this.postStateToWebview() break + case "accountLoginClicked": { + // Generate nonce for state validation + const nonce = crypto.randomBytes(32).toString("hex") + await this.storeSecret("authNonce", nonce) + + // Open browser for authentication with state param + console.log("Login button clicked in account page") + console.log("Opening auth page with state param") + + const uriScheme = vscode.env.uriScheme + + const authUrl = vscode.Uri.parse( + `https://app.cline.bot/auth?state=${encodeURIComponent(nonce)}&callback_url=${encodeURIComponent(`${uriScheme || "vscode"}://saoudrizwan.claude-dev/auth`)}`, + ) + vscode.env.openExternal(authUrl) + break + } + case "accountLogoutClicked": { + await this.handleSignOut() + break + } case "openMcpSettings": { const mcpSettingsFilePath = await this.mcpHub?.getMcpSettingsFilePath() if (mcpSettingsFilePath) { @@ -626,6 +673,10 @@ export class ClineProvider implements vscode.WebviewViewProvider { } break } + case "openExtensionSettings": { + await vscode.commands.executeCommand("workbench.action.openSettings", "@ext:saoudrizwan.claude-dev") + break + } // Add more switch case statements here as more webview message commands // are created within the webview context (i.e. inside media/main.js) } @@ -741,6 +792,32 @@ export class ClineProvider implements vscode.WebviewViewProvider { } } + // Auth + + public async validateAuthState(state: string | null): Promise { + const storedNonce = await this.getSecret("authNonce") + if (!state || state !== storedNonce) { + return false + } + await this.storeSecret("authNonce", undefined) // Clear after use + return true + } + + async handleAuthCallback(token: string) { + try { + // First sign in with Firebase to trigger auth state change + await this.authManager.signInWithCustomToken(token) + + // Then store the token securely + await this.storeSecret("authToken", token) + await this.postStateToWebview() + vscode.window.showInformationMessage("Successfully logged in to Cline") + } catch (error) { + console.error("Failed to handle auth callback:", error) + vscode.window.showErrorMessage("Failed to log in to Cline") + } + } + // OpenRouter async handleOpenRouterCallback(code: string) { @@ -1014,7 +1091,10 @@ export class ClineProvider implements vscode.WebviewViewProvider { autoApprovalSettings, browserSettings, chatSettings, + userInfo, } = await this.getState() + + const authToken = await this.getSecret("authToken") return { version: this.context.extension?.packageJSON?.version ?? "", apiConfiguration, @@ -1029,6 +1109,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { browserSettings, chatSettings, localeLanguage: vscode.env.language, + isLoggedIn: !!authToken, + userInfo, } } @@ -1119,6 +1201,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { chatSettings, vsCodeLmModelSelector, localeLanguage, + userInfo, ] = await Promise.all([ this.getGlobalState("apiProvider") as Promise, this.getGlobalState("apiModelId") as Promise, @@ -1154,6 +1237,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.getGlobalState("chatSettings") as Promise, this.getGlobalState("vsCodeLmModelSelector") as Promise, this.getGlobalState("localeLanguage") as Promise, + this.getGlobalState("userInfo") as Promise, ]) let apiProvider: ApiProvider @@ -1206,6 +1290,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { autoApprovalSettings: autoApprovalSettings || DEFAULT_AUTO_APPROVAL_SETTINGS, // default value can be 0 or empty string browserSettings: browserSettings || DEFAULT_BROWSER_SETTINGS, chatSettings: chatSettings || DEFAULT_CHAT_SETTINGS, + userInfo, } } @@ -1261,7 +1346,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { } } - private async getSecret(key: SecretKey) { + async getSecret(key: SecretKey) { return await this.context.secrets.get(key) } @@ -1283,6 +1368,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { "openAiNativeApiKey", "deepSeekApiKey", "mistralApiKey", + "authToken", ] for (const key of secretKeys) { await this.storeSecret(key, undefined) diff --git a/src/extension.ts b/src/extension.ts index 35dda8b588..1faee0133b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -110,6 +110,15 @@ export function activate(context: vscode.ExtensionContext) { }), ) + context.subscriptions.push( + vscode.commands.registerCommand("cline.accountLoginClicked", () => { + sidebarProvider.postMessageToWebview({ + type: "action", + action: "accountLoginClicked", + }) + }), + ) + /* We use the text document content provider API to show the left side for diff view by creating a virtual document for the original content. This makes it readonly so users know to edit the right side if they want to keep their changes. @@ -126,6 +135,12 @@ export function activate(context: vscode.ExtensionContext) { // URI Handler const handleUri = async (uri: vscode.Uri) => { + console.log("URI Handler called with:", { + path: uri.path, + query: uri.query, + scheme: uri.scheme, + }) + const path = uri.path const query = new URLSearchParams(uri.query.replace(/\+/g, "%2B")) const visibleProvider = ClineProvider.getVisibleInstance() @@ -140,6 +155,26 @@ export function activate(context: vscode.ExtensionContext) { } break } + case "/auth": { + const token = query.get("token") + const state = query.get("state") + + console.log("Auth callback received:", { + token: token, + state: state, + }) + + // Validate state parameter + if (!(await visibleProvider.validateAuthState(state))) { + vscode.window.showErrorMessage("Invalid auth state") + return + } + + if (token) { + await visibleProvider.handleAuthCallback(token) + } + break + } default: break } diff --git a/src/integrations/debug/DebugConsoleManager.ts b/src/integrations/debug/DebugConsoleManager.ts new file mode 100644 index 0000000000..4ec7bdf65a --- /dev/null +++ b/src/integrations/debug/DebugConsoleManager.ts @@ -0,0 +1,73 @@ +import * as vscode from "vscode" + +interface DebugSession { + id: string + name: string + output: string[] + lastRetrievedIndex: number +} + +export class DebugConsoleManager { + private sessions: Map = new Map() + private disposables: vscode.Disposable[] = [] + + constructor() { + // Listen for debug session start events + this.disposables.push( + vscode.debug.onDidStartDebugSession((session) => { + this.sessions.set(session.id, { + id: session.id, + name: session.name, + output: [], + lastRetrievedIndex: -1, + }) + }), + ) + + // Listen for debug session end events + this.disposables.push( + vscode.debug.onDidTerminateDebugSession((session) => { + this.sessions.delete(session.id) + }), + ) + + // Listen for debug console output + this.disposables.push( + vscode.debug.onDidReceiveDebugSessionCustomEvent((e: vscode.DebugSessionCustomEvent) => { + if (e.event === "output" && e.body?.output) { + const session = this.sessions.get(e.session.id) + if (session) { + session.output.push(e.body.output) + } + } + }), + ) + } + + /** + * Get all active debug sessions + */ + getActiveSessions(): { id: string; name: string }[] { + return Array.from(this.sessions.values()).map(({ id, name }) => ({ id, name })) + } + + /** + * Get any new output since the last retrieval for a specific debug session + */ + getUnretrievedOutput(sessionId: string): string | undefined { + const session = this.sessions.get(sessionId) + if (!session) return undefined + + const newOutput = session.output.slice(session.lastRetrievedIndex + 1).join("") + session.lastRetrievedIndex = session.output.length - 1 + return newOutput || undefined + } + + /** + * Clean up resources + */ + dispose() { + this.disposables.forEach((d) => d.dispose()) + this.sessions.clear() + } +} diff --git a/src/services/auth/FirebaseAuthManager.ts b/src/services/auth/FirebaseAuthManager.ts new file mode 100644 index 0000000000..835a84c0c0 --- /dev/null +++ b/src/services/auth/FirebaseAuthManager.ts @@ -0,0 +1,99 @@ +import { initializeApp } from "firebase/app" +import { Auth, User, getAuth, onAuthStateChanged, signInWithCustomToken, signOut } from "firebase/auth" +import * as vscode from "vscode" +import { ClineProvider } from "../../core/webview/ClineProvider" +import { firebaseConfig } from "./config" + +export interface UserInfo { + displayName: string | null + email: string | null + photoURL: string | null +} + +export class FirebaseAuthManager { + private providerRef: WeakRef + private auth: Auth + private disposables: vscode.Disposable[] = [] + + constructor(provider: ClineProvider) { + console.log("Initializing FirebaseAuthManager", { provider }) + this.providerRef = new WeakRef(provider) + const app = initializeApp(firebaseConfig) + this.auth = getAuth(app) + console.log("Firebase app initialized", { appConfig: firebaseConfig }) + + // Auth state listener + onAuthStateChanged(this.auth, this.handleAuthStateChange.bind(this)) + console.log("Auth state change listener added") + + // Try to restore session + this.restoreSession() + } + + private async restoreSession() { + console.log("Attempting to restore session") + const provider = this.providerRef.deref() + if (!provider) { + console.log("Provider reference lost during session restore") + return + } + + const storedToken = await provider.getSecret("authToken") + if (storedToken) { + console.log("Found stored auth token, attempting to restore session") + try { + await this.signInWithCustomToken(storedToken) + console.log("Session restored successfully") + } catch (error) { + console.error("Failed to restore session, clearing token:", error) + await provider.setAuthToken(undefined) + await provider.setUserInfo(undefined) + } + } else { + console.log("No stored auth token found") + } + } + + private async handleAuthStateChange(user: User | null) { + console.log("Auth state changed", { user }) + const provider = this.providerRef.deref() + if (!provider) { + console.log("Provider reference lost") + return + } + + if (user) { + console.log("User signed in", { userId: user.uid }) + const idToken = await user.getIdToken() + await provider.setAuthToken(idToken) + // Store public user info in state + await provider.setUserInfo({ + displayName: user.displayName, + email: user.email, + photoURL: user.photoURL, + }) + console.log("User info set in provider", { user }) + } else { + console.log("User signed out") + await provider.setAuthToken(undefined) + await provider.setUserInfo(undefined) + } + await provider.postStateToWebview() + console.log("Webview state updated") + } + + async signInWithCustomToken(token: string) { + console.log("Signing in with custom token", { token }) + await signInWithCustomToken(this.auth, token) + } + + async signOut() { + console.log("Signing out") + await signOut(this.auth) + } + + dispose() { + this.disposables.forEach((d) => d.dispose()) + console.log("Disposables disposed", { count: this.disposables.length }) + } +} diff --git a/src/services/auth/config.ts b/src/services/auth/config.ts new file mode 100644 index 0000000000..86034075a2 --- /dev/null +++ b/src/services/auth/config.ts @@ -0,0 +1,10 @@ +// Public Firebase config (safe for open source) +export const firebaseConfig = { + apiKey: "AIzaSyDcXAaanNgR2_T0dq2oOl5XyKPksYHppVo", + authDomain: "cline-bot.firebaseapp.com", + projectId: "cline-bot", + storageBucket: "cline-bot.firebasestorage.app", + messagingSenderId: "364369702101", + appId: "1:364369702101:web:0013885dcf20b43799c65c", + measurementId: "G-MDPRELSCD1", +} diff --git a/src/services/mcp/McpHub.ts b/src/services/mcp/McpHub.ts index 2ea31b830d..dba50d54f6 100644 --- a/src/services/mcp/McpHub.ts +++ b/src/services/mcp/McpHub.ts @@ -59,6 +59,10 @@ export class McpHub { return this.connections.filter((conn) => !conn.server.disabled).map((conn) => conn.server) } + isMcpEnabled(): boolean { + return vscode.workspace.getConfiguration("cline.mcp").get("enabled") ?? true + } + async getMcpServersPath(): Promise { const provider = this.providerRef.deref() if (!provider) { diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 43f0fb5033..af1b25d03a 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -25,7 +25,14 @@ export interface ExtensionMessage { | "vsCodeLmModels" | "requestVsCodeLmModels" text?: string - action?: "chatButtonClicked" | "mcpButtonClicked" | "settingsButtonClicked" | "historyButtonClicked" | "didBecomeVisible" + action?: + | "chatButtonClicked" + | "mcpButtonClicked" + | "settingsButtonClicked" + | "historyButtonClicked" + | "didBecomeVisible" + | "accountLoginClicked" + | "accountLogoutClicked" invoke?: "sendMessage" | "primaryButtonClick" | "secondaryButtonClick" state?: ExtensionState images?: string[] @@ -52,6 +59,12 @@ export interface ExtensionState { browserSettings: BrowserSettings chatSettings: ChatSettings localeLanguage: string + isLoggedIn: boolean + userInfo?: { + displayName: string | null + email: string | null + photoURL: string | null + } } export interface ClineMessage { diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 6783bc0d79..f2d41d31e1 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -33,10 +33,13 @@ export interface WebviewMessage { | "checkpointDiff" | "checkpointRestore" | "taskCompletionViewChanges" + | "openExtensionSettings" | "requestVsCodeLmModels" | "toggleToolAutoApprove" | "toggleMcpServer" | "getLatestState" + | "accountLoginClicked" + | "accountLogoutClicked" // | "relaunchChromeDebugMode" text?: string disabled?: boolean diff --git a/src/shared/api.ts b/src/shared/api.ts index f753525fc3..2eeb6387ed 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -377,6 +377,16 @@ export const deepSeekModels = { cacheWritesPrice: 0.14, cacheReadsPrice: 0.014, }, + "deepseek-reasoner": { + maxTokens: 8_000, + contextWindow: 64_000, + supportsImages: false, + supportsPromptCache: true, // supports context caching, but not in the way anthropic does it (deepseek reports input tokens and reads/writes in the same usage report) FIXME: we need to show users cache stats how deepseek does it + inputPrice: 0, // technically there is no input price, it's all either a cache hit or miss (ApiOptions will not show this) + outputPrice: 2.19, + cacheWritesPrice: 0.55, + cacheReadsPrice: 0.14, + }, } as const satisfies Record // Mistral diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts index 440a353ec6..063cf3bfc7 100644 --- a/src/test/extension.test.ts +++ b/src/test/extension.test.ts @@ -4,7 +4,7 @@ import path from "path" import "should" import * as vscode from "vscode" -const packagePath = path.join(__dirname, "..", "..", "..", "package.json") +const packagePath = path.join(__dirname, "..", "..", "package.json") describe("Cline Extension", () => { after(() => { @@ -23,4 +23,69 @@ describe("Cline Extension", () => { await new Promise((resolve) => setTimeout(resolve, 400)) await vscode.commands.executeCommand("cline.plusButtonClicked") }) + + // New test to verify xvfb and webview functionality + it("should create and display a webview panel", async () => { + // Create a webview panel + const panel = vscode.window.createWebviewPanel("testWebview", "CI/CD Test", vscode.ViewColumn.One, { + enableScripts: true, + }) + + // Set some HTML content + panel.webview.html = ` + + + + + xvfb Test + + +
Testing xvfb display server
+ + + ` + + // Verify panel exists + should.exist(panel) + panel.visible.should.be.true() + + // Clean up + panel.dispose() + }) + + // Test webview message passing + it("should handle webview messages", async () => { + const panel = vscode.window.createWebviewPanel("testWebview", "Message Test", vscode.ViewColumn.One, { + enableScripts: true, + }) + + // Set up message handling + const messagePromise = new Promise((resolve) => { + panel.webview.onDidReceiveMessage((message) => resolve(message.text), undefined) + }) + + // Add message sending script + panel.webview.html = ` + + + + + Message Test + + + + + + ` + + // Wait for message + const message = await messagePromise + message.should.equal("test-message") + + // Clean up + panel.dispose() + }) }) diff --git a/src/test/suite/extension.test.js b/src/test/suite/extension.test.js new file mode 100644 index 0000000000..f9d3305db0 --- /dev/null +++ b/src/test/suite/extension.test.js @@ -0,0 +1,37 @@ +const { expect } = require("chai") +const vscode = require("vscode") + +describe("Extension Tests", function () { + this.timeout(60000) // Increased timeout for extension operations + + it("should activate extension successfully", async () => { + // Get the extension + const extension = vscode.extensions.getExtension("saoudrizwan.claude-dev") + expect(extension).to.not.be.undefined + + // Activate the extension if not already activated + if (!extension.isActive) { + await extension.activate() + } + expect(extension.isActive).to.be.true + }) + + it("should open sidebar view", async () => { + // Execute the command to open sidebar + await vscode.commands.executeCommand("cline.plusButtonClicked") + + // Wait for sidebar to be visible + await new Promise((resolve) => setTimeout(resolve, 1000)) + + // Get all views + const views = vscode.window.visibleTextEditors + // Just verify the command executed without error + // The actual view verification is handled in the TypeScript tests + }) + + it("should handle basic commands", async () => { + // Test basic command execution + await vscode.commands.executeCommand("cline.historyButtonClicked") + // Success if no error thrown + }) +}) diff --git a/src/test/suite/index.js b/src/test/suite/index.js new file mode 100644 index 0000000000..36dccbf9f0 --- /dev/null +++ b/src/test/suite/index.js @@ -0,0 +1,43 @@ +const path = require("path") +const Mocha = require("mocha") +const glob = require("glob") + +async function run() { + // Create the mocha test + const mocha = new Mocha({ + ui: "bdd", + color: true, + timeout: 60000, // Increased timeout for extension operations + }) + + const testsRoot = path.resolve(__dirname, ".") + + try { + // Find all test files + const files = await glob("*.test.js", { cwd: testsRoot }) + + // Add files to the test suite + files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))) + + // Run the mocha test + return new Promise((resolve, reject) => { + try { + // Run the tests + mocha.run((failures) => { + if (failures > 0) { + reject(new Error(`${failures} tests failed.`)) + } else { + resolve() + } + }) + } catch (err) { + reject(err) + } + }) + } catch (err) { + console.error("Failed to run tests:", err) + throw err + } +} + +module.exports = { run } diff --git a/src/test/webview/chat-native.test.ts b/src/test/webview/chat-native.test.ts new file mode 100644 index 0000000000..775af4b7ae --- /dev/null +++ b/src/test/webview/chat-native.test.ts @@ -0,0 +1,116 @@ +import * as vscode from "vscode" +import { describe, it, beforeEach, afterEach } from "mocha" +import { strict as assert } from "assert" +import { join } from "path" +describe("Chat Integration Tests", () => { + let panel: vscode.WebviewPanel + let disposables: vscode.Disposable[] = [] + + beforeEach(async () => { + // Create VSCode webview panel + panel = vscode.window.createWebviewPanel("testWebview", "Chat Test", vscode.ViewColumn.One, { + enableScripts: true, + retainContextWhenHidden: true, + }) + + // Set up minimal test webview + panel.webview.html = ` + + + + + + + +
+ + + ` + }) + + afterEach(() => { + panel.dispose() + disposables.forEach((d) => d.dispose()) + disposables = [] + }) + + it("should send chat messages", async () => { + // Set up message listener + const messagePromise = new Promise((resolve) => { + panel.webview.onDidReceiveMessage((message) => { + if (message.type === "newTask") { + resolve(message) + } + }) + }) + + // Trigger send message + await panel.webview.postMessage({ + type: "sendMessage", + text: "Create a hello world app", + }) + + // Verify message was sent + const message = await messagePromise + assert.equal(message.type, "newTask") + assert.equal(message.text, "Create a hello world app") + }) + + it("should toggle between plan and act modes", async () => { + // Set up state change listener + const stateChangePromise = new Promise((resolve) => { + panel.webview.onDidReceiveMessage((message) => { + if (message.type === "chatSettings") { + resolve(message) + } + }) + }) + + // Trigger mode toggle + await panel.webview.postMessage({ type: "toggleMode" }) + + // Verify mode changed + const stateChange = await stateChangePromise + assert.equal(stateChange.chatSettings.mode, "act") + }) + + it("should handle tool approval flow", async () => { + // Set up approval listener + const approvalPromise = new Promise((resolve) => { + panel.webview.onDidReceiveMessage((message) => { + if (message.type === "askResponse") { + resolve(message) + } + }) + }) + + // Trigger tool approval + await panel.webview.postMessage({ + type: "invoke", + invoke: "primaryButtonClick", + }) + + // Verify approval was sent + const response = await approvalPromise + assert.equal(response.type, "askResponse") + assert.equal(response.askResponse, "yesButtonClicked") + }) +}) diff --git a/tsconfig.test.json b/tsconfig.test.json index 40ca1269d2..92f67542f5 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -8,7 +8,11 @@ "compilerOptions": { "module": "commonjs", "moduleResolution": "node", - "types": ["node", "mocha", "should", "vscode"] + "types": ["node", "mocha", "should", "vscode", "chai"], + "typeRoots": ["./node_modules/@types", "./src/test/types"], + "outDir": "out", + "rootDir": "src" }, - "include": ["src/**/*.test.ts"] + "include": ["src/**/*.test.ts"], + "exclude": ["src/test/**/*.js"] } diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index e9eae809f4..9d9f7796a5 100644 --- a/webview-ui/src/App.tsx +++ b/webview-ui/src/App.tsx @@ -5,6 +5,7 @@ import ChatView from "./components/chat/ChatView" 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 { vscode } from "./utils/vscode" import McpView from "./components/mcp/McpView" @@ -16,6 +17,7 @@ const AppContent = () => { const [showSettings, setShowSettings] = useState(false) const [showHistory, setShowHistory] = useState(false) const [showMcp, setShowMcp] = useState(false) + const [showAccount, setShowAccount] = useState(false) const [showAnnouncement, setShowAnnouncement] = useState(false) const handleMessage = useCallback((e: MessageEvent) => { @@ -27,21 +29,31 @@ const AppContent = () => { 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) setShowMcp(true) + setShowAccount(false) + break + case "accountLoginClicked": + setShowSettings(false) + setShowHistory(false) + setShowMcp(false) + setShowAccount(true) break case "chatButtonClicked": setShowSettings(false) setShowHistory(false) setShowMcp(false) + setShowAccount(false) break } break @@ -76,6 +88,7 @@ const AppContent = () => { {showSettings && setShowSettings(false)} />} {showHistory && setShowHistory(false)} />} {showMcp && setShowMcp(false)} />} + {showAccount && 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.) */} { @@ -83,7 +96,7 @@ const AppContent = () => { setShowMcp(false) setShowHistory(true) }} - isHidden={showSettings || showHistory || showMcp} + isHidden={showSettings || showHistory || showMcp || showAccount} showAnnouncement={showAnnouncement} hideAnnouncement={() => { setShowAnnouncement(false) diff --git a/webview-ui/src/components/account/AccountOptions.tsx b/webview-ui/src/components/account/AccountOptions.tsx new file mode 100644 index 0000000000..fc2c9ddd0c --- /dev/null +++ b/webview-ui/src/components/account/AccountOptions.tsx @@ -0,0 +1,15 @@ +import { memo } from "react" +import { vscode } from "../../utils/vscode" + +const AccountOptions = () => { + const handleAccountClick = () => { + vscode.postMessage({ type: "accountLoginClicked" }) + } + + // Call handleAccountClick immediately when component mounts + handleAccountClick() + + return null // This component doesn't render anything +} + +export default memo(AccountOptions) diff --git a/webview-ui/src/components/account/AccountView.tsx b/webview-ui/src/components/account/AccountView.tsx new file mode 100644 index 0000000000..d4d9ec5fb9 --- /dev/null +++ b/webview-ui/src/components/account/AccountView.tsx @@ -0,0 +1,83 @@ +import { VSCodeButton } from "@vscode/webview-ui-toolkit/react" +import { memo } from "react" +import { useExtensionState } from "../../context/ExtensionStateContext" +import { vscode } from "../../utils/vscode" + +type AccountViewProps = { + onDone: () => void +} + +const AccountView = ({ onDone }: AccountViewProps) => { + const { isLoggedIn, userInfo } = useExtensionState() + + const handleLogin = () => { + vscode.postMessage({ type: "accountLoginClicked" }) + } + + const handleLogout = () => { + vscode.postMessage({ type: "accountLogoutClicked" }) + } + + return ( +
+
+

Account

+ Done +
+
+
+ {isLoggedIn ? ( + <> + {userInfo?.photoURL && ( + Profile + )} +
+ {userInfo?.displayName &&
Name: {userInfo.displayName}
} + {userInfo?.email &&
Email: {userInfo.email}
} +
+ Log out + + ) : ( + Log in to Cline + )} +
+
+
+ ) +} + +export default memo(AccountView) diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index ce0a4180ca..b9fbcbd2aa 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -43,6 +43,8 @@ interface ChatTextAreaProps { onHeightChange?: (height: number) => void } +const PLAN_MODE_COLOR = "var(--vscode-inputValidation-warningBorder)" + const SwitchOption = styled.div<{ isActive: boolean }>` padding: 2px 8px; color: ${(props) => (props.isActive ? "white" : "var(--vscode-input-foreground)")}; @@ -69,13 +71,14 @@ const SwitchContainer = styled.div<{ disabled: boolean }>` transform: scale(0.85); transform-origin: right center; margin-left: -10px; // compensate for the transform so flex spacing works + user-select: none; // Prevent text selection ` -const Slider = styled.div<{ isAct: boolean }>` +const Slider = styled.div<{ isAct: boolean; isPlan?: boolean }>` position: absolute; height: 100%; width: 50%; - background-color: var(--vscode-focusBorder); + background-color: ${(props) => (props.isPlan ? PLAN_MODE_COLOR : "var(--vscode-focusBorder)")}; transition: transform 0.2s ease; transform: translateX(${(props) => (props.isAct ? "100%" : "0%")}); ` @@ -372,6 +375,7 @@ const ChatTextArea = forwardRef( const isComposing = event.nativeEvent?.isComposing ?? false if (event.key === "Enter" && !event.shiftKey && !isComposing) { event.preventDefault() + setIsTextAreaFocused(false) onSend() } @@ -742,6 +746,93 @@ const ChatTextArea = forwardRef( } }, [showModelSelector]) + /** + * Handles the drag over event to allow dropping. + * Prevents the default behavior to enable drop. + * + * @param {React.DragEvent} e - The drag event. + */ + const onDragOver = (e: React.DragEvent) => { + e.preventDefault() + } + + /** + * Handles the drop event for files and text. + * Processes dropped images and text, updating the state accordingly. + * + * @param {React.DragEvent} e - The drop event. + */ + const onDrop = async (e: React.DragEvent) => { + e.preventDefault() + + const files = Array.from(e.dataTransfer.files) + const text = e.dataTransfer.getData("text") + + if (text) { + handleTextDrop(text) + return + } + + const acceptedTypes = ["png", "jpeg", "webp"] + const imageFiles = files.filter((file) => { + const [type, subtype] = file.type.split("/") + return type === "image" && acceptedTypes.includes(subtype) + }) + + if (shouldDisableImages || imageFiles.length === 0) return + + const imageDataArray = await readImageFiles(imageFiles) + const dataUrls = imageDataArray.filter((dataUrl): dataUrl is string => dataUrl !== null) + + if (dataUrls.length > 0) { + setSelectedImages((prevImages) => [...prevImages, ...dataUrls].slice(0, MAX_IMAGES_PER_MESSAGE)) + } else { + console.warn("No valid images were processed") + } + } + + /** + * Handles the drop event for text. + * Inserts the dropped text at the current cursor position. + * + * @param {string} text - The dropped text. + */ + const handleTextDrop = (text: string) => { + const newValue = inputValue.slice(0, cursorPosition) + text + inputValue.slice(cursorPosition) + setInputValue(newValue) + const newCursorPosition = cursorPosition + text.length + setCursorPosition(newCursorPosition) + setIntendedCursorPosition(newCursorPosition) + } + + /** + * Reads image files and returns their data URLs. + * Uses FileReader to read the files as data URLs. + * + * @param {File[]} imageFiles - The image files to read. + * @returns {Promise<(string | null)[]>} - A promise that resolves to an array of data URLs or null values. + */ + const readImageFiles = (imageFiles: File[]): Promise<(string | null)[]> => { + return Promise.all( + imageFiles.map( + (file) => + new Promise((resolve) => { + const reader = new FileReader() + reader.onloadend = () => { + if (reader.error) { + console.error("Error reading file:", reader.error) + resolve(null) + } else { + const result = reader.result + resolve(typeof result === "string" ? result : null) + } + } + reader.readAsDataURL(file) + }), + ), + ) + } + return (
( opacity: textAreaDisabled ? 0.5 : 1, position: "relative", display: "flex", - }}> + }} + onDrop={onDrop} + onDragOver={onDragOver}> {showContextMenu && (
( }} /> { if (typeof ref === "function") { ref(el) @@ -862,6 +956,9 @@ const ChatTextArea = forwardRef( cursor: textAreaDisabled ? "not-allowed" : undefined, flex: 1, zIndex: 1, + outline: isTextAreaFocused + ? `1px solid ${chatSettings.mode === "plan" ? PLAN_MODE_COLOR : "var(--vscode-focusBorder)"}` + : "none", }} onScroll={() => updateHighlights()} /> @@ -909,9 +1006,11 @@ const ChatTextArea = forwardRef( }} /> */}
{ if (!textAreaDisabled) { + setIsTextAreaFocused(false) onSend() } }} @@ -923,6 +1022,7 @@ const ChatTextArea = forwardRef( ( ( - - + + Plan Act diff --git a/webview-ui/src/components/common/SettingsButton.tsx b/webview-ui/src/components/common/SettingsButton.tsx new file mode 100644 index 0000000000..2f63240c71 --- /dev/null +++ b/webview-ui/src/components/common/SettingsButton.tsx @@ -0,0 +1,36 @@ +import { VSCodeButton } from "@vscode/webview-ui-toolkit/react" +import styled from "styled-components" + +const StyledButton = styled(VSCodeButton)` + --settings-button-bg: var(--vscode-button-secondaryBackground); + --settings-button-hover: var(--vscode-button-secondaryHoverBackground); + --settings-button-active: var(--vscode-button-secondaryBackground); + + background-color: var(--settings-button-bg) !important; + border-color: var(--settings-button-bg) !important; + width: 100% !important; + + &:hover { + background-color: var(--settings-button-hover) !important; + border-color: var(--settings-button-hover) !important; + } + + &:active { + background-color: var(--settings-button-active) !important; + border-color: var(--settings-button-active) !important; + } + + i.codicon { + margin-right: 6px; + flex-shrink: 0; + font-size: 16px !important; + } +` + +interface SettingsButtonProps extends React.ComponentProps {} + +const SettingsButton: React.FC = (props) => { + return +} + +export default SettingsButton diff --git a/webview-ui/src/components/mcp/McpView.tsx b/webview-ui/src/components/mcp/McpView.tsx index 4c47faa7bb..b8afdbb05f 100644 --- a/webview-ui/src/components/mcp/McpView.tsx +++ b/webview-ui/src/components/mcp/McpView.tsx @@ -12,6 +12,7 @@ type McpViewProps = { const McpView = ({ onDone }: McpViewProps) => { const { mcpServers: servers } = useExtensionState() + // const [servers, setServers] = useState([ // // Add some mock servers for testing // { @@ -100,7 +101,7 @@ const McpView = ({ onDone }: McpViewProps) => { style={{ color: "var(--vscode-foreground)", fontSize: "13px", - marginBottom: "20px", + marginBottom: "16px", marginTop: "5px", }}> The{" "} @@ -118,7 +119,6 @@ const McpView = ({ onDone }: McpViewProps) => {
- {/* Server List */} {servers.length > 0 && (
{
)} - {/* Edit Settings Button */} + {/* Server Configuration Button */} +
{ onClick={() => { vscode.postMessage({ type: "openMcpSettings" }) }}> - - Edit MCP Settings + + Configure MCP Servers
+ {/* Advanced Settings Link */} +
+ { + vscode.postMessage({ + type: "openExtensionSettings", + text: "cline.mcp", + }) + }} + style={{ fontSize: "12px" }}> + Advanced MCP Settings + +
+ {/* Bottom padding */}
diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index f97f737aa2..0638c55bf5 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -52,9 +52,9 @@ interface ApiOptionsProps { // This is necessary to ensure dropdown opens downward, important for when this is used in popup const DROPDOWN_Z_INDEX = 1001 // Higher than the OpenRouterModelPicker's and ModelSelectorTooltip's z-index -const DropdownContainer = styled.div` +const DropdownContainer = styled.div<{ zIndex?: number }>` position: relative; - z-index: ${DROPDOWN_Z_INDEX}; + z-index: ${(props) => props.zIndex || DROPDOWN_Z_INDEX}; // Force dropdowns to open downward & vscode-dropdown::part(listbox) { @@ -403,7 +403,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is placeholder={t("enterAwsSessionToken")}> {t("awsSessionToken")} -
+ @@ -439,7 +439,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is us-gov-west-1 {/* us-gov-east-1 */} -
+ { @@ -476,7 +476,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is placeholder={t("enterGcpProjectId")}> {t("gcpProjectId")} -
+ @@ -492,7 +492,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is europe-west4 asia-southeast1 -
+

-

+ @@ -660,7 +660,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is }}> {t("experimentalFeature")}

-
+
)} @@ -798,7 +798,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is selectedProvider !== "vscode-lm" && showModelOptions && ( <> -
+ @@ -809,7 +809,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is {selectedProvider === "openai-native" && createDropdown(openAiNativeModels)} {selectedProvider === "deepseek" && createDropdown(deepSeekModels)} {selectedProvider === "mistral" && createDropdown(mistralModels)} -
+ { return new Intl.NumberFormat("en-US", { style: "currency", diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 75c0a3fed6..7f17aaed4b 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -6,6 +6,7 @@ import { validateApiConfiguration, validateModelId } from "../../utils/validate" import { vscode } from "../../utils/vscode" import ApiOptions from "./ApiOptions" import LanguageOptions from "./LanguageOptions" +import SettingsButton from "../common/SettingsButton" const IS_DEV = false // FIXME: use flags when packaging @@ -136,14 +137,29 @@ const SettingsView = ({ onDone }: SettingsViewProps) => { )} +
+ vscode.postMessage({ type: "openExtensionSettings" })} + style={{ + margin: "0 0 16px 0", + }}> + + Advanced Settings + +