mirror of
https://github.com/cline/cline.git
synced 2026-09-07 04:44:58 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7df5e936a6 |
@@ -2,4 +2,4 @@
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Integrate Claude Code
|
||||
updating o3 model pricing
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Fix list_files tool to return files if the targeted directory is a hidden directory
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Sorting mcp marketplace by newest listings by default
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
add ability to constrain size of terminal output
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Migrate focusChatInput message to protobus
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Exclude clinerules from checkpoints
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
chore(bedrock): remove @anthropic-ai/bedrock-sdk
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Update developer reset to allow for resetting workspace settings.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Added file context warnings to reduce diff edit errors when resuming a task after it has been restored
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Clear the chat input when the user changes mode within a task
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix(bedrock): remove Anthropic-Bedrock SDK
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Added a configurable default terminal profile setting
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Remove ‘-beta’ from grok model id
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add prompt caching indicator to grok 3
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fixed race condition where clineAsk was undefined, leading to task restoration and other downstream issues
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
update the copy button functionality
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix where clineMessages were not saving checkpoint commitHash on some messages
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Better debounce on checkmark control menu
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Add SAP AI Core as a provider for Cline
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Adding MCP Rich Display settings in cline's settings page, to enable users to change it in a persistent manner
|
||||
@@ -1,4 +1,4 @@
|
||||
blank_issues_enabled: false
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: ✨ Feature Request
|
||||
url: https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop
|
||||
@@ -6,3 +6,6 @@ contact_links:
|
||||
- name: 👋 Cline Discord
|
||||
url: https://discord.gg/cline
|
||||
about: Join our Discord community for discussions and support
|
||||
- name: ❓ Other Questions?
|
||||
url: https://x.com/sdrzn
|
||||
about: Contact the developer on X @sdrzn for other inquiries
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
name: 📝 Detailed Feature Proposal
|
||||
description: Propose a new feature or improvement
|
||||
labels: ["proposal"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Feature Proposal for Cline**
|
||||
|
||||
Thank you for creating a feature proposal for Cline! This template is for clear, actionable proposals that define a specific problem and a high-confidence solution. Please provide enough detail to enable fast prioritization, discussion, and execution.
|
||||
|
||||
Detailed proposals will be prioritized, while vague proposals may be closed or require extensive back and forth communication.
|
||||
|
||||
Before submitting:
|
||||
- Search existing [Issues](https://github.com/cline/cline/issues) and [Discussions](https://github.com/cline/cline/discussions) to avoid duplicates
|
||||
- Don’t start implementation until the proposal is reviewed and approved
|
||||
|
||||
- type: textarea
|
||||
id: problem-description
|
||||
attributes:
|
||||
label: What problem does this solve?
|
||||
description: |
|
||||
Describe the problem clearly from a user's point of view. Focus on why this matters, who it affects, and when it occurs.
|
||||
|
||||
✅ Solid:
|
||||
- "LLM provider returns 400 error when nearing the context window instead of truncating"
|
||||
- "Submit button is invisible in dark mode"
|
||||
|
||||
❌ Avoid:
|
||||
- "Performance is bad"
|
||||
- "UI needs work"
|
||||
|
||||
Your description should include:
|
||||
- Who is affected?
|
||||
- When does it happen?
|
||||
- What's the current vs expected behavior?
|
||||
- What is the impact?
|
||||
placeholder: Be specific about the problem, who it affects, and the impact.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: proposed-solution
|
||||
attributes:
|
||||
label: What’s the proposed solution?
|
||||
description: |
|
||||
Describe how the problem should be solved. Be specific about UX, system behavior, and any flows that would change.
|
||||
|
||||
✅ Solid:
|
||||
- "Add error handling immediately after attempting to create the llm stream and retry after manually truncating"
|
||||
- "Update button styling to ensure contrast in all themes"
|
||||
|
||||
❌ Avoid:
|
||||
- "Improve performance"
|
||||
- "Fix the bug"
|
||||
|
||||
Your solution should include:
|
||||
- What exactly will change?
|
||||
- How will users interact with it?
|
||||
- What’s the expected outcome?
|
||||
placeholder: Describe the proposed changes and how they solve the problem.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: acceptance-criteria
|
||||
attributes:
|
||||
label: How will we know it works? (Acceptance Criteria)
|
||||
description: Define clear, testable success criteria.
|
||||
placeholder: Provide specific and testable conditions for success.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: estimated-effort
|
||||
attributes:
|
||||
label: Estimated effort and complexity
|
||||
description: |
|
||||
Help us understand scope and risks. Include:
|
||||
|
||||
- Size estimate (XS/S/M/L/XL or hours/days)
|
||||
- Why this size? What’s technically involved?
|
||||
- Any tricky parts, refactors, or risks?
|
||||
- Performance or compatibility concerns?
|
||||
- Any dependencies on systems, teams, or libraries?
|
||||
placeholder: Size, reasoning, risks, and dependencies.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: technical-considerations
|
||||
attributes:
|
||||
label: Technical considerations, tradeoffs, and/or risks (optional)
|
||||
description: |
|
||||
Include any technical context that helps us evaluate or implement the proposal more effectively.
|
||||
|
||||
You may include:
|
||||
- Architectural changes or required refactors
|
||||
- Performance implications or system-level impacts
|
||||
- Known limitations, risks, or tricky edge cases
|
||||
- Compatibility concerns or migration steps
|
||||
- Alternative approaches you considered and why they were not chosen
|
||||
- Dependencies on other systems, teams, or libraries
|
||||
- Were other approaches considered? Why is this one preferred?
|
||||
placeholder: Technical considerations, tradeoffs, and/or risks.
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional context (optional)
|
||||
description: Diagrams, mockups, logs, links, or anything else that helps explain or justify the proposal.
|
||||
placeholder: Diagrams, mockups, logs, links, or anything else that helps explain or justify the proposal.
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Proposal checklist
|
||||
options:
|
||||
- label: I’ve checked for existing issues or related proposals
|
||||
required: true
|
||||
- label: I understand this needs review before implementation can start
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: willingness-to-contribute
|
||||
attributes:
|
||||
label: Interested in implementing this?
|
||||
description: Optional
|
||||
options:
|
||||
- label: Yes, I’d like to help implement this feature
|
||||
@@ -1,47 +1,10 @@
|
||||
<!--
|
||||
Thank you for contributing to Cline!
|
||||
|
||||
⚠️ Important: Before submitting this PR, please ensure you have:
|
||||
- Opened an issue and discussed your proposed changes with the community / contributors
|
||||
- Received approval from a core Cline contributor prior to proceeding with the implementation
|
||||
- Link the associated issue in the "Related Issue" section
|
||||
|
||||
Limited exceptions:
|
||||
Small bug fixes, typo corrections, minor wording improvements, or simple type fixes that don't change functionality may be submitted directly.
|
||||
|
||||
Why this requirement?
|
||||
We deeply appreciate all community contributions - they are the core reason we're able to operate successfully and keep innovating! We welcome community input and want to make it as easy as possible for people to submit quality work. This process helps our core maintainers review new ideas faster and saves contributor time by ensuring you have the go-ahead before spending time on implementation.
|
||||
-->
|
||||
|
||||
### Related Issue
|
||||
|
||||
<!-- Replace XXXX with the issue number that this PR addresses -->
|
||||
**Issue:** #XXXX
|
||||
|
||||
### Description
|
||||
|
||||
<!--
|
||||
Help reviewers understand your changes by making this PR readable and well-organized:
|
||||
|
||||
- What problem does this PR solve?
|
||||
- Why were these changes introduced and what purpose do they serve?
|
||||
- For larger changes, provide context about your approach and reasoning
|
||||
|
||||
Small PRs may need minimal description, but larger changes benefit from explaining where you're coming from. Much of this context can be in the linked issue above, so feel free to reference it rather than repeating everything here.
|
||||
-->
|
||||
<!-- Describe your changes in detail. What problem does this PR solve? -->
|
||||
|
||||
### Test Procedure
|
||||
|
||||
<!--
|
||||
Please walk us through your testing approach and thought process. This helps reviewers understand that you've thoroughly considered the impact of your changes:
|
||||
|
||||
- How did you test this change?
|
||||
- What could potentially break and how did you verify it doesn't?
|
||||
- What existing functionality might be affected and how did you check it still works?
|
||||
- Why are you confident this is ready for merge?
|
||||
|
||||
We're not looking for exhaustive documentation - just evidence that you've thought through the implications of your changes and tested accordingly.
|
||||
-->
|
||||
<!-- How did you test this? Are you confident that it will not introduce bugs? If so, why? -->
|
||||
|
||||
### Type of Change
|
||||
|
||||
@@ -66,15 +29,7 @@ We're not looking for exhaustive documentation - just evidence that you've thoug
|
||||
|
||||
### Screenshots
|
||||
|
||||
<!--
|
||||
Help reviewers quickly understand your changes:
|
||||
|
||||
- **UI Changes**: Please include screenshots showing before/after states
|
||||
- **Complex Workflows**: Consider uploading a screen recording (video) if your changes involve multiple steps or state transitions
|
||||
- **Backend Changes**: Not required, but feel free to include terminal output or other evidence that demonstrates functionality
|
||||
|
||||
This helps reviewers see what you've built without having to pull down and test your branch first.
|
||||
-->
|
||||
<!-- For UI changes, add screenshots here -->
|
||||
|
||||
### Additional Notes
|
||||
|
||||
|
||||
+3
-3
@@ -21,9 +21,7 @@ coverage
|
||||
|
||||
*evals.env
|
||||
|
||||
# Generated files
|
||||
src/generated/
|
||||
# Core
|
||||
# Generated proto files
|
||||
src/core/controller/*/methods.ts
|
||||
src/core/controller/*/index.ts
|
||||
src/core/controller/grpc-service-config.ts
|
||||
@@ -32,6 +30,8 @@ src/shared/proto/*.ts
|
||||
src/shared/proto/host/*.ts
|
||||
# Webview
|
||||
webview-ui/src/services/grpc-client.ts
|
||||
# Standalone
|
||||
src/standalone/server-setup.ts
|
||||
# Host bridge
|
||||
src/hosts/vscode/*/methods.ts
|
||||
src/hosts/vscode/*/index.ts
|
||||
|
||||
+4
-4
@@ -9,9 +9,9 @@ npm run lint || {
|
||||
|
||||
# Run Prettier
|
||||
echo "Running Prettier..."
|
||||
npx lint-staged --verbose || {
|
||||
echo "❌ Prettier failed. Please fix the errors and try committing again."
|
||||
exit 1
|
||||
}
|
||||
npm run format || {
|
||||
echo "❌ Prettier check failed. Run 'npm run format:fix' to automatically fix formatting issues."
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "✅ All checks passed!"
|
||||
|
||||
Vendored
+2
-3
@@ -42,7 +42,7 @@
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Run Standalone Service",
|
||||
"name": "Run Standalone Extension",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"sourceMaps": true,
|
||||
"resolveSourceMapLocations": ["${workspaceFolder}/**", "!**/node_modules/**"],
|
||||
@@ -53,8 +53,7 @@
|
||||
"GRPC_TRACE": "all",
|
||||
"GRPC_VERBOSITY": "DEBUG",
|
||||
"NODE_PATH": "${workspaceFolder}/dist-standalone/node_modules",
|
||||
"CLINE_DIR": "${userHome}/.cline-standalone",
|
||||
"HOST_BRIDGE_ADDRESS": "localhost:50052"
|
||||
"CLINE_DIR": "${userHome}/.cline-standalone"
|
||||
},
|
||||
"program": "standalone.js"
|
||||
}
|
||||
|
||||
Vendored
+2
-38
@@ -128,25 +128,7 @@
|
||||
"type": "npm",
|
||||
"script": "watch:esbuild",
|
||||
"group": "build",
|
||||
"problemMatcher": {
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "^✘ \\[ERROR\\] (.*)$",
|
||||
"message": 1
|
||||
},
|
||||
{
|
||||
"regexp": "^\\s+(.*):(\\d+):(\\d+):$",
|
||||
"file": 1,
|
||||
"line": 2,
|
||||
"column": 3
|
||||
}
|
||||
],
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": "^\\[watch\\] build started$",
|
||||
"endsPattern": "^\\[watch\\] build finished$"
|
||||
}
|
||||
},
|
||||
"problemMatcher": "$esbuild-watch",
|
||||
"isBackground": true,
|
||||
"label": "npm: watch:esbuild",
|
||||
"dependsOn": ["npm: protos"],
|
||||
@@ -164,25 +146,7 @@
|
||||
"type": "npm",
|
||||
"script": "watch:esbuild:test",
|
||||
"group": "build",
|
||||
"problemMatcher": {
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "^✘ \\[ERROR\\] (.*)$",
|
||||
"message": 1
|
||||
},
|
||||
{
|
||||
"regexp": "^\\s+(.*):(\\d+):(\\d+):$",
|
||||
"file": 1,
|
||||
"line": 2,
|
||||
"column": 3
|
||||
}
|
||||
],
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": "^\\[watch\\] build started$",
|
||||
"endsPattern": "^\\[watch\\] build finished$"
|
||||
}
|
||||
},
|
||||
"problemMatcher": "$esbuild-watch",
|
||||
"isBackground": true,
|
||||
"label": "npm: watch:esbuild:test",
|
||||
"dependsOn": ["npm: protos"],
|
||||
|
||||
@@ -1,17 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [3.17.13]
|
||||
|
||||
- Add Thinking UX for Gemini models, providing visual feedback during model reasoning
|
||||
- Add support for Notifications MCP integration with Cline
|
||||
- Add prompt caching indicator for Grok 3 models
|
||||
- Sort MCP marketplace by newest listings by default for easier discovery of recent servers
|
||||
- Update O3 model family pricing to reflect latest OpenAI rates
|
||||
- Remove '-beta' suffix from Grok model identifiers
|
||||
- Fix AWS Bedrock provider by removing deprecated Anthropic-Bedrock SDK (Thanks @watany-dev!)
|
||||
- Fix menu display issue for terminal timeout settings
|
||||
- Improve chat input field styling and behavior
|
||||
|
||||
## [3.17.12]
|
||||
|
||||
- **Free Grok Model Available!** Access Grok 3 completely free through the Cline provider
|
||||
|
||||
@@ -18,18 +18,6 @@ We also welcome contributions to our [documentation](https://github.com/cline/cl
|
||||
|
||||
If you're planning to work on a bigger feature, please create a [feature request](https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop) first so we can discuss whether it aligns with Cline's vision.
|
||||
|
||||
## Before Contributing
|
||||
|
||||
All contributions must begin with a GitHub Issue, unless the change is for small bug fixes, typo corrections, minor wording improvements, or simple type fixes that don't change functionality.
|
||||
|
||||
- **Check existing issues**: Search [GitHub Issues](https://github.com/cline/cline/issues).
|
||||
- **Create an issue**: Use appropriate templates:
|
||||
- **Bugs:** "Bug Report" template.
|
||||
- **Features:** "Detailed Feature Proposal" template. Approval from a core Cline contributor required before starting.
|
||||
- **Claim issues**: Comment your interest.
|
||||
|
||||
**PRs without approved issues may be closed.**
|
||||
|
||||
## Development Setup
|
||||
|
||||
1. **VS Code Extensions**
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
version: v2
|
||||
modules:
|
||||
- path: proto
|
||||
name: cline/cline/lint
|
||||
|
||||
lint:
|
||||
use:
|
||||
- STANDARD
|
||||
|
||||
except: # Add exceptions for current patterns that contradict STANDARD settings
|
||||
- RPC_PASCAL_CASE # rpcs are camel case (start with lowercase)
|
||||
- PACKAGE_DIRECTORY_MATCH # the protos in the cline package are not in a dir named cline.
|
||||
- RPC_REQUEST_RESPONSE_UNIQUE # request messages are not unique.
|
||||
- RPC_REQUEST_STANDARD_NAME # request messages dont all end with Request
|
||||
- RPC_RESPONSE_STANDARD_NAME # response messages dont all end with Response
|
||||
- PACKAGE_VERSION_SUFFIX # package name does not contain version.
|
||||
- ENUM_VALUE_PREFIX # enum values dont start with the enum name.
|
||||
- ENUM_ZERO_VALUE_SUFFIX # first value does not have to be UNSPECIFIED.
|
||||
|
||||
# breaking:
|
||||
# use:
|
||||
# - WIRE_JSON # Detect changes that break the json wire format (this is the minimum recommended level.)
|
||||
Generated
+8614
-3312
File diff suppressed because it is too large
Load Diff
+6
-14
@@ -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.17.13",
|
||||
"version": "3.17.12",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
@@ -331,12 +331,12 @@
|
||||
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
|
||||
"package": "npm run check-types && npm run build:webview && npm run lint && node esbuild.js --production",
|
||||
"protos": "node proto/build-proto.js && node scripts/generate-server-setup.mjs",
|
||||
"postprotos": "prettier src/shared/proto src/core/controller src/hosts/ webview-ui/src/services src/generated --write --log-level warn",
|
||||
"postprotos": "prettier src/shared/proto src/core/controller src/hosts/ webview-ui/src/services src/standalone/server-setup.ts --write --log-level silent",
|
||||
"compile-tests": "node ./scripts/build-tests.js",
|
||||
"watch-tests": "tsc -p . -w --outDir out",
|
||||
"pretest": "npm run compile-tests && npm run compile && npm run compile-standalone && npm run lint",
|
||||
"check-types": "npm run protos && tsc --noEmit",
|
||||
"lint": "eslint src --ext ts && eslint webview-ui/src --ext ts && buf lint && cd webview-ui && npm run lint",
|
||||
"lint": "eslint src --ext ts && eslint webview-ui/src --ext ts && cd webview-ui && npm run lint",
|
||||
"format": "prettier . --check",
|
||||
"format:fix": "prettier . --write",
|
||||
"test": "npm-run-all test:unit test:integration",
|
||||
@@ -358,13 +358,7 @@
|
||||
"docs:rename-file": "cd docs && mintlify rename",
|
||||
"report-issue": "node scripts/report-issue.js"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*": [
|
||||
"prettier --write --ignore-unknown --log-level=log"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@bufbuild/buf": "^1.54.0",
|
||||
"@changesets/cli": "^2.27.12",
|
||||
"@types/chai": "^5.0.1",
|
||||
"@types/clone-deep": "^4.0.4",
|
||||
@@ -390,7 +384,6 @@
|
||||
"eslint-plugin-eslint-rules": "file:eslint-rules",
|
||||
"grpc-tools": "^1.13.0",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^16.1.0",
|
||||
"mintlify": "^4.0.515",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.3.3",
|
||||
@@ -404,14 +397,14 @@
|
||||
"typescript": "^5.4.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/bedrock-sdk": "^0.12.4",
|
||||
"@anthropic-ai/sdk": "^0.37.0",
|
||||
"@anthropic-ai/vertex-sdk": "^0.6.4",
|
||||
"@aws-sdk/client-bedrock-runtime": "^3.826.0",
|
||||
"@aws-sdk/credential-providers": "^3.826.0",
|
||||
"@aws-sdk/client-bedrock-runtime": "^3.821.0",
|
||||
"@bufbuild/protobuf": "^2.2.5",
|
||||
"@cerebras/cerebras_cloud_sdk": "^1.35.0",
|
||||
"@google-cloud/vertexai": "^1.9.3",
|
||||
"@google/genai": "1.0.0",
|
||||
"@google/genai": "^0.13.0",
|
||||
"@grpc/grpc-js": "^1.9.15",
|
||||
"@grpc/reflection": "^1.0.4",
|
||||
"@mistralai/mistralai": "^1.5.0",
|
||||
@@ -448,7 +441,6 @@
|
||||
"jschardet": "^3.1.4",
|
||||
"mammoth": "^1.8.0",
|
||||
"monaco-vscode-textmate-theme-converter": "^0.1.7",
|
||||
"nice-grpc": "^2.1.12",
|
||||
"ollama": "^0.5.13",
|
||||
"open": "^10.1.2",
|
||||
"open-graph-scraper": "^6.9.0",
|
||||
|
||||
+67
-87
@@ -10,26 +10,25 @@ import os from "os"
|
||||
|
||||
import { createRequire } from "module"
|
||||
const require = createRequire(import.meta.url)
|
||||
const PROTOC = path.join(require.resolve("grpc-tools"), "../bin/protoc")
|
||||
const protoc = path.join(require.resolve("grpc-tools"), "../bin/protoc")
|
||||
|
||||
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url))
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const SCRIPT_DIR = path.dirname(__filename)
|
||||
const ROOT_DIR = path.resolve(SCRIPT_DIR, "..")
|
||||
|
||||
const TS_OUT_DIR = path.join(ROOT_DIR, "src/shared/proto")
|
||||
const GRPC_JS_OUT_DIR = path.join(ROOT_DIR, "src/generated/grpc-js")
|
||||
const NICE_JS_OUT_DIR = path.join(ROOT_DIR, "src/generated/nice-grpc")
|
||||
const DESCRIPTOR_OUT_DIR = path.join(ROOT_DIR, "dist-standalone/proto")
|
||||
const TS_OUT_DIR = path.join(ROOT_DIR, "src", "shared", "proto")
|
||||
|
||||
const isWindows = process.platform === "win32"
|
||||
const TS_PROTO_PLUGIN = isWindows
|
||||
const tsProtoPlugin = isWindows
|
||||
? path.join(ROOT_DIR, "node_modules", ".bin", "protoc-gen-ts_proto.cmd") // Use the .bin directory path for Windows
|
||||
: require.resolve("ts-proto/protoc-gen-ts_proto")
|
||||
|
||||
const TS_PROTO_OPTIONS = [
|
||||
"env=node",
|
||||
"esModuleInterop=true",
|
||||
"outputServices=generic-definitions", // output generic ServiceDefinitions
|
||||
|
||||
"outputIndex=true", // output an index file for each package which exports all protos in the package.
|
||||
"outputServices=generic-definitions",
|
||||
|
||||
"useOptionals=messages", // Message fields are optional, scalars are not.
|
||||
"useDate=false", // Timestamp fields will not be automatically converted to Date.
|
||||
]
|
||||
@@ -51,7 +50,7 @@ const serviceNameMap = {
|
||||
ui: "cline.UiService",
|
||||
// Add new services here - no other code changes needed!
|
||||
}
|
||||
const serviceDirs = Object.keys(serviceNameMap).map((serviceKey) => path.join(ROOT_DIR, "src/core/controller", serviceKey))
|
||||
const serviceDirs = Object.keys(serviceNameMap).map((serviceKey) => path.join(ROOT_DIR, "src", "core", "controller", serviceKey))
|
||||
|
||||
// List of host gRPC services (IDE API bridge)
|
||||
// These services are implemented in the IDE extension and called by the standalone Cline Core
|
||||
@@ -60,7 +59,9 @@ const hostServiceNameMap = {
|
||||
watch: "host.WatchService",
|
||||
// Add new host services here
|
||||
}
|
||||
const hostServiceDirs = Object.keys(hostServiceNameMap).map((serviceKey) => path.join(ROOT_DIR, "src/hosts/vscode", serviceKey))
|
||||
const hostServiceDirs = Object.keys(hostServiceNameMap).map((serviceKey) =>
|
||||
path.join(ROOT_DIR, "src", "hosts", "vscode", serviceKey),
|
||||
)
|
||||
|
||||
async function main() {
|
||||
console.log(chalk.bold.blue("Starting Protocol Buffer code generation..."))
|
||||
@@ -69,43 +70,53 @@ async function main() {
|
||||
checkAppleSiliconCompatibility()
|
||||
|
||||
// Create output directories if they don't exist
|
||||
for (const dir of [TS_OUT_DIR, GRPC_JS_OUT_DIR, NICE_JS_OUT_DIR, DESCRIPTOR_OUT_DIR]) {
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
}
|
||||
|
||||
await fs.mkdir(TS_OUT_DIR, { recursive: true })
|
||||
await cleanup()
|
||||
|
||||
// Check for missing proto files for services in serviceNameMap
|
||||
await ensureProtoFilesExist()
|
||||
|
||||
// Process all proto files
|
||||
console.log(chalk.cyan("Processing proto files from"), SCRIPT_DIR)
|
||||
const protoFiles = await globby("**/*.proto", { cwd: SCRIPT_DIR, realpath: true })
|
||||
console.log(chalk.cyan(`Processing ${protoFiles.length} proto files from`), SCRIPT_DIR)
|
||||
|
||||
tsProtoc(TS_OUT_DIR, protoFiles, TS_PROTO_OPTIONS)
|
||||
// grpc-js is used to generate service impls for the ProtoBus service.
|
||||
tsProtoc(GRPC_JS_OUT_DIR, protoFiles, ["outputServices=grpc-js,outputClientImpl=false", ...TS_PROTO_OPTIONS])
|
||||
// nice-js is used for the Host Bridge client impls because it uses promises.
|
||||
tsProtoc(NICE_JS_OUT_DIR, protoFiles, ["outputServices=nice-grpc,useExactTypes=false", ...TS_PROTO_OPTIONS])
|
||||
// Build the protoc command with proper path handling for cross-platform
|
||||
const tsProtocCommand = [
|
||||
protoc,
|
||||
`--proto_path="${SCRIPT_DIR}"`,
|
||||
`--plugin=protoc-gen-ts_proto="${tsProtoPlugin}"`,
|
||||
`--ts_proto_out="${TS_OUT_DIR}"`,
|
||||
`--ts_proto_opt=${TS_PROTO_OPTIONS.join(",")} `,
|
||||
...protoFiles,
|
||||
].join(" ")
|
||||
try {
|
||||
console.log(chalk.cyan(`Generating TypeScript code for:\n${protoFiles.join("\n")}...`))
|
||||
execSync(tsProtocCommand, { stdio: "inherit" })
|
||||
} catch (error) {
|
||||
console.error(chalk.red("Error generating TypeScript for proto files:"), error)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const descriptorFile = path.join(DESCRIPTOR_OUT_DIR, "descriptor_set.pb")
|
||||
const descriptorOutDir = path.join(ROOT_DIR, "dist-standalone", "proto")
|
||||
await fs.mkdir(descriptorOutDir, { recursive: true })
|
||||
const descriptorFile = path.join(descriptorOutDir, "descriptor_set.pb")
|
||||
const descriptorProtocCommand = [
|
||||
PROTOC,
|
||||
protoc,
|
||||
`--proto_path="${SCRIPT_DIR}"`,
|
||||
`--descriptor_set_out="${descriptorFile}"`,
|
||||
"--include_imports",
|
||||
...protoFiles,
|
||||
].join(" ")
|
||||
try {
|
||||
log_verbose(chalk.cyan("Generating descriptor set..."))
|
||||
console.log(chalk.cyan("Generating descriptor set..."))
|
||||
execSync(descriptorProtocCommand, { stdio: "inherit" })
|
||||
} catch (error) {
|
||||
console.error(chalk.red("Error generating descriptor set for proto file:"), error)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
log_verbose(chalk.green("Protocol Buffer code generation completed successfully."))
|
||||
log_verbose(chalk.green(`TypeScript files generated in: ${TS_OUT_DIR}`))
|
||||
console.log(chalk.green("Protocol Buffer code generation completed successfully."))
|
||||
console.log(chalk.green(`TypeScript files generated in: ${TS_OUT_DIR}`))
|
||||
|
||||
await generateMethodRegistrations()
|
||||
await generateHostMethodRegistrations()
|
||||
@@ -113,28 +124,6 @@ async function main() {
|
||||
await generateHostServiceConfig()
|
||||
await generateGrpcClientConfig()
|
||||
await generateHostGrpcClientConfig()
|
||||
|
||||
console.log(chalk.bold.blue("Finished Protocol Buffer code generation."))
|
||||
}
|
||||
|
||||
async function tsProtoc(outDir, protoFiles, protoOptions) {
|
||||
// Build the protoc command with proper path handling for cross-platform
|
||||
const command = [
|
||||
PROTOC,
|
||||
`--proto_path="${SCRIPT_DIR}"`,
|
||||
`--plugin=protoc-gen-ts_proto="${TS_PROTO_PLUGIN}"`,
|
||||
`--ts_proto_out="${outDir}"`,
|
||||
`--ts_proto_opt=${protoOptions.join(",")} `,
|
||||
...protoFiles.map((s) => `"${s}"`),
|
||||
].join(" ")
|
||||
try {
|
||||
log_verbose(chalk.cyan(`Generating TypeScript code in ${outDir} for:\n${protoFiles.join("\n")}...`))
|
||||
log_verbose(command)
|
||||
execSync(command, { stdio: "inherit" })
|
||||
} catch (error) {
|
||||
console.error(chalk.red("Error generating TypeScript for proto files:"), error)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,14 +131,14 @@ async function tsProtoc(outDir, protoFiles, protoOptions) {
|
||||
* This eliminates the need for manual imports and client creation in grpc-client.ts
|
||||
*/
|
||||
async function generateGrpcClientConfig() {
|
||||
log_verbose(chalk.cyan("Generating gRPC client configuration..."))
|
||||
console.log(chalk.cyan("Generating gRPC client configuration..."))
|
||||
|
||||
const serviceImports = []
|
||||
const serviceClientCreations = []
|
||||
const serviceExports = []
|
||||
|
||||
// Process each service in the serviceNameMap
|
||||
for (const [dirName, _fullServiceName] of Object.entries(serviceNameMap)) {
|
||||
for (const [dirName, fullServiceName] of Object.entries(serviceNameMap)) {
|
||||
const capitalizedName = dirName.charAt(0).toUpperCase() + dirName.slice(1)
|
||||
|
||||
// Add import statement
|
||||
@@ -179,7 +168,7 @@ export {
|
||||
|
||||
const configPath = path.join(ROOT_DIR, "webview-ui", "src", "services", "grpc-client.ts")
|
||||
await fs.writeFile(configPath, content)
|
||||
log_verbose(chalk.green(`Generated gRPC client at ${configPath}`))
|
||||
console.log(chalk.green(`Generated gRPC client at ${configPath}`))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -189,7 +178,7 @@ export {
|
||||
* @returns Map of service names to their streaming methods
|
||||
*/
|
||||
async function parseProtoForStreamingMethods(protoFiles, scriptDir) {
|
||||
log_verbose(chalk.cyan("Parsing proto files for streaming methods..."))
|
||||
console.log(chalk.cyan("Parsing proto files for streaming methods..."))
|
||||
|
||||
// Map of service name to array of streaming method names
|
||||
const streamingMethodsMap = new Map()
|
||||
@@ -241,7 +230,7 @@ async function parseProtoForStreamingMethods(protoFiles, scriptDir) {
|
||||
}
|
||||
|
||||
async function generateMethodRegistrations() {
|
||||
log_verbose(chalk.cyan("Generating method registration files..."))
|
||||
console.log(chalk.cyan("Generating method registration files..."))
|
||||
|
||||
// Parse proto files for streaming methods
|
||||
const protoFiles = await globby("*.proto", { cwd: SCRIPT_DIR })
|
||||
@@ -251,7 +240,7 @@ async function generateMethodRegistrations() {
|
||||
try {
|
||||
await fs.access(serviceDir)
|
||||
} catch (error) {
|
||||
log_verbose(chalk.cyan(`Creating directory ${serviceDir} for new service`))
|
||||
console.log(chalk.cyan(`Creating directory ${serviceDir} for new service`))
|
||||
await fs.mkdir(serviceDir, { recursive: true })
|
||||
}
|
||||
|
||||
@@ -262,7 +251,7 @@ async function generateMethodRegistrations() {
|
||||
const fullServiceName = serviceNameMap[serviceName]
|
||||
const streamingMethods = streamingMethodsMap.get(fullServiceName) || []
|
||||
|
||||
log_verbose(chalk.cyan(`Generating method registrations for ${serviceName}...`))
|
||||
console.log(chalk.cyan(`Generating method registrations for ${serviceName}...`))
|
||||
|
||||
// Get all TypeScript files in the service directory
|
||||
const files = await globby("*.ts", { cwd: serviceDir })
|
||||
@@ -315,7 +304,7 @@ export function registerAllMethods(): void {
|
||||
|
||||
// Write the methods.ts file
|
||||
await fs.writeFile(registryFile, methodsContent)
|
||||
log_verbose(chalk.green(`Generated ${registryFile}`))
|
||||
console.log(chalk.green(`Generated ${registryFile}`))
|
||||
|
||||
// Generate index.ts file
|
||||
const capitalizedServiceName = serviceName.charAt(0).toUpperCase() + serviceName.slice(1)
|
||||
@@ -344,10 +333,10 @@ registerAllMethods()`
|
||||
|
||||
// Write the index.ts file
|
||||
await fs.writeFile(indexFile, indexContent)
|
||||
log_verbose(chalk.green(`Generated ${indexFile}`))
|
||||
console.log(chalk.green(`Generated ${indexFile}`))
|
||||
}
|
||||
|
||||
log_verbose(chalk.green("Method registration files generated successfully."))
|
||||
console.log(chalk.green("Method registration files generated successfully."))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -355,7 +344,7 @@ registerAllMethods()`
|
||||
* This eliminates the need for manual switch/case statements in grpc-handler.ts
|
||||
*/
|
||||
async function generateServiceConfig() {
|
||||
log_verbose(chalk.cyan("Generating service configuration file..."))
|
||||
console.log(chalk.cyan("Generating service configuration file..."))
|
||||
|
||||
const serviceImports = []
|
||||
const serviceConfigs = []
|
||||
@@ -396,7 +385,7 @@ export const serviceHandlers: Record<string, ServiceHandlerConfig> = {${serviceC
|
||||
|
||||
const configPath = path.join(ROOT_DIR, "src", "core", "controller", "grpc-service-config.ts")
|
||||
await fs.writeFile(configPath, content)
|
||||
log_verbose(chalk.green(`Generated service configuration at ${configPath}`))
|
||||
console.log(chalk.green(`Generated service configuration at ${configPath}`))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -404,7 +393,7 @@ export const serviceHandlers: Record<string, ServiceHandlerConfig> = {${serviceC
|
||||
* If a .proto file doesn't exist, create a template file
|
||||
*/
|
||||
async function ensureProtoFilesExist() {
|
||||
log_verbose(chalk.cyan("Checking for missing proto files..."))
|
||||
console.log(chalk.cyan("Checking for missing proto files..."))
|
||||
|
||||
// Get existing proto files
|
||||
const existingProtoFiles = await globby("*.proto", { cwd: SCRIPT_DIR })
|
||||
@@ -413,7 +402,7 @@ async function ensureProtoFilesExist() {
|
||||
// Check each service in serviceNameMap
|
||||
for (const [serviceName, fullServiceName] of Object.entries(serviceNameMap)) {
|
||||
if (!existingProtoServices.includes(serviceName)) {
|
||||
log_verbose(chalk.yellow(`Creating template proto file for ${serviceName}...`))
|
||||
console.log(chalk.yellow(`Creating template proto file for ${serviceName}...`))
|
||||
|
||||
// Extract service class name from full name (e.g., "cline.ModelsService" -> "ModelsService")
|
||||
const serviceClassName = fullServiceName.split(".").pop()
|
||||
@@ -446,7 +435,7 @@ service ${serviceClassName} {
|
||||
// Write the template proto file
|
||||
const protoFilePath = path.join(SCRIPT_DIR, `${serviceName}.proto`)
|
||||
await fs.writeFile(protoFilePath, protoContent)
|
||||
log_verbose(chalk.green(`Created template proto file at ${protoFilePath}`))
|
||||
console.log(chalk.green(`Created template proto file at ${protoFilePath}`))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -455,7 +444,7 @@ service ${serviceClassName} {
|
||||
* Generate method registration files for host services
|
||||
*/
|
||||
async function generateHostMethodRegistrations() {
|
||||
log_verbose(chalk.cyan("Generating host method registration files..."))
|
||||
console.log(chalk.cyan("Generating host method registration files..."))
|
||||
|
||||
// Parse proto files for streaming methods
|
||||
const hostProtoFiles = await globby("*.proto", { cwd: path.join(SCRIPT_DIR, "host") })
|
||||
@@ -465,7 +454,7 @@ async function generateHostMethodRegistrations() {
|
||||
try {
|
||||
await fs.access(serviceDir)
|
||||
} catch (error) {
|
||||
log_verbose(chalk.cyan(`Creating directory ${serviceDir} for new host service`))
|
||||
console.log(chalk.cyan(`Creating directory ${serviceDir} for new host service`))
|
||||
await fs.mkdir(serviceDir, { recursive: true })
|
||||
}
|
||||
|
||||
@@ -476,7 +465,7 @@ async function generateHostMethodRegistrations() {
|
||||
const fullServiceName = hostServiceNameMap[serviceName]
|
||||
const streamingMethods = streamingMethodsMap.get(fullServiceName) || []
|
||||
|
||||
log_verbose(chalk.cyan(`Generating method registrations for host ${serviceName}...`))
|
||||
console.log(chalk.cyan(`Generating method registrations for host ${serviceName}...`))
|
||||
|
||||
// Get all TypeScript files in the service directory
|
||||
const files = await globby("*.ts", { cwd: serviceDir })
|
||||
@@ -529,7 +518,7 @@ export function registerAllMethods(): void {
|
||||
|
||||
// Write the methods.ts file
|
||||
await fs.writeFile(registryFile, methodsContent)
|
||||
log_verbose(chalk.green(`Generated ${registryFile}`))
|
||||
console.log(chalk.green(`Generated ${registryFile}`))
|
||||
|
||||
// Generate index.ts file
|
||||
const capitalizedServiceName = serviceName.charAt(0).toUpperCase() + serviceName.slice(1)
|
||||
@@ -558,17 +547,17 @@ registerAllMethods()`
|
||||
|
||||
// Write the index.ts file
|
||||
await fs.writeFile(indexFile, indexContent)
|
||||
log_verbose(chalk.green(`Generated ${indexFile}`))
|
||||
console.log(chalk.green(`Generated ${indexFile}`))
|
||||
}
|
||||
|
||||
log_verbose(chalk.green("Host method registration files generated successfully."))
|
||||
console.log(chalk.green("Host method registration files generated successfully."))
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a service configuration file for host services
|
||||
*/
|
||||
async function generateHostServiceConfig() {
|
||||
log_verbose(chalk.cyan("Generating host service configuration file..."))
|
||||
console.log(chalk.cyan("Generating host service configuration file..."))
|
||||
|
||||
const serviceImports = []
|
||||
const serviceConfigs = []
|
||||
@@ -609,21 +598,21 @@ export const hostServiceHandlers: Record<string, HostServiceHandlerConfig> = {${
|
||||
const configPath = path.join(ROOT_DIR, "src", "hosts", "vscode", "host-grpc-service-config.ts")
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true })
|
||||
await fs.writeFile(configPath, content)
|
||||
log_verbose(chalk.green(`Generated host service configuration at ${configPath}`))
|
||||
console.log(chalk.green(`Generated host service configuration at ${configPath}`))
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a gRPC client configuration file for host services
|
||||
*/
|
||||
async function generateHostGrpcClientConfig() {
|
||||
log_verbose(chalk.cyan("Generating host gRPC client configuration..."))
|
||||
console.log(chalk.cyan("Generating host gRPC client configuration..."))
|
||||
|
||||
const serviceImports = []
|
||||
const serviceClientCreations = []
|
||||
const serviceExports = []
|
||||
|
||||
// Process each service in the hostServiceNameMap
|
||||
for (const [dirName, _fullServiceName] of Object.entries(hostServiceNameMap)) {
|
||||
for (const [dirName, fullServiceName] of Object.entries(hostServiceNameMap)) {
|
||||
const capitalizedName = dirName.charAt(0).toUpperCase() + dirName.slice(1)
|
||||
|
||||
// Add import statement
|
||||
@@ -654,25 +643,22 @@ export {
|
||||
const configPath = path.join(ROOT_DIR, "src", "hosts", "vscode", "client", "host-grpc-client.ts")
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true })
|
||||
await fs.writeFile(configPath, content)
|
||||
log_verbose(chalk.green(`Generated host gRPC client at ${configPath}`))
|
||||
console.log(chalk.green(`Generated host gRPC client at ${configPath}`))
|
||||
}
|
||||
|
||||
async function cleanup() {
|
||||
// Clean up existing generated files
|
||||
log_verbose(chalk.cyan("Cleaning up existing generated TypeScript files..."))
|
||||
console.log(chalk.cyan("Cleaning up existing generated TypeScript files..."))
|
||||
const existingFiles = await globby("**/*.ts", { cwd: TS_OUT_DIR })
|
||||
for (const file of existingFiles) {
|
||||
await fs.unlink(path.join(TS_OUT_DIR, file))
|
||||
}
|
||||
|
||||
// Clean up generated files that were moved.
|
||||
await fs.rm(path.join(ROOT_DIR, "src/standalone/services/host-grpc-client.ts"), { force: true })
|
||||
await rmdir(path.join(ROOT_DIR, "src/standalone/services"))
|
||||
|
||||
await fs.rm(path.join(ROOT_DIR, "hosts/vscode"), { force: true, recursive: true })
|
||||
await fs.rm(path.join(ROOT_DIR, "src", "standalone", "services", "host-grpc-client.ts"), { force: true })
|
||||
await rmdir(path.join(ROOT_DIR, "src", "standalone", "services"))
|
||||
await fs.rm(path.join(ROOT_DIR, "hosts", "vscode"), { force: true, recursive: true })
|
||||
await rmdir(path.join(ROOT_DIR, "hosts"))
|
||||
|
||||
await fs.rm(path.join(ROOT_DIR, "src/standalone/server-setup.ts"), { force: true })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -719,12 +705,6 @@ function checkAppleSiliconCompatibility() {
|
||||
}
|
||||
}
|
||||
|
||||
function log_verbose(s) {
|
||||
if (process.argv.includes("-v") || process.argv.includes("--verbose")) {
|
||||
console.log(s)
|
||||
}
|
||||
}
|
||||
|
||||
// Run the main function
|
||||
main().catch((error) => {
|
||||
console.error(chalk.red("Error:"), error)
|
||||
|
||||
@@ -25,12 +25,12 @@ message Uri {
|
||||
string path = 3;
|
||||
string query = 4;
|
||||
string fragment = 5;
|
||||
string fs_path = 6;
|
||||
string fsPath = 6;
|
||||
}
|
||||
|
||||
// Request for joining path segments to a URI
|
||||
message JoinPathRequest {
|
||||
cline.Metadata metadata = 1;
|
||||
Uri base = 2;
|
||||
repeated string path_segments = 3;
|
||||
repeated string pathSegments = 3;
|
||||
}
|
||||
|
||||
+9
-160
@@ -22,21 +22,19 @@ service ModelsService {
|
||||
rpc refreshRequestyModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
// Subscribe to OpenRouter models updates
|
||||
rpc subscribeToOpenRouterModels(EmptyRequest) returns (stream OpenRouterCompatibleModelInfo);
|
||||
// Updates API configuration
|
||||
rpc updateApiConfigurationProto(UpdateApiConfigurationRequest) returns (Empty);
|
||||
}
|
||||
|
||||
// List of VS Code LM models
|
||||
message VsCodeLmModelsArray {
|
||||
repeated LanguageModelChatSelector models = 1;
|
||||
repeated VsCodeLmModel models = 1;
|
||||
}
|
||||
|
||||
// Structure representing a language model chat selector
|
||||
message LanguageModelChatSelector {
|
||||
optional string vendor = 1;
|
||||
optional string family = 2;
|
||||
optional string version = 3;
|
||||
optional string id = 4;
|
||||
// Structure representing a VS Code LM model
|
||||
message VsCodeLmModel {
|
||||
string vendor = 1;
|
||||
string family = 2;
|
||||
string version = 3;
|
||||
string id = 4;
|
||||
}
|
||||
|
||||
// Price tier for tiered pricing models
|
||||
@@ -85,155 +83,6 @@ message OpenRouterCompatibleModelInfo {
|
||||
// Request for fetching OpenAI models
|
||||
message OpenAiModelsRequest {
|
||||
Metadata metadata = 1;
|
||||
string base_url = 2;
|
||||
string api_key = 3;
|
||||
}
|
||||
|
||||
// Request for updating API configuration
|
||||
message UpdateApiConfigurationRequest {
|
||||
Metadata metadata = 1;
|
||||
ModelsApiConfiguration api_configuration = 2;
|
||||
}
|
||||
|
||||
// API Provider enumeration
|
||||
enum ApiProvider {
|
||||
ANTHROPIC = 0;
|
||||
OPENROUTER = 1;
|
||||
BEDROCK = 2;
|
||||
VERTEX = 3;
|
||||
OPENAI = 4;
|
||||
OLLAMA = 5;
|
||||
LMSTUDIO = 6;
|
||||
GEMINI = 7;
|
||||
OPENAI_NATIVE = 8;
|
||||
REQUESTY = 9;
|
||||
TOGETHER = 10;
|
||||
DEEPSEEK = 11;
|
||||
QWEN = 12;
|
||||
DOUBAO = 13;
|
||||
MISTRAL = 14;
|
||||
VSCODE_LM = 15;
|
||||
CLINE = 16;
|
||||
LITELLM = 17;
|
||||
NEBIUS = 18;
|
||||
FIREWORKS = 19;
|
||||
ASKSAGE = 20;
|
||||
XAI = 21;
|
||||
SAMBANOVA = 22;
|
||||
CEREBRAS = 23;
|
||||
SAPAICORE = 24;
|
||||
CLAUDE_CODE = 25;
|
||||
}
|
||||
|
||||
// Model info for OpenAI-compatible models
|
||||
message OpenAiCompatibleModelInfo {
|
||||
optional int32 max_tokens = 1;
|
||||
optional int32 context_window = 2;
|
||||
optional bool supports_images = 3;
|
||||
bool supports_prompt_cache = 4;
|
||||
optional double input_price = 5;
|
||||
optional double output_price = 6;
|
||||
optional ThinkingConfig thinking_config = 7;
|
||||
optional bool supports_global_endpoint = 8;
|
||||
optional double cache_writes_price = 9;
|
||||
optional double cache_reads_price = 10;
|
||||
optional string description = 11;
|
||||
repeated ModelTier tiers = 12;
|
||||
optional double temperature = 13;
|
||||
optional bool is_r1_format_required = 14;
|
||||
}
|
||||
|
||||
// Model info for LiteLLM models
|
||||
message LiteLLMModelInfo {
|
||||
optional int32 max_tokens = 1;
|
||||
optional int32 context_window = 2;
|
||||
optional bool supports_images = 3;
|
||||
bool supports_prompt_cache = 4;
|
||||
optional double input_price = 5;
|
||||
optional double output_price = 6;
|
||||
optional ThinkingConfig thinking_config = 7;
|
||||
optional bool supports_global_endpoint = 8;
|
||||
optional double cache_writes_price = 9;
|
||||
optional double cache_reads_price = 10;
|
||||
optional string description = 11;
|
||||
repeated ModelTier tiers = 12;
|
||||
optional double temperature = 13;
|
||||
}
|
||||
|
||||
// Main ApiConfiguration message
|
||||
message ModelsApiConfiguration {
|
||||
// From ApiHandlerOptions (excluding onRetryAttempt function)
|
||||
optional string api_model_id = 1;
|
||||
optional string api_key = 2;
|
||||
optional string cline_api_key = 3;
|
||||
optional string task_id = 4;
|
||||
optional string lite_llm_base_url = 5;
|
||||
optional string lite_llm_model_id = 6;
|
||||
optional string lite_llm_api_key = 7;
|
||||
optional bool lite_llm_use_prompt_cache = 8;
|
||||
map<string, string> open_ai_headers = 9;
|
||||
optional LiteLLMModelInfo lite_llm_model_info = 10;
|
||||
optional string anthropic_base_url = 11;
|
||||
optional string open_router_api_key = 12;
|
||||
optional string open_router_model_id = 13;
|
||||
optional OpenRouterModelInfo open_router_model_info = 14;
|
||||
optional string open_router_provider_sorting = 15;
|
||||
optional string aws_access_key = 16;
|
||||
optional string aws_secret_key = 17;
|
||||
optional string aws_session_token = 18;
|
||||
optional string aws_region = 19;
|
||||
optional bool aws_use_cross_region_inference = 20;
|
||||
optional bool aws_bedrock_use_prompt_cache = 21;
|
||||
optional bool aws_use_profile = 22;
|
||||
optional string aws_profile = 23;
|
||||
optional string aws_bedrock_endpoint = 24;
|
||||
optional bool aws_bedrock_custom_selected = 25;
|
||||
optional string aws_bedrock_custom_model_base_id = 26;
|
||||
optional string vertex_project_id = 27;
|
||||
optional string vertex_region = 28;
|
||||
optional string open_ai_base_url = 29;
|
||||
optional string open_ai_api_key = 30;
|
||||
optional string open_ai_model_id = 31;
|
||||
optional OpenAiCompatibleModelInfo open_ai_model_info = 32;
|
||||
optional string ollama_model_id = 33;
|
||||
optional string ollama_base_url = 34;
|
||||
optional string ollama_api_options_ctx_num = 35;
|
||||
optional string lm_studio_model_id = 36;
|
||||
optional string lm_studio_base_url = 37;
|
||||
optional string gemini_api_key = 38;
|
||||
optional string gemini_base_url = 39;
|
||||
optional string open_ai_native_api_key = 40;
|
||||
optional string deep_seek_api_key = 41;
|
||||
optional string requesty_api_key = 42;
|
||||
optional string requesty_model_id = 43;
|
||||
optional OpenRouterModelInfo requesty_model_info = 44;
|
||||
optional string together_api_key = 45;
|
||||
optional string together_model_id = 46;
|
||||
optional string fireworks_api_key = 47;
|
||||
optional string fireworks_model_id = 48;
|
||||
optional int32 fireworks_model_max_completion_tokens = 49;
|
||||
optional int32 fireworks_model_max_tokens = 50;
|
||||
optional string qwen_api_key = 51;
|
||||
optional string doubao_api_key = 52;
|
||||
optional string mistral_api_key = 53;
|
||||
optional string azure_api_version = 54;
|
||||
optional LanguageModelChatSelector vs_code_lm_model_selector = 55;
|
||||
optional string qwen_api_line = 56;
|
||||
optional string nebius_api_key = 57;
|
||||
optional string asksage_api_url = 58;
|
||||
optional string asksage_api_key = 59;
|
||||
optional string xai_api_key = 60;
|
||||
optional int32 thinking_budget_tokens = 61;
|
||||
optional string reasoning_effort = 62;
|
||||
optional string sambanova_api_key = 63;
|
||||
optional string cerebras_api_key = 64;
|
||||
optional int32 request_timeout_ms = 65;
|
||||
optional ApiProvider api_provider = 66;
|
||||
repeated string favorited_model_ids = 67;
|
||||
optional string sap_ai_core_client_id = 68;
|
||||
optional string sap_ai_core_client_secret = 69;
|
||||
optional string sap_ai_resource_group = 70;
|
||||
optional string sap_ai_core_token_url = 71;
|
||||
optional string sap_ai_core_base_url = 72;
|
||||
optional string claude_code_path = 73;
|
||||
string baseUrl = 2;
|
||||
string apiKey = 3;
|
||||
}
|
||||
|
||||
+14
-47
@@ -7,14 +7,11 @@ import "common.proto";
|
||||
|
||||
service StateService {
|
||||
rpc getLatestState(EmptyRequest) returns (State);
|
||||
rpc updateTerminalConnectionTimeout(Int64Request) returns (Int64);
|
||||
rpc updateTerminalReuseEnabled(BooleanRequest) returns (Empty);
|
||||
rpc updateDefaultTerminalProfile(StringRequest) returns (TerminalProfileUpdateResponse);
|
||||
rpc getAvailableTerminalProfiles(EmptyRequest) returns (TerminalProfiles);
|
||||
rpc subscribeToState(EmptyRequest) returns (stream State);
|
||||
rpc toggleFavoriteModel(StringRequest) returns (Empty);
|
||||
rpc resetState(ResetStateRequest) returns (Empty);
|
||||
rpc resetState(EmptyRequest) returns (Empty);
|
||||
rpc togglePlanActMode(TogglePlanActModeRequest) returns (Empty);
|
||||
rpc updateTerminalConnectionTimeout(Int64Request) returns (Int64);
|
||||
rpc updateAutoApprovalSettings(AutoApprovalSettingsRequest) returns (Empty);
|
||||
rpc updateSettings(UpdateSettingsRequest) returns (Empty);
|
||||
}
|
||||
@@ -23,23 +20,6 @@ message State {
|
||||
string state_json = 1;
|
||||
}
|
||||
|
||||
message TerminalProfiles {
|
||||
repeated TerminalProfile profiles = 1;
|
||||
}
|
||||
|
||||
message TerminalProfile {
|
||||
string id = 1;
|
||||
string name = 2;
|
||||
optional string path = 3;
|
||||
optional string description = 4;
|
||||
}
|
||||
|
||||
message TerminalProfileUpdateResponse {
|
||||
int32 closed_count = 1;
|
||||
int32 busy_terminals_count = 2;
|
||||
bool has_busy_terminals = 3;
|
||||
}
|
||||
|
||||
message TogglePlanActModeRequest {
|
||||
Metadata metadata = 1;
|
||||
ChatSettings chat_settings = 2;
|
||||
@@ -63,13 +43,10 @@ message ChatContent {
|
||||
repeated string files = 3;
|
||||
}
|
||||
|
||||
message ResetStateRequest {
|
||||
Metadata metadata = 1;
|
||||
optional bool global = 2;
|
||||
}
|
||||
|
||||
// Message for auto approval settings
|
||||
message AutoApprovalSettingsRequest {
|
||||
Metadata metadata = 1;
|
||||
|
||||
message Actions {
|
||||
bool read_files = 1;
|
||||
bool read_files_externally = 2;
|
||||
@@ -80,6 +57,7 @@ message AutoApprovalSettingsRequest {
|
||||
bool use_browser = 7;
|
||||
bool use_mcp = 8;
|
||||
}
|
||||
|
||||
int32 version = 2;
|
||||
bool enabled = 3;
|
||||
Actions actions = 4;
|
||||
@@ -92,16 +70,15 @@ message AutoApprovalSettingsRequest {
|
||||
message UpdateSettingsRequest {
|
||||
Metadata metadata = 1;
|
||||
optional ApiConfiguration api_configuration = 2;
|
||||
optional string telemetry_setting = 3;
|
||||
optional bool plan_act_separate_models_setting = 4;
|
||||
optional bool enable_checkpoints_setting = 5;
|
||||
optional bool mcp_marketplace_enabled = 6;
|
||||
optional ChatSettings chat_settings = 7;
|
||||
optional int64 shell_integration_timeout = 8;
|
||||
optional bool terminal_reuse_enabled = 9;
|
||||
optional bool mcp_responses_collapsed = 10;
|
||||
optional bool mcp_rich_display_enabled = 11;
|
||||
optional int64 terminal_output_line_limit = 12;
|
||||
optional string custom_instructions_setting = 3;
|
||||
optional string telemetry_setting = 4;
|
||||
optional bool plan_act_separate_models_setting = 5;
|
||||
optional bool enable_checkpoints_setting = 6;
|
||||
optional bool mcp_marketplace_enabled = 7;
|
||||
optional ChatSettings chat_settings = 8;
|
||||
optional int64 shell_integration_timeout = 9;
|
||||
optional bool terminal_reuse_enabled = 10;
|
||||
optional bool mcp_responses_collapsed = 11;
|
||||
}
|
||||
|
||||
// Complete API Configuration message
|
||||
@@ -210,14 +187,4 @@ message ApiConfiguration {
|
||||
|
||||
// Favorited model IDs
|
||||
repeated string favorited_model_ids = 71;
|
||||
|
||||
// SAP AI Core specific
|
||||
optional string sap_ai_core_client_id = 72;
|
||||
optional string sap_ai_core_client_secret = 73;
|
||||
optional string sap_ai_core_base_url = 74;
|
||||
optional string sap_ai_core_token_url = 75;
|
||||
optional string sap_ai_resource_group = 76;
|
||||
|
||||
// Claude Code specific
|
||||
optional string claude_code_path = 77;
|
||||
}
|
||||
|
||||
+8
-9
@@ -15,7 +15,7 @@ enum WebviewProviderType {
|
||||
// Define a new message type for webview provider info
|
||||
message WebviewProviderTypeRequest {
|
||||
Metadata metadata = 1;
|
||||
WebviewProviderType provider_type = 2;
|
||||
WebviewProviderType providerType = 2;
|
||||
}
|
||||
|
||||
// Enum for ClineMessage type
|
||||
@@ -65,14 +65,13 @@ enum ClineSay {
|
||||
BROWSER_ACTION_RESULT = 16;
|
||||
MCP_SERVER_REQUEST_STARTED = 17;
|
||||
MCP_SERVER_RESPONSE = 18;
|
||||
MCP_NOTIFICATION = 19;
|
||||
USE_MCP_SERVER_SAY = 20;
|
||||
DIFF_ERROR = 21;
|
||||
DELETED_API_REQS = 22;
|
||||
CLINEIGNORE_ERROR = 23;
|
||||
CHECKPOINT_CREATED = 24;
|
||||
LOAD_MCP_DOCUMENTATION = 25;
|
||||
INFO = 26;
|
||||
USE_MCP_SERVER_SAY = 19;
|
||||
DIFF_ERROR = 20;
|
||||
DELETED_API_REQS = 21;
|
||||
CLINEIGNORE_ERROR = 22;
|
||||
CHECKPOINT_CREATED = 23;
|
||||
LOAD_MCP_DOCUMENTATION = 24;
|
||||
INFO = 25;
|
||||
}
|
||||
|
||||
// Enum for ClineSayTool tool types
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import * as fs from "fs"
|
||||
import * as grpc from "@grpc/grpc-js"
|
||||
import * as protoLoader from "@grpc/proto-loader"
|
||||
import * as fs from "fs"
|
||||
import * as health from "grpc-health-check"
|
||||
import path, { basename, dirname } from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
import path from "path"
|
||||
|
||||
const OUT_FILE = path.resolve("src/generated/standalone/server-setup.ts")
|
||||
const OUT_FILE = path.resolve("src/standalone/server-setup.ts")
|
||||
const DESCRIPTOR_SET = path.resolve("dist-standalone/proto/descriptor_set.pb")
|
||||
|
||||
// Load service definitions.
|
||||
@@ -29,17 +29,17 @@ function generateHandlersAndExports() {
|
||||
const dir = domain.charAt(0).toLowerCase() + domain.slice(1)
|
||||
imports.push(`// ${domain} Service`)
|
||||
handlerSetup.push(` // ${domain} Service`)
|
||||
handlerSetup.push(` server.addService(cline.${name}Service, {`)
|
||||
handlerSetup.push(` server.addService(proto.cline.${name}.service, {`)
|
||||
for (const [rpcName, rpc] of Object.entries(def.service)) {
|
||||
imports.push(`import { ${rpcName} } from "@core/controller/${dir}/${rpcName}"`)
|
||||
const requestType = "cline." + rpc.requestType.type.name
|
||||
imports.push(`import { ${rpcName} } from "../core/controller/${dir}/${rpcName}"`)
|
||||
const requestType = "proto.cline." + rpc.requestType.type.name
|
||||
if (rpc.requestStream) {
|
||||
throw new Error("Request streaming is not supported")
|
||||
}
|
||||
if (rpc.responseStream) {
|
||||
handlerSetup.push(` ${rpcName}: wrapStreamingResponse<${requestType},void>(${rpcName}, controller),`)
|
||||
} else {
|
||||
const responseType = "cline." + rpc.responseType.type.name
|
||||
const responseType = "proto.cline." + rpc.responseType.type.name
|
||||
handlerSetup.push(` ${rpcName}: wrapper<${requestType},${responseType}>(${rpcName}, controller),`)
|
||||
}
|
||||
}
|
||||
@@ -60,13 +60,14 @@ const scriptName = path.basename(fileURLToPath(import.meta.url))
|
||||
let output = `// GENERATED CODE -- DO NOT EDIT!
|
||||
// Generated by ${scriptName}
|
||||
import * as grpc from "@grpc/grpc-js"
|
||||
import { cline } from "@generated/grpc-js"
|
||||
import { Controller } from "@core/controller"
|
||||
import { GrpcHandlerWrapper, GrpcStreamingResponseHandlerWrapper } from "@/standalone/grpc-types"
|
||||
import * as proto from "@/shared/proto"
|
||||
import { Controller } from "../core/controller"
|
||||
import { GrpcHandlerWrapper, GrpcStreamingResponseHandlerWrapper } from "./grpc-types"
|
||||
|
||||
${imports}
|
||||
export function addProtobusServices(
|
||||
export function addServices(
|
||||
server: grpc.Server,
|
||||
proto: any,
|
||||
controller: Controller,
|
||||
wrapper: GrpcHandlerWrapper,
|
||||
wrapStreamingResponse: GrpcStreamingResponseHandlerWrapper,
|
||||
@@ -75,7 +76,6 @@ ${handlerSetup}
|
||||
}
|
||||
`
|
||||
// Write output file
|
||||
fs.mkdirSync(dirname(OUT_FILE), { recursive: true })
|
||||
fs.writeFileSync(OUT_FILE, output)
|
||||
|
||||
console.log(`Generated service handlers in ${OUT_FILE}.`)
|
||||
|
||||
@@ -25,8 +25,6 @@ import { AskSageHandler } from "./providers/asksage"
|
||||
import { XAIHandler } from "./providers/xai"
|
||||
import { SambanovaHandler } from "./providers/sambanova"
|
||||
import { CerebrasHandler } from "./providers/cerebras"
|
||||
import { SapAiCoreHandler } from "./providers/sapaicore"
|
||||
import { ClaudeCodeHandler } from "./providers/claude-code"
|
||||
|
||||
export interface ApiHandler {
|
||||
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream
|
||||
@@ -89,10 +87,6 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
|
||||
return new SambanovaHandler(options)
|
||||
case "cerebras":
|
||||
return new CerebrasHandler(options)
|
||||
case "sapaicore":
|
||||
return new SapAiCoreHandler(options)
|
||||
case "claude-code":
|
||||
return new ClaudeCodeHandler(options)
|
||||
default:
|
||||
return new AnthropicHandler(options)
|
||||
}
|
||||
|
||||
@@ -35,59 +35,6 @@ interface ExtendedMetadata {
|
||||
}
|
||||
}
|
||||
|
||||
// Define types for stream response content blocks
|
||||
interface ContentBlockStart {
|
||||
contentBlockIndex?: number
|
||||
start?: {
|
||||
type?: string
|
||||
thinking?: string
|
||||
}
|
||||
contentBlock?: {
|
||||
type?: string
|
||||
thinking?: string
|
||||
}
|
||||
type?: string
|
||||
thinking?: string
|
||||
}
|
||||
|
||||
// Define types for stream response deltas
|
||||
interface ContentBlockDelta {
|
||||
contentBlockIndex?: number
|
||||
delta?: {
|
||||
type?: string
|
||||
thinking?: string
|
||||
text?: string
|
||||
reasoningContent?: {
|
||||
text?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Define types for supported content types
|
||||
type SupportedContentType = "text" | "image" | "thinking"
|
||||
|
||||
interface ContentItem {
|
||||
type: SupportedContentType
|
||||
text?: string
|
||||
source?: {
|
||||
data: string | Buffer | Uint8Array
|
||||
media_type?: string
|
||||
}
|
||||
}
|
||||
|
||||
// Define cache point type for AWS Bedrock
|
||||
interface CachePointContentBlock {
|
||||
cachePoint: {
|
||||
type: "default"
|
||||
}
|
||||
}
|
||||
|
||||
// Define provider options type based on AWS SDK patterns
|
||||
interface ProviderChainOptions {
|
||||
ignoreCache?: boolean
|
||||
profile?: string
|
||||
}
|
||||
|
||||
// https://docs.anthropic.com/en/api/claude-on-amazon-bedrock
|
||||
export class AwsBedrockHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
@@ -96,7 +43,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
this.options = options
|
||||
}
|
||||
|
||||
@withRetry({ maxRetries: 4 })
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
// cross region inference requires prefixing the model id with the region
|
||||
const modelId = await this.getModelId()
|
||||
@@ -160,7 +107,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
sessionToken?: string
|
||||
}> {
|
||||
// Configure provider options
|
||||
const providerOptions: ProviderChainOptions = {}
|
||||
const providerOptions: any = {}
|
||||
if (this.options.awsUseProfile) {
|
||||
// For profile-based auth, always use ignoreCache to detect credential file changes
|
||||
// This solves the AWS Identity Manager issue where credential files change externally
|
||||
@@ -514,7 +461,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
|
||||
// Handle content block start - check if Bedrock uses Anthropic SDK format
|
||||
if (chunk.contentBlockStart) {
|
||||
const blockStart = chunk.contentBlockStart as ContentBlockStart
|
||||
const blockStart = chunk.contentBlockStart as any
|
||||
const blockIndex = chunk.contentBlockStart.contentBlockIndex
|
||||
|
||||
// Check for thinking block in various possible formats
|
||||
@@ -550,7 +497,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
|
||||
// Check if this is a thinking block
|
||||
const blockType = blockTypes.get(blockIndex)
|
||||
const delta = chunk.contentBlockDelta.delta as ContentBlockDelta["delta"]
|
||||
const delta = chunk.contentBlockDelta.delta as any
|
||||
|
||||
// Handle thinking delta (Anthropic SDK format)
|
||||
if (delta?.type === "thinking_delta" || delta?.thinking) {
|
||||
@@ -780,7 +727,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
// Log unsupported content types for debugging
|
||||
console.warn(`Unsupported content type: ${(item as ContentItem).type}`)
|
||||
console.warn(`Unsupported content type: ${(item as any).type}`)
|
||||
return null
|
||||
})
|
||||
.filter((item): item is ContentBlock => item !== null)
|
||||
@@ -824,7 +771,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
imageData = new Uint8Array(Buffer.from(base64Data, "base64"))
|
||||
} else if (item.source.data && typeof item.source.data === "object") {
|
||||
// Try to convert to Uint8Array
|
||||
imageData = new Uint8Array(Buffer.from(item.source.data as Buffer | Uint8Array))
|
||||
imageData = new Uint8Array(Buffer.from(item.source.data as any))
|
||||
} else {
|
||||
throw new Error("Unsupported image data format")
|
||||
}
|
||||
@@ -870,7 +817,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
cachePoint: {
|
||||
type: "default",
|
||||
},
|
||||
} as CachePointContentBlock, // Properly typed cache point for AWS SDK
|
||||
} as any, // Type assertion needed for AWS SDK compatibility
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
import type { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { claudeCodeDefaultModelId, ClaudeCodeModelId, claudeCodeModels, type ApiHandlerOptions } from "@/shared/api"
|
||||
import { type ApiHandler } from ".."
|
||||
import { ApiStreamUsageChunk, type ApiStream } from "../transform/stream"
|
||||
import { withRetry } from "../retry"
|
||||
import { runClaudeCode } from "@/integrations/claude-code/run"
|
||||
import { ClaudeCodeMessage } from "@/integrations/claude-code/types"
|
||||
|
||||
export class ClaudeCodeHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
}
|
||||
|
||||
@withRetry({
|
||||
maxRetries: 4,
|
||||
baseDelay: 2000,
|
||||
maxDelay: 15000,
|
||||
})
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const claudeProcess = runClaudeCode({
|
||||
systemPrompt,
|
||||
messages,
|
||||
path: this.options.claudeCodePath,
|
||||
modelId: this.getModel().id,
|
||||
})
|
||||
|
||||
const dataQueue: string[] = []
|
||||
let processError = null
|
||||
let errorOutput = ""
|
||||
let exitCode: number | null = null
|
||||
|
||||
claudeProcess.stdout.on("data", (data) => {
|
||||
const output = data.toString()
|
||||
const lines = output.split("\n").filter((line: string) => line.trim() !== "")
|
||||
|
||||
for (const line of lines) {
|
||||
dataQueue.push(line)
|
||||
}
|
||||
})
|
||||
|
||||
claudeProcess.stderr.on("data", (data) => {
|
||||
errorOutput += data.toString()
|
||||
})
|
||||
|
||||
claudeProcess.on("close", (code) => {
|
||||
exitCode = code
|
||||
})
|
||||
|
||||
claudeProcess.on("error", (error) => {
|
||||
processError = error
|
||||
})
|
||||
|
||||
// Usage is included with assistant messages,
|
||||
// but cost is included in the result chunk
|
||||
let usage: ApiStreamUsageChunk = {
|
||||
type: "usage",
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
cacheWriteTokens: 0,
|
||||
}
|
||||
|
||||
while (exitCode !== 0 || dataQueue.length > 0) {
|
||||
if (dataQueue.length === 0) {
|
||||
await new Promise((resolve) => setImmediate(resolve))
|
||||
}
|
||||
|
||||
if (exitCode !== null && exitCode !== 0) {
|
||||
throw new Error(
|
||||
`Claude Code process exited with code ${exitCode}.${errorOutput ? ` Error output: ${errorOutput.trim()}` : ""}`,
|
||||
)
|
||||
}
|
||||
|
||||
const data = dataQueue.shift()
|
||||
if (!data) {
|
||||
continue
|
||||
}
|
||||
|
||||
const chunk = this.attemptParseChunk(data)
|
||||
|
||||
if (!chunk) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: data || "",
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (chunk.type === "system" && chunk.subtype === "init") {
|
||||
continue
|
||||
}
|
||||
|
||||
if (chunk.type === "assistant" && "message" in chunk) {
|
||||
const message = chunk.message
|
||||
|
||||
if (message.stop_reason !== null && message.stop_reason !== "tool_use") {
|
||||
const errorMessage = message.content[0]?.text || `Claude Code stopped with reason: ${message.stop_reason}`
|
||||
|
||||
if (errorMessage.includes("Invalid model name")) {
|
||||
throw new Error(
|
||||
errorMessage +
|
||||
`\n\nAPI keys and subscription plans allow different models. Make sure the selected model is included in your plan.`,
|
||||
)
|
||||
}
|
||||
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
for (const content of message.content) {
|
||||
if (content.type === "text") {
|
||||
yield {
|
||||
type: "text",
|
||||
text: content.text,
|
||||
}
|
||||
} else {
|
||||
console.warn("Unsupported content type:", content.type)
|
||||
}
|
||||
}
|
||||
|
||||
usage.inputTokens += message.usage.input_tokens
|
||||
usage.outputTokens += message.usage.output_tokens
|
||||
usage.cacheReadTokens = (usage.cacheReadTokens || 0) + (message.usage.cache_read_input_tokens || 0)
|
||||
usage.cacheWriteTokens = (usage.cacheWriteTokens || 0) + (message.usage.cache_creation_input_tokens || 0)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (chunk.type === "result" && "result" in chunk) {
|
||||
usage.totalCost = chunk.cost_usd || 0
|
||||
|
||||
yield usage
|
||||
}
|
||||
|
||||
if (processError) {
|
||||
throw processError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getModel() {
|
||||
const modelId = this.options.apiModelId
|
||||
if (modelId && modelId in claudeCodeModels) {
|
||||
const id = modelId as ClaudeCodeModelId
|
||||
return { id, info: claudeCodeModels[id] }
|
||||
}
|
||||
|
||||
return {
|
||||
id: claudeCodeDefaultModelId,
|
||||
info: claudeCodeModels[claudeCodeDefaultModelId],
|
||||
}
|
||||
}
|
||||
|
||||
// TOOD: Validate instead of parsing
|
||||
private attemptParseChunk(data: string): ClaudeCodeMessage | null {
|
||||
try {
|
||||
return JSON.parse(data)
|
||||
} catch (error) {
|
||||
console.error("Error parsing chunk:", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import type { Anthropic } from "@anthropic-ai/sdk"
|
||||
// Restore GenerateContentConfig import and add GenerateContentResponseUsageMetadata
|
||||
import { GoogleGenAI, type GenerateContentConfig, type GenerateContentResponseUsageMetadata } from "@google/genai"
|
||||
import { withRetry } from "../retry"
|
||||
import { Part } from "@google/genai"
|
||||
import { ApiHandler } from "../"
|
||||
import { ApiHandlerOptions, geminiDefaultModelId, GeminiModelId, geminiModels, ModelInfo } from "@shared/api"
|
||||
import { convertAnthropicMessageToGemini } from "../transform/gemini-format"
|
||||
@@ -97,10 +96,9 @@ export class GeminiHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
// Add thinking config if the model supports it
|
||||
if (thinkingBudget > 0) {
|
||||
if (info.thinkingConfig?.outputPrice !== undefined && maxBudget > 0) {
|
||||
requestConfig.thinkingConfig = {
|
||||
thinkingBudget: thinkingBudget,
|
||||
includeThoughts: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +111,6 @@ export class GeminiHandler implements ApiHandler {
|
||||
let promptTokens = 0
|
||||
let outputTokens = 0
|
||||
let cacheReadTokens = 0
|
||||
let thoughtsTokenCount = 0 // Initialize thought token counts
|
||||
let lastUsageMetadata: GenerateContentResponseUsageMetadata | undefined
|
||||
|
||||
try {
|
||||
@@ -133,31 +130,6 @@ export class GeminiHandler implements ApiHandler {
|
||||
isFirstSdkChunk = false
|
||||
}
|
||||
|
||||
// Handle thinking content from Gemini's response
|
||||
const candidateForThoughts = chunk?.candidates?.[0]
|
||||
const partsForThoughts = candidateForThoughts?.content?.parts
|
||||
let thoughts = "" // Initialize as empty string
|
||||
|
||||
if (partsForThoughts) {
|
||||
// This ensures partsForThoughts is a Part[] array
|
||||
for (const part of partsForThoughts) {
|
||||
const { thought, text } = part as Part
|
||||
if (thought && text) {
|
||||
// Ensure part.text exists
|
||||
// Handle the thought part
|
||||
thoughts += text + "\n" // Append thought and a newline
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (thoughts.trim() !== "") {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: thoughts.trim(),
|
||||
}
|
||||
thoughts = "" // Reset thoughts after yielding
|
||||
}
|
||||
|
||||
if (chunk.text) {
|
||||
yield {
|
||||
type: "text",
|
||||
@@ -169,7 +141,6 @@ export class GeminiHandler implements ApiHandler {
|
||||
lastUsageMetadata = chunk.usageMetadata
|
||||
promptTokens = lastUsageMetadata.promptTokenCount ?? promptTokens
|
||||
outputTokens = lastUsageMetadata.candidatesTokenCount ?? outputTokens
|
||||
thoughtsTokenCount = lastUsageMetadata.thoughtsTokenCount ?? thoughtsTokenCount
|
||||
cacheReadTokens = lastUsageMetadata.cachedContentTokenCount ?? cacheReadTokens
|
||||
}
|
||||
}
|
||||
@@ -180,14 +151,12 @@ export class GeminiHandler implements ApiHandler {
|
||||
info,
|
||||
inputTokens: promptTokens,
|
||||
outputTokens,
|
||||
thoughtsTokenCount,
|
||||
cacheReadTokens,
|
||||
})
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: promptTokens,
|
||||
outputTokens,
|
||||
thoughtsTokenCount,
|
||||
cacheReadTokens,
|
||||
cacheWriteTokens: 0,
|
||||
totalCost,
|
||||
@@ -270,13 +239,11 @@ export class GeminiHandler implements ApiHandler {
|
||||
info,
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
thoughtsTokenCount = 0,
|
||||
cacheReadTokens = 0,
|
||||
}: {
|
||||
info: ModelInfo
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
thoughtsTokenCount: number
|
||||
cacheReadTokens?: number
|
||||
}) {
|
||||
// Exit early if any required pricing information is missing
|
||||
@@ -308,18 +275,18 @@ export class GeminiHandler implements ApiHandler {
|
||||
const inputTokensCost = inputPrice * (uncachedInputTokens / 1_000_000)
|
||||
|
||||
// 2. Output token costs
|
||||
const responseTokensCost = outputPrice * ((outputTokens + thoughtsTokenCount) / 1_000_000)
|
||||
const outputTokensCost = outputPrice * (outputTokens / 1_000_000)
|
||||
|
||||
// 3. Cache read costs (immediate)
|
||||
const cacheReadCost = (cacheReadTokens ?? 0) > 0 ? cacheReadsPrice * ((cacheReadTokens ?? 0) / 1_000_000) : 0
|
||||
|
||||
// Calculate total immediate cost (excluding cache write/storage costs)
|
||||
const totalCost = inputTokensCost + responseTokensCost + cacheReadCost
|
||||
const totalCost = inputTokensCost + outputTokensCost + cacheReadCost
|
||||
|
||||
// Create the trace object for debugging
|
||||
const trace: Record<string, { price: number; tokens: number; cost: number }> = {
|
||||
input: { price: inputPrice, tokens: uncachedInputTokens, cost: inputTokensCost },
|
||||
output: { price: outputPrice, tokens: outputTokens, cost: responseTokensCost },
|
||||
output: { price: outputPrice, tokens: outputTokens, cost: outputTokensCost },
|
||||
}
|
||||
|
||||
// Only include cache read costs in the trace (cache write costs are tracked separately)
|
||||
|
||||
@@ -1,540 +0,0 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import axios from "axios"
|
||||
import OpenAI from "openai"
|
||||
import { ApiHandler } from "../"
|
||||
import { ApiHandlerOptions, ModelInfo, sapAiCoreDefaultModelId, SapAiCoreModelId, sapAiCoreModels } from "../../shared/api"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
interface Deployment {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
interface Token {
|
||||
access_token: string
|
||||
expires_in: number
|
||||
scope: string
|
||||
jti: string
|
||||
token_type: string
|
||||
expires_at: number
|
||||
}
|
||||
export class SapAiCoreHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private token?: Token
|
||||
private deployments?: Deployment[]
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
}
|
||||
|
||||
private async authenticate(): Promise<Token> {
|
||||
const payload = {
|
||||
grant_type: "client_credentials",
|
||||
client_id: this.options.sapAiCoreClientId || "",
|
||||
client_secret: this.options.sapAiCoreClientSecret || "",
|
||||
}
|
||||
|
||||
const tokenUrl = (this.options.sapAiCoreTokenUrl || "").replace(/\/+$/, "") + "/oauth/token"
|
||||
const response = await axios.post(tokenUrl, payload, {
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
})
|
||||
const token = response.data as Token
|
||||
token.expires_at = Date.now() + token.expires_in * 1000
|
||||
return token
|
||||
}
|
||||
|
||||
private async getToken(): Promise<string> {
|
||||
if (!this.token || this.token.expires_at < Date.now()) {
|
||||
this.token = await this.authenticate()
|
||||
}
|
||||
return this.token.access_token
|
||||
}
|
||||
|
||||
private async getAiCoreDeployments(): Promise<Deployment[]> {
|
||||
if (this.options.sapAiCoreClientSecret === "") {
|
||||
return [{ id: "notconfigured", name: "ai-core-not-configured" }]
|
||||
}
|
||||
|
||||
const token = await this.getToken()
|
||||
const headers = {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"AI-Resource-Group": this.options.sapAiResourceGroup || "default",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
const url = `${this.options.sapAiCoreBaseUrl}/v2/lm/deployments?$top=10000&$skip=0`
|
||||
|
||||
try {
|
||||
const response = await axios.get(url, { headers })
|
||||
const deployments = response.data.resources
|
||||
|
||||
return deployments
|
||||
.filter((deployment: any) => deployment.targetStatus === "RUNNING")
|
||||
.map((deployment: any) => {
|
||||
const model = deployment.details?.resources?.backend_details?.model
|
||||
if (!model?.name || !model?.version) {
|
||||
return null // Skip this row
|
||||
}
|
||||
return {
|
||||
id: deployment.id,
|
||||
name: `${model.name}:${model.version}`,
|
||||
}
|
||||
})
|
||||
.filter((deployment: any) => deployment !== null)
|
||||
} catch (error) {
|
||||
console.error("Error fetching deployments:", error)
|
||||
throw new Error("Failed to fetch deployments")
|
||||
}
|
||||
}
|
||||
|
||||
private async getDeploymentForModel(modelId: string): Promise<string> {
|
||||
// If deployments are not fetched yet or the model is not found in the fetched deployments, fetch deployments
|
||||
if (!this.deployments || !this.hasDeploymentForModel(modelId)) {
|
||||
this.deployments = await this.getAiCoreDeployments()
|
||||
}
|
||||
|
||||
const deployment = this.deployments.find((d) => {
|
||||
const deploymentBaseName = d.name.split(":")[0].toLowerCase()
|
||||
const modelBaseName = modelId.split(":")[0].toLowerCase()
|
||||
return deploymentBaseName === modelBaseName
|
||||
})
|
||||
|
||||
if (!deployment) {
|
||||
throw new Error(`No running deployment found for model ${modelId}`)
|
||||
}
|
||||
|
||||
return deployment.id
|
||||
}
|
||||
|
||||
private hasDeploymentForModel(modelId: string): boolean {
|
||||
return this.deployments?.some((d) => d.name.split(":")[0].toLowerCase() === modelId.split(":")[0].toLowerCase()) ?? false
|
||||
}
|
||||
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const token = await this.getToken()
|
||||
const headers = {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"AI-Resource-Group": this.options.sapAiResourceGroup || "default",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
const model = this.getModel()
|
||||
const deploymentId = await this.getDeploymentForModel(model.id)
|
||||
|
||||
const anthropicModels = [
|
||||
"anthropic--claude-3.7-sonnet",
|
||||
"anthropic--claude-3.5-sonnet",
|
||||
"anthropic--claude-3-sonnet",
|
||||
"anthropic--claude-3-haiku",
|
||||
"anthropic--claude-3-opus",
|
||||
]
|
||||
|
||||
const openAIModels = ["gpt-4o", "gpt-4", "gpt-4o-mini", "o1", "gpt-4.1", "gpt-4.1-nano", "o3-mini", "o3", "o4-mini"]
|
||||
|
||||
let url: string
|
||||
let payload: any
|
||||
if (anthropicModels.includes(model.id)) {
|
||||
url = `${this.options.sapAiCoreBaseUrl}/v2/inference/deployments/${deploymentId}/invoke-with-response-stream`
|
||||
|
||||
if (model.id === "anthropic--claude-3.7-sonnet") {
|
||||
url = `${this.options.sapAiCoreBaseUrl}/v2/inference/deployments/${deploymentId}/converse-stream`
|
||||
payload = {
|
||||
inferenceConfig: {
|
||||
maxTokens: model.info.maxTokens,
|
||||
temperature: 0.0,
|
||||
},
|
||||
system: systemPrompt ? [{ text: systemPrompt }] : undefined,
|
||||
messages: this.formatAnthropicMessages(messages),
|
||||
}
|
||||
} else {
|
||||
payload = {
|
||||
max_tokens: model.info.maxTokens,
|
||||
system: systemPrompt,
|
||||
messages,
|
||||
anthropic_version: "bedrock-2023-05-31",
|
||||
}
|
||||
}
|
||||
} else if (openAIModels.includes(model.id)) {
|
||||
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
url = `${this.options.sapAiCoreBaseUrl}/v2/inference/deployments/${deploymentId}/chat/completions?api-version=2024-12-01-preview`
|
||||
payload = {
|
||||
stream: true,
|
||||
messages: openAiMessages,
|
||||
max_tokens: model.info.maxTokens,
|
||||
temperature: 0.0,
|
||||
frequency_penalty: 0,
|
||||
presence_penalty: 0,
|
||||
stop: null,
|
||||
stream_options: { include_usage: true },
|
||||
}
|
||||
|
||||
if (["o1", "o3-mini", "o3", "o4-mini"].includes(model.id)) {
|
||||
delete payload.max_tokens
|
||||
delete payload.temperature
|
||||
}
|
||||
|
||||
if (model.id === "o3-mini") {
|
||||
delete payload.stream
|
||||
delete payload.stream_options
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Unsupported model: ${model.id}`)
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.post(url, JSON.stringify(payload, null, 2), {
|
||||
headers,
|
||||
responseType: "stream",
|
||||
})
|
||||
|
||||
if (model.id === "o3-mini") {
|
||||
const response = await axios.post(url, JSON.stringify(payload, null, 2), { headers })
|
||||
|
||||
// Yield the usage information
|
||||
if (response.data.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: response.data.usage.prompt_tokens,
|
||||
outputTokens: response.data.usage.completion_tokens,
|
||||
}
|
||||
}
|
||||
|
||||
// Yield the content
|
||||
if (response.data.choices && response.data.choices.length > 0) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: response.data.choices[0].message.content,
|
||||
}
|
||||
}
|
||||
|
||||
// Final usage yield
|
||||
if (response.data.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: response.data.usage.prompt_tokens,
|
||||
outputTokens: response.data.usage.completion_tokens,
|
||||
}
|
||||
}
|
||||
} else if (openAIModels.includes(model.id)) {
|
||||
yield* this.streamCompletionGPT(response.data, model)
|
||||
} else if (model.id === "anthropic--claude-3.7-sonnet") {
|
||||
yield* this.streamCompletionSonnet37(response.data, model)
|
||||
} else {
|
||||
yield* this.streamCompletion(response.data, model)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.response) {
|
||||
// The request was made and the server responded with a status code
|
||||
// that falls out of the range of 2xx
|
||||
console.error("Error status:", error.response.status)
|
||||
console.error("Error data:", error.response.data)
|
||||
console.error("Error headers:", error.response.headers)
|
||||
|
||||
if (error.response.status === 404) {
|
||||
console.error("404 Error reason:", error.response.data)
|
||||
throw new Error(`404 Not Found: ${error.response.data}`)
|
||||
}
|
||||
} else if (error.request) {
|
||||
// The request was made but no response was received
|
||||
console.error("Error request:", error.request)
|
||||
throw new Error("No response received from server")
|
||||
} else {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.error("Error message:", error.message)
|
||||
throw new Error(`Error setting up request: ${error.message}`)
|
||||
}
|
||||
|
||||
throw new Error("Failed to create message")
|
||||
}
|
||||
}
|
||||
|
||||
private async *streamCompletion(
|
||||
stream: any,
|
||||
model: { id: SapAiCoreModelId; info: ModelInfo },
|
||||
): AsyncGenerator<any, void, unknown> {
|
||||
let usage = { input_tokens: 0, output_tokens: 0 }
|
||||
|
||||
try {
|
||||
for await (const chunk of stream) {
|
||||
const lines = chunk.toString().split("\n").filter(Boolean)
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("data: ")) {
|
||||
const jsonData = line.slice(6)
|
||||
try {
|
||||
const data = JSON.parse(jsonData)
|
||||
console.log("Received data:", data)
|
||||
if (data.type === "message_start") {
|
||||
usage.input_tokens = data.message.usage.input_tokens
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: usage.input_tokens,
|
||||
outputTokens: usage.output_tokens,
|
||||
}
|
||||
} else if (data.type === "content_block_start" || data.type === "content_block_delta") {
|
||||
const contentBlock = data.type === "content_block_start" ? data.content_block : data.delta
|
||||
|
||||
if (contentBlock.type === "text" || contentBlock.type === "text_delta") {
|
||||
yield {
|
||||
type: "text",
|
||||
text: contentBlock.text || "",
|
||||
}
|
||||
}
|
||||
} else if (data.type === "message_delta") {
|
||||
if (data.usage) {
|
||||
usage.output_tokens = data.usage.output_tokens
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: 0,
|
||||
outputTokens: data.usage.output_tokens,
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to parse JSON data:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error streaming completion:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private async *streamCompletionSonnet37(
|
||||
stream: any,
|
||||
model: { id: SapAiCoreModelId; info: ModelInfo },
|
||||
): AsyncGenerator<any, void, unknown> {
|
||||
function toStrictJson(str: string): string {
|
||||
// Wrap it in parentheses so JS will treat it as an expression
|
||||
const obj = new Function("return " + str)()
|
||||
return JSON.stringify(obj)
|
||||
}
|
||||
|
||||
let usage = { input_tokens: 0, output_tokens: 0 }
|
||||
|
||||
try {
|
||||
// Iterate over the stream and process each chunk
|
||||
for await (const chunk of stream) {
|
||||
const lines = chunk.toString().split("\n").filter(Boolean)
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("data: ")) {
|
||||
const jsonData = line.slice(6)
|
||||
|
||||
try {
|
||||
// Parse the incoming JSON data from the stream
|
||||
const data = JSON.parse(toStrictJson(jsonData))
|
||||
console.log("Received data:", data)
|
||||
|
||||
// Handle metadata (token usage)
|
||||
if (data.metadata?.usage) {
|
||||
const inputTokens = data.metadata.usage.inputTokens || 0
|
||||
const outputTokens = data.metadata.usage.outputTokens || 0
|
||||
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle content block delta (text generation)
|
||||
if (data.contentBlockDelta) {
|
||||
if (data.contentBlockDelta?.delta?.text) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: data.contentBlockDelta.delta.text,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle reasoning content if present
|
||||
if (data.contentBlockDelta?.delta?.reasoningContent?.text) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: data.contentBlockDelta.delta.reasoningContent.text,
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to parse JSON data:", error)
|
||||
yield {
|
||||
type: "text",
|
||||
text: `[ERROR] Failed to parse response data: ${error instanceof Error ? error.message : String(error)}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error streaming completion:", error)
|
||||
yield {
|
||||
type: "text",
|
||||
text: `[ERROR] Failed to process stream: ${error instanceof Error ? error.message : String(error)}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async *streamCompletionGPT(
|
||||
stream: any,
|
||||
model: { id: SapAiCoreModelId; info: ModelInfo },
|
||||
): AsyncGenerator<any, void, unknown> {
|
||||
let currentContent = ""
|
||||
let inputTokens = 0
|
||||
let outputTokens = 0
|
||||
|
||||
try {
|
||||
for await (const chunk of stream) {
|
||||
const lines = chunk.toString().split("\n").filter(Boolean)
|
||||
for (const line of lines) {
|
||||
if (line.trim() === "data: [DONE]") {
|
||||
// End of stream, yield final usage
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (line.startsWith("data: ")) {
|
||||
const jsonData = line.slice(6)
|
||||
try {
|
||||
const data = JSON.parse(jsonData)
|
||||
console.log("Received GPT data:", data)
|
||||
|
||||
if (data.choices && data.choices.length > 0) {
|
||||
const choice = data.choices[0]
|
||||
if (choice.delta && choice.delta.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: choice.delta.content,
|
||||
}
|
||||
currentContent += choice.delta.content
|
||||
}
|
||||
}
|
||||
|
||||
// Handle usage information
|
||||
if (data.usage) {
|
||||
inputTokens = data.usage.prompt_tokens || inputTokens
|
||||
outputTokens = data.usage.completion_tokens || outputTokens
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
}
|
||||
}
|
||||
|
||||
if (data.choices && data.choices[0].finish_reason === "stop") {
|
||||
// Final usage yield, if not already provided
|
||||
if (!data.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to parse GPT JSON data:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error streaming GPT completion:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
createUserReadableRequest(
|
||||
userContent: Array<
|
||||
Anthropic.TextBlockParam | Anthropic.ImageBlockParam | Anthropic.ToolUseBlockParam | Anthropic.ToolResultBlockParam
|
||||
>,
|
||||
): any {
|
||||
return {
|
||||
model: this.getModel().id,
|
||||
max_tokens: this.getModel().info.maxTokens,
|
||||
system: "(see SYSTEM_PROMPT in src/ClaudeDev.ts)",
|
||||
messages: [{ conversation_history: "..." }, { role: "user", content: userContent }],
|
||||
tools: "(see tools in src/ClaudeDev.ts)",
|
||||
tool_choice: { type: "auto" },
|
||||
}
|
||||
}
|
||||
|
||||
getModel(): { id: SapAiCoreModelId; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
if (modelId && modelId in sapAiCoreModels) {
|
||||
const id = modelId as SapAiCoreModelId
|
||||
return { id, info: sapAiCoreModels[id] }
|
||||
}
|
||||
return { id: sapAiCoreDefaultModelId, info: sapAiCoreModels[sapAiCoreDefaultModelId] }
|
||||
}
|
||||
|
||||
private getValidImageFormat(mediaType: string): string {
|
||||
const format = mediaType.split("/")[1]?.toLowerCase()
|
||||
const validFormats = ["png", "jpeg", "gif", "webp"]
|
||||
|
||||
if (validFormats.includes(format)) {
|
||||
return format
|
||||
}
|
||||
throw new Error(`Unsupported image format: ${format}`)
|
||||
}
|
||||
|
||||
private formatAnthropicMessages(messages: Anthropic.Messages.MessageParam[]): any[] {
|
||||
return messages.map((m) => {
|
||||
const contentBlocks: any[] = []
|
||||
|
||||
if (typeof m.content === "string") {
|
||||
contentBlocks.push({ text: m.content })
|
||||
} else if (Array.isArray(m.content)) {
|
||||
for (const block of m.content) {
|
||||
if (block.type === "text") {
|
||||
if (!block.text) {
|
||||
throw new Error('Text block is missing the "text" field.')
|
||||
}
|
||||
contentBlocks.push({ text: block.text })
|
||||
} else if (block.type === "image") {
|
||||
if (!block.source) {
|
||||
throw new Error('Image block is missing the "source" field.')
|
||||
}
|
||||
|
||||
const { type, media_type, data } = block.source
|
||||
|
||||
if (!type || !media_type || !data) {
|
||||
throw new Error('Image source must have "type", "media_type", and "data" fields.')
|
||||
}
|
||||
|
||||
if (type !== "base64") {
|
||||
throw new Error(`Unsupported image source type: ${type}. Only "base64" is supported.`)
|
||||
}
|
||||
|
||||
const format = this.getValidImageFormat(media_type)
|
||||
|
||||
contentBlocks.push({
|
||||
image: {
|
||||
format,
|
||||
source: {
|
||||
bytes: data,
|
||||
},
|
||||
},
|
||||
})
|
||||
} else {
|
||||
throw new Error(`Unsupported content block type: ${block.type}`)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Error("Unsupported content format.")
|
||||
}
|
||||
|
||||
return {
|
||||
role: m.role,
|
||||
content: contentBlocks,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,5 @@ export interface ApiStreamUsageChunk {
|
||||
outputTokens: number
|
||||
cacheWriteTokens?: number
|
||||
cacheReadTokens?: number
|
||||
thoughtsTokenCount?: number // openrouter
|
||||
totalCost?: number // openrouter
|
||||
}
|
||||
|
||||
@@ -8,7 +8,10 @@ export function checkIsOpenRouterContextWindowError(error: any): boolean {
|
||||
|
||||
export function checkIsAnthropicContextWindowError(response: any): boolean {
|
||||
try {
|
||||
return response?.error?.error?.type === "invalid_request_error"
|
||||
return (
|
||||
response?.error?.error?.type === "invalid_request_error" &&
|
||||
response?.error?.error?.message?.includes("prompt is too long")
|
||||
)
|
||||
} catch (e: unknown) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { getTaskMetadata, saveTaskMetadata } from "@core/storage/disk"
|
||||
import { getWorkspaceState, updateWorkspaceState } from "@core/storage/state"
|
||||
import { getGlobalState } from "@core/storage/state"
|
||||
import type { FileMetadataEntry } from "./ContextTrackerTypes"
|
||||
import type { ClineMessage } from "@shared/ExtensionMessage"
|
||||
|
||||
// This class is responsible for tracking file operations that may result in stale context.
|
||||
// If a user modifies a file outside of Cline, the context may become stale and need to be updated.
|
||||
@@ -14,12 +11,10 @@ import type { ClineMessage } from "@shared/ExtensionMessage"
|
||||
// a diff edit because the file was modified since Cline last read it.
|
||||
|
||||
// FileContextTracker
|
||||
/**
|
||||
This class is responsible for tracking file operations.
|
||||
If the full contents of a file are passed to Cline via a tool, mention, or edit, the file is marked as active.
|
||||
If a file is modified outside of Cline, we detect and track this change to prevent stale context.
|
||||
This is used when restoring a task (non-git "checkpoint" restore), and mid-task.
|
||||
*/
|
||||
//
|
||||
// This class is responsible for tracking file operations.
|
||||
// If the full contents of a file are pass to Cline via a tool, mention, or edit, the file is marked as active.
|
||||
// If a file is modified outside of Cline, we detect and track this change to prevent stale context.
|
||||
export class FileContextTracker {
|
||||
private context: vscode.ExtensionContext
|
||||
readonly taskId: string
|
||||
@@ -34,9 +29,7 @@ export class FileContextTracker {
|
||||
this.taskId = taskId
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current working directory or returns undefined if it cannot be determined
|
||||
*/
|
||||
// Gets the current working directory or returns undefined if it cannot be determined
|
||||
private getCwd(): string | undefined {
|
||||
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
|
||||
if (!cwd) {
|
||||
@@ -45,9 +38,7 @@ export class FileContextTracker {
|
||||
return cwd
|
||||
}
|
||||
|
||||
/**
|
||||
* File watchers are set up for each file that is tracked in the task metadata.
|
||||
*/
|
||||
// File watchers are set up for each file that is tracked in the task metadata.
|
||||
async setupFileWatcher(filePath: string) {
|
||||
// Only setup watcher if it doesn't already exist for this file
|
||||
if (this.fileWatchers.has(filePath)) {
|
||||
@@ -79,10 +70,8 @@ export class FileContextTracker {
|
||||
this.fileWatchers.set(filePath, watcher)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks a file operation in metadata and sets up a watcher for the file
|
||||
* This is the main entry point for FileContextTracker and is called when a file is passed to Cline via a tool, mention, or edit.
|
||||
*/
|
||||
// Tracks a file operation in metadata and sets up a watcher for the file
|
||||
// This is the main entry point for FileContextTracker and is called when a file is passed to Cline via a tool, mention, or edit.
|
||||
async trackFileContext(filePath: string, operation: "read_tool" | "user_edited" | "cline_edited" | "file_mentioned") {
|
||||
try {
|
||||
const cwd = this.getCwd()
|
||||
@@ -100,11 +89,9 @@ export class FileContextTracker {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a file to the metadata tracker
|
||||
* This handles the business logic of determining if the file is new, stale, or active.
|
||||
* It also updates the metadata with the latest read/edit dates.
|
||||
*/
|
||||
// Adds a file to the metadata tracker
|
||||
// This handles the business logic of determining if the file is new, stale, or active.
|
||||
// It also updates the metadata with the latest read/edit dates.
|
||||
async addFileToFileContextTracker(
|
||||
context: vscode.ExtensionContext,
|
||||
taskId: string,
|
||||
@@ -167,149 +154,23 @@ export class FileContextTracker {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns (and then clears) the set of recently modified files
|
||||
*/
|
||||
// Returns (and then clears) the set of recently modified files
|
||||
getAndClearRecentlyModifiedFiles(): string[] {
|
||||
const files = Array.from(this.recentlyModifiedFiles)
|
||||
this.recentlyModifiedFiles.clear()
|
||||
return files
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a file as edited by Cline to prevent false positives in file watchers
|
||||
*/
|
||||
// Marks a file as edited by Cline to prevent false positives in file watchers
|
||||
markFileAsEditedByCline(filePath: string): void {
|
||||
this.recentlyEditedByCline.add(filePath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes all file watchers
|
||||
*/
|
||||
// Disposes all file watchers
|
||||
dispose(): void {
|
||||
for (const watcher of this.fileWatchers.values()) {
|
||||
watcher.dispose()
|
||||
}
|
||||
this.fileWatchers.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects files that were edited by Cline or users after a specific message timestamp
|
||||
* This is used when restoring checkpoints to warn about potential file content mismatches
|
||||
*/
|
||||
async detectFilesEditedAfterMessage(messageTs: number, deletedMessages: ClineMessage[]): Promise<string[]> {
|
||||
const editedFiles: string[] = []
|
||||
|
||||
try {
|
||||
// Check task metadata for files that were edited by Cline or users after the message timestamp
|
||||
const taskMetadata = await getTaskMetadata(this.context, this.taskId)
|
||||
|
||||
if (taskMetadata?.files_in_context) {
|
||||
for (const fileEntry of taskMetadata.files_in_context) {
|
||||
const clineEditedAfter = fileEntry.cline_edit_date && fileEntry.cline_edit_date > messageTs
|
||||
const userEditedAfter = fileEntry.user_edit_date && fileEntry.user_edit_date > messageTs
|
||||
|
||||
if (clineEditedAfter || userEditedAfter) {
|
||||
editedFiles.push(fileEntry.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error checking file context metadata:", error)
|
||||
}
|
||||
|
||||
// Also check deleted task messages for file operations
|
||||
for (const message of deletedMessages) {
|
||||
if (message.say === "tool" && message.text) {
|
||||
try {
|
||||
const toolData = JSON.parse(message.text)
|
||||
if ((toolData.tool === "editedExistingFile" || toolData.tool === "newFileCreated") && toolData.path) {
|
||||
if (!editedFiles.includes(toolData.path)) {
|
||||
editedFiles.push(toolData.path)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error checking task messages:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...new Set(editedFiles)]
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores pending file context warning in workspace state so it persists across task reinitialization
|
||||
*/
|
||||
async storePendingFileContextWarning(files: string[]): Promise<void> {
|
||||
try {
|
||||
const key = `pendingFileContextWarning_${this.taskId}`
|
||||
await updateWorkspaceState(this.context, key, files)
|
||||
} catch (error) {
|
||||
console.error("Error storing pending file context warning:", error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves pending file context warning from workspace state (without clearing it)
|
||||
*/
|
||||
async retrievePendingFileContextWarning(): Promise<string[] | undefined> {
|
||||
try {
|
||||
const key = `pendingFileContextWarning_${this.taskId}`
|
||||
const files = (await getWorkspaceState(this.context, key)) as string[]
|
||||
return files
|
||||
} catch (error) {
|
||||
console.error("Error retrieving pending file context warning:", error)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves and clears pending file context warning from workspace state
|
||||
*/
|
||||
async retrieveAndClearPendingFileContextWarning(): Promise<string[] | undefined> {
|
||||
try {
|
||||
const files = await this.retrievePendingFileContextWarning()
|
||||
if (files) {
|
||||
await updateWorkspaceState(this.context, `pendingFileContextWarning_${this.taskId}`, undefined)
|
||||
return files
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error retrieving pending file context warning:", error)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Static method to clean up orphaned pending file context warnings at startup
|
||||
* This removes warnings for tasks that may no longer exist
|
||||
*/
|
||||
static async cleanupOrphanedWarnings(context: vscode.ExtensionContext): Promise<void> {
|
||||
const startTime = Date.now()
|
||||
try {
|
||||
const taskHistory = ((await getGlobalState(context, "taskHistory")) as Array<{ id: string }>) || []
|
||||
const existingTaskIds = new Set(taskHistory.map((task) => task.id))
|
||||
const allStateKeys = context.workspaceState.keys()
|
||||
const pendingWarningKeys = allStateKeys.filter((key) => key.startsWith("pendingFileContextWarning_"))
|
||||
|
||||
const orphanedPendingContextTasks: string[] = []
|
||||
for (const key of pendingWarningKeys) {
|
||||
const taskId = key.replace("pendingFileContextWarning_", "")
|
||||
if (!existingTaskIds.has(taskId)) {
|
||||
orphanedPendingContextTasks.push(key)
|
||||
}
|
||||
}
|
||||
|
||||
if (orphanedPendingContextTasks.length > 0) {
|
||||
for (const key of orphanedPendingContextTasks) {
|
||||
await updateWorkspaceState(context, key, undefined)
|
||||
}
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime
|
||||
console.log(
|
||||
`FileContextTracker: Processed ${existingTaskIds.size} tasks, found ${pendingWarningKeys.length} pending warnings, ${orphanedPendingContextTasks.length} orphaned, deleted ${orphanedPendingContextTasks.length}, took ${duration}ms`,
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Error cleaning up orphaned file context warnings:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,17 +64,17 @@ export class GrpcRequestRegistry {
|
||||
*/
|
||||
public cancelRequest(requestId: string): boolean {
|
||||
const requestInfo = this.activeRequests.get(requestId)
|
||||
if (!requestInfo) {
|
||||
return false
|
||||
if (requestInfo) {
|
||||
try {
|
||||
requestInfo.cleanup()
|
||||
console.log(`[DEBUG] Cleaned up request: ${requestId}`)
|
||||
} catch (error) {
|
||||
console.error(`Error cleaning up request ${requestId}:`, error)
|
||||
}
|
||||
this.activeRequests.delete(requestId)
|
||||
return true
|
||||
}
|
||||
try {
|
||||
requestInfo.cleanup()
|
||||
console.log(`[DEBUG] Cleaned up request: ${requestId}`)
|
||||
} catch (error) {
|
||||
console.error(`Error cleaning up request ${requestId}:`, error)
|
||||
}
|
||||
this.activeRequests.delete(requestId)
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -130,13 +130,12 @@ export class Controller {
|
||||
await this.clearTask() // ensures that an existing task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one
|
||||
const {
|
||||
apiConfiguration,
|
||||
customInstructions,
|
||||
autoApprovalSettings,
|
||||
browserSettings,
|
||||
chatSettings,
|
||||
shellIntegrationTimeout,
|
||||
terminalReuseEnabled,
|
||||
terminalOutputLineLimit,
|
||||
defaultTerminalProfile,
|
||||
enableCheckpointsSetting,
|
||||
isNewUser,
|
||||
taskHistory,
|
||||
@@ -172,9 +171,8 @@ export class Controller {
|
||||
chatSettings,
|
||||
shellIntegrationTimeout,
|
||||
terminalReuseEnabled ?? true,
|
||||
terminalOutputLineLimit ?? 500,
|
||||
defaultTerminalProfile ?? "default",
|
||||
enableCheckpointsSetting ?? true,
|
||||
customInstructions,
|
||||
task,
|
||||
images,
|
||||
files,
|
||||
@@ -206,7 +204,15 @@ export class Controller {
|
||||
await this.setUserInfo(message.user || undefined)
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
|
||||
case "apiConfiguration":
|
||||
if (message.apiConfiguration) {
|
||||
await updateApiConfiguration(this.context, message.apiConfiguration)
|
||||
if (this.task) {
|
||||
this.task.api = buildApiHandler(message.apiConfiguration)
|
||||
}
|
||||
}
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
case "fetchUserCreditsData": {
|
||||
await this.fetchUserCreditsData()
|
||||
break
|
||||
@@ -224,7 +230,6 @@ export class Controller {
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
}
|
||||
|
||||
case "clearAllTaskHistory": {
|
||||
const answer = await vscode.window.showWarningMessage(
|
||||
"What would you like to delete?",
|
||||
@@ -285,12 +290,6 @@ export class Controller {
|
||||
previousModeReasoningEffort: newReasoningEffort,
|
||||
previousModeAwsBedrockCustomSelected: newAwsBedrockCustomSelected,
|
||||
previousModeAwsBedrockCustomModelBaseId: newAwsBedrockCustomModelBaseId,
|
||||
previousModeSapAiCoreClientId: newSapAiCoreClientId,
|
||||
previousModeSapAiCoreClientSecret: newSapAiCoreClientSecret,
|
||||
previousModeSapAiCoreBaseUrl: newSapAiCoreBaseUrl,
|
||||
previousModeSapAiCoreTokenUrl: newSapAiCoreTokenUrl,
|
||||
previousModeSapAiCoreResourceGroup: newSapAiResourceGroup,
|
||||
previousModeSapAiCoreModelId: newSapAiCoreModelId,
|
||||
planActSeparateModelsSetting,
|
||||
} = await getAllExtensionState(this.context)
|
||||
|
||||
@@ -356,23 +355,6 @@ export class Controller {
|
||||
await updateWorkspaceState(this.context, "previousModeModelId", apiConfiguration.requestyModelId)
|
||||
await updateWorkspaceState(this.context, "previousModeModelInfo", apiConfiguration.requestyModelInfo)
|
||||
break
|
||||
case "sapaicore":
|
||||
await updateWorkspaceState(this.context, "previousModeModelId", apiConfiguration.apiModelId)
|
||||
await updateWorkspaceState(this.context, "previousModeSapAiCoreClientId", apiConfiguration.sapAiCoreClientId)
|
||||
await updateWorkspaceState(
|
||||
this.context,
|
||||
"previousModeSapAiCoreClientSecret",
|
||||
apiConfiguration.sapAiCoreClientSecret,
|
||||
)
|
||||
await updateWorkspaceState(this.context, "previousModeSapAiCoreBaseUrl", apiConfiguration.sapAiCoreBaseUrl)
|
||||
await updateWorkspaceState(this.context, "previousModeSapAiCoreTokenUrl", apiConfiguration.sapAiCoreTokenUrl)
|
||||
await updateWorkspaceState(
|
||||
this.context,
|
||||
"previousModeSapAiCoreResourceGroup",
|
||||
apiConfiguration.sapAiResourceGroup,
|
||||
)
|
||||
await updateWorkspaceState(this.context, "previousModeSapAiCoreModelId", apiConfiguration.sapAiCoreModelId)
|
||||
break
|
||||
}
|
||||
|
||||
// Restore the model used in previous mode
|
||||
@@ -428,9 +410,6 @@ export class Controller {
|
||||
await updateWorkspaceState(this.context, "requestyModelId", newModelId)
|
||||
await updateWorkspaceState(this.context, "requestyModelInfo", newModelInfo)
|
||||
break
|
||||
case "sapaicore":
|
||||
await updateWorkspaceState(this.context, "apiModelId", newModelId)
|
||||
break
|
||||
}
|
||||
|
||||
if (this.task) {
|
||||
@@ -489,6 +468,14 @@ export class Controller {
|
||||
}
|
||||
}
|
||||
|
||||
async updateCustomInstructions(instructions?: string) {
|
||||
// User may be clearing the field
|
||||
await updateGlobalState(this.context, "customInstructions", instructions || undefined)
|
||||
if (this.task) {
|
||||
this.task.customInstructions = instructions || undefined
|
||||
}
|
||||
}
|
||||
|
||||
// Account
|
||||
|
||||
async fetchUserCreditsData() {
|
||||
@@ -970,13 +957,13 @@ export class Controller {
|
||||
const {
|
||||
apiConfiguration,
|
||||
lastShownAnnouncementId,
|
||||
customInstructions,
|
||||
taskHistory,
|
||||
autoApprovalSettings,
|
||||
browserSettings,
|
||||
chatSettings,
|
||||
userInfo,
|
||||
mcpMarketplaceEnabled,
|
||||
mcpRichDisplayEnabled,
|
||||
telemetrySetting,
|
||||
planActSeparateModelsSetting,
|
||||
enableCheckpointsSetting,
|
||||
@@ -984,10 +971,8 @@ export class Controller {
|
||||
globalWorkflowToggles,
|
||||
shellIntegrationTimeout,
|
||||
terminalReuseEnabled,
|
||||
defaultTerminalProfile,
|
||||
isNewUser,
|
||||
mcpResponsesCollapsed,
|
||||
terminalOutputLineLimit,
|
||||
} = await getAllExtensionState(this.context)
|
||||
|
||||
const localClineRulesToggles =
|
||||
@@ -1004,6 +989,7 @@ export class Controller {
|
||||
return {
|
||||
version: this.context.extension?.packageJSON?.version ?? "",
|
||||
apiConfiguration,
|
||||
customInstructions,
|
||||
uriScheme: vscode.env.uriScheme,
|
||||
currentTaskItem: this.task?.taskId ? (taskHistory || []).find((item) => item.id === this.task?.taskId) : undefined,
|
||||
checkpointTrackerErrorMessage: this.task?.checkpointTrackerErrorMessage,
|
||||
@@ -1019,7 +1005,6 @@ export class Controller {
|
||||
chatSettings,
|
||||
userInfo,
|
||||
mcpMarketplaceEnabled,
|
||||
mcpRichDisplayEnabled,
|
||||
telemetrySetting,
|
||||
planActSeparateModelsSetting,
|
||||
enableCheckpointsSetting: enableCheckpointsSetting ?? true,
|
||||
@@ -1032,10 +1017,8 @@ export class Controller {
|
||||
globalWorkflowToggles: globalWorkflowToggles || {},
|
||||
shellIntegrationTimeout,
|
||||
terminalReuseEnabled,
|
||||
defaultTerminalProfile,
|
||||
isNewUser,
|
||||
mcpResponsesCollapsed,
|
||||
terminalOutputLineLimit,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import type { Controller } from "../index"
|
||||
import { Empty } from "@shared/proto/common"
|
||||
import { UpdateApiConfigurationRequest } from "@shared/proto/models"
|
||||
import { updateApiConfiguration } from "../../storage/state"
|
||||
import { buildApiHandler } from "@api/index"
|
||||
import { convertProtoToApiConfiguration } from "@shared/proto-conversions/models/api-configuration-conversion"
|
||||
|
||||
/**
|
||||
* Updates API configuration
|
||||
* @param controller The controller instance
|
||||
* @param request The update API configuration request
|
||||
* @returns Empty response
|
||||
*/
|
||||
export async function updateApiConfigurationProto(
|
||||
controller: Controller,
|
||||
request: UpdateApiConfigurationRequest,
|
||||
): Promise<Empty> {
|
||||
try {
|
||||
if (!request.apiConfiguration) {
|
||||
console.log("[APICONFIG: updateApiConfigurationProto] API configuration is required")
|
||||
throw new Error("API configuration is required")
|
||||
}
|
||||
|
||||
// Convert proto ApiConfiguration to application ApiConfiguration
|
||||
const appApiConfiguration = convertProtoToApiConfiguration(request.apiConfiguration)
|
||||
|
||||
// Update the API configuration in storage
|
||||
await updateApiConfiguration(controller.context, appApiConfiguration)
|
||||
|
||||
// Update the task's API handler if there's an active task
|
||||
if (controller.task) {
|
||||
controller.task.api = buildApiHandler(appApiConfiguration)
|
||||
}
|
||||
|
||||
// Post updated state to webview
|
||||
await controller.postStateToWebview()
|
||||
|
||||
return Empty.create()
|
||||
} catch (error) {
|
||||
console.error(`Failed to update API configuration: ${error}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Controller } from "../index"
|
||||
import * as proto from "@/shared/proto"
|
||||
import { getAvailableTerminalProfiles as getTerminalProfilesFromShell } from "../../../utils/shell"
|
||||
|
||||
export async function getAvailableTerminalProfiles(
|
||||
controller: Controller,
|
||||
request: proto.cline.EmptyRequest,
|
||||
): Promise<proto.cline.TerminalProfiles> {
|
||||
const profiles = getTerminalProfilesFromShell()
|
||||
|
||||
return proto.cline.TerminalProfiles.create({
|
||||
profiles: profiles.map((profile) => ({
|
||||
id: profile.id,
|
||||
name: profile.name,
|
||||
path: profile.path || "",
|
||||
description: profile.description || "",
|
||||
})),
|
||||
})
|
||||
}
|
||||
@@ -1,25 +1,19 @@
|
||||
import { Controller } from ".."
|
||||
import { Empty } from "../../../shared/proto/common"
|
||||
import { ResetStateRequest } from "../../../shared/proto/state"
|
||||
import { resetGlobalState, resetWorkspaceState } from "../../../core/storage/state"
|
||||
import { Empty, EmptyRequest } from "../../../shared/proto/common"
|
||||
import { resetExtensionState } from "../../../core/storage/state"
|
||||
import * as vscode from "vscode"
|
||||
import { sendChatButtonClickedEvent } from "../ui/subscribeToChatButtonClicked"
|
||||
|
||||
/**
|
||||
* Resets the extension state to its defaults
|
||||
* @param controller The controller instance
|
||||
* @param request The reset state request containing the global flag
|
||||
* @param request An empty request (no parameters needed)
|
||||
* @returns An empty response
|
||||
*/
|
||||
export async function resetState(controller: Controller, request: ResetStateRequest): Promise<Empty> {
|
||||
export async function resetState(controller: Controller, request: EmptyRequest): Promise<Empty> {
|
||||
try {
|
||||
if (request.global) {
|
||||
vscode.window.showInformationMessage("Resetting global state...")
|
||||
await resetGlobalState(controller.context)
|
||||
} else {
|
||||
vscode.window.showInformationMessage("Resetting workspace state...")
|
||||
await resetWorkspaceState(controller.context)
|
||||
}
|
||||
vscode.window.showInformationMessage("Resetting state...")
|
||||
await resetExtensionState(controller.context)
|
||||
|
||||
if (controller.task) {
|
||||
controller.task.abortTask()
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import * as vscode from "vscode"
|
||||
import { Controller } from "../index"
|
||||
import * as proto from "@/shared/proto"
|
||||
import { updateGlobalState } from "../../storage/state"
|
||||
import { TerminalInfo } from "@/integrations/terminal/TerminalRegistry"
|
||||
|
||||
export async function updateDefaultTerminalProfile(
|
||||
controller: Controller,
|
||||
request: proto.cline.StringRequest,
|
||||
): Promise<proto.cline.TerminalProfileUpdateResponse> {
|
||||
const profileId = request.value
|
||||
|
||||
// Update the terminal profile in the state
|
||||
await updateGlobalState(controller.context, "defaultTerminalProfile", profileId)
|
||||
|
||||
let closedCount = 0
|
||||
let busyTerminals: TerminalInfo[] = []
|
||||
|
||||
// Update the terminal manager of the current task if it exists
|
||||
if (controller.task) {
|
||||
// Call the updated setDefaultTerminalProfile method that returns closed terminal info
|
||||
const result = controller.task.terminalManager.setDefaultTerminalProfile(profileId)
|
||||
closedCount = result.closedCount
|
||||
busyTerminals = result.busyTerminals
|
||||
|
||||
// Show information message if terminals were closed
|
||||
if (closedCount > 0) {
|
||||
vscode.window.showInformationMessage(
|
||||
`Closed ${closedCount} ${closedCount === 1 ? "terminal" : "terminals"} with different profile.`,
|
||||
)
|
||||
}
|
||||
|
||||
// Show warning if there are busy terminals that couldn't be closed
|
||||
if (busyTerminals.length > 0) {
|
||||
vscode.window.showWarningMessage(
|
||||
`${busyTerminals.length} busy ${busyTerminals.length === 1 ? "terminal has" : "terminals have"} a different profile. ` +
|
||||
`Close ${busyTerminals.length === 1 ? "it" : "them"} to use the new profile for all commands.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast state update to all webviews
|
||||
await controller.postStateToWebview()
|
||||
|
||||
return proto.cline.TerminalProfileUpdateResponse.create({
|
||||
closedCount,
|
||||
busyTerminalsCount: busyTerminals.length,
|
||||
hasBusyTerminals: busyTerminals.length > 0,
|
||||
})
|
||||
}
|
||||
@@ -25,6 +25,11 @@ export async function updateSettings(controller: Controller, request: UpdateSett
|
||||
}
|
||||
}
|
||||
|
||||
// Update custom instructions
|
||||
if (request.customInstructionsSetting !== undefined) {
|
||||
await controller.updateCustomInstructions(request.customInstructionsSetting)
|
||||
}
|
||||
|
||||
// Update telemetry setting
|
||||
if (request.telemetrySetting) {
|
||||
await controller.updateTelemetrySetting(request.telemetrySetting as TelemetrySetting)
|
||||
@@ -50,11 +55,6 @@ export async function updateSettings(controller: Controller, request: UpdateSett
|
||||
await controller.context.globalState.update("mcpResponsesCollapsed", request.mcpResponsesCollapsed)
|
||||
}
|
||||
|
||||
// Update MCP responses collapsed setting
|
||||
if (request.mcpRichDisplayEnabled !== undefined) {
|
||||
await controller.context.globalState.update("mcpRichDisplayEnabled", request.mcpRichDisplayEnabled)
|
||||
}
|
||||
|
||||
// Update chat settings
|
||||
if (request.chatSettings) {
|
||||
const chatSettings = convertProtoChatSettingsToChatSettings(request.chatSettings)
|
||||
@@ -74,11 +74,6 @@ export async function updateSettings(controller: Controller, request: UpdateSett
|
||||
await controller.context.globalState.update("terminalReuseEnabled", request.terminalReuseEnabled)
|
||||
}
|
||||
|
||||
// Update terminal output line limit
|
||||
if (request.terminalOutputLineLimit !== undefined) {
|
||||
await controller.context.globalState.update("terminalOutputLineLimit", Number(request.terminalOutputLineLimit))
|
||||
}
|
||||
|
||||
// Post updated state to webview
|
||||
await controller.postStateToWebview()
|
||||
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
import { Controller } from "../index"
|
||||
import * as proto from "@/shared/proto"
|
||||
import { Controller } from ".."
|
||||
import { Int64, Int64Request } from "../../../shared/proto/common"
|
||||
import { updateGlobalState } from "../../storage/state"
|
||||
|
||||
export async function updateTerminalConnectionTimeout(
|
||||
controller: Controller,
|
||||
request: proto.cline.Int64Request,
|
||||
): Promise<proto.cline.Int64> {
|
||||
const timeoutValue = request.value
|
||||
/**
|
||||
* Updates the terminal connection timeout setting
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing the timeout value in milliseconds
|
||||
* @returns The updated timeout value
|
||||
*/
|
||||
export async function updateTerminalConnectionTimeout(controller: Controller, request: Int64Request): Promise<Int64> {
|
||||
try {
|
||||
const timeout = request.value
|
||||
|
||||
// Update the terminal connection timeout setting in the state
|
||||
await updateGlobalState(controller.context, "shellIntegrationTimeout", timeoutValue)
|
||||
|
||||
// Broadcast state update to all webviews
|
||||
await controller.postStateToWebview()
|
||||
|
||||
return proto.cline.Int64.create({ value: timeoutValue })
|
||||
if (typeof timeout === "number" && !isNaN(timeout) && timeout > 0) {
|
||||
// Update the global state directly
|
||||
await updateGlobalState(controller.context, "shellIntegrationTimeout", timeout)
|
||||
return Int64.create({ value: timeout })
|
||||
} else {
|
||||
console.warn(`Invalid shell integration timeout value received: ${timeout}. Expected a positive number.`)
|
||||
throw new Error("Invalid timeout value. Expected a positive number.")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to update terminal connection timeout: ${error}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { Controller } from "../index"
|
||||
import * as proto from "@/shared/proto"
|
||||
import { updateGlobalState } from "../../storage/state"
|
||||
|
||||
export async function updateTerminalReuseEnabled(
|
||||
controller: Controller,
|
||||
request: proto.cline.BooleanRequest,
|
||||
): Promise<proto.cline.Empty> {
|
||||
const enabled = request.value
|
||||
|
||||
// Update the terminal reuse setting in the state
|
||||
await updateGlobalState(controller.context, "terminalReuseEnabled", enabled)
|
||||
|
||||
// Broadcast state update to all webviews
|
||||
await controller.postStateToWebview()
|
||||
|
||||
return proto.cline.Empty.create({})
|
||||
}
|
||||
@@ -663,6 +663,7 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
|
||||
}
|
||||
|
||||
export function addUserInstructions(
|
||||
settingsCustomInstructions?: string,
|
||||
globalClineRulesFileInstructions?: string,
|
||||
localClineRulesFileInstructions?: string,
|
||||
localCursorRulesFileInstructions?: string,
|
||||
@@ -675,6 +676,9 @@ export function addUserInstructions(
|
||||
if (preferredLanguageInstructions) {
|
||||
customInstructions += preferredLanguageInstructions + "\n\n"
|
||||
}
|
||||
if (settingsCustomInstructions) {
|
||||
customInstructions += settingsCustomInstructions + "\n\n"
|
||||
}
|
||||
if (globalClineRulesFileInstructions) {
|
||||
customInstructions += globalClineRulesFileInstructions + "\n\n"
|
||||
}
|
||||
|
||||
@@ -149,14 +149,13 @@ Otherwise, if you have not completed the task and do not need additional informa
|
||||
cwd: string,
|
||||
wasRecent: boolean | 0 | undefined,
|
||||
responseText?: string,
|
||||
hasPendingFileContextWarnings?: boolean,
|
||||
): [string, string] => {
|
||||
const taskResumptionMessage = `[TASK RESUMPTION] ${
|
||||
mode === "plan"
|
||||
? `This task was interrupted ${agoText}. The conversation may have been incomplete. Be aware that the project state may have changed since then. The current working directory is now '${cwd.toPosix()}'.\n\nNote: If you previously attempted a tool use that the user did not provide a result for, you should assume the tool use was not successful. However you are in PLAN MODE, so rather than continuing the task, you must respond to the user's message.`
|
||||
: `This task was interrupted ${agoText}. It may or may not be complete, so please reassess the task context. Be aware that the project state may have changed since then. The current working directory is now '${cwd.toPosix()}'. If the task has not been completed, retry the last step before interruption and proceed with completing the task.\n\nNote: If you previously attempted a tool use that the user did not provide a result for, you should assume the tool use was not successful and assess whether you should retry. If the last tool was a browser_action, the browser has been closed and you must launch a new browser if needed.`
|
||||
}${
|
||||
wasRecent && !hasPendingFileContextWarnings
|
||||
wasRecent
|
||||
? "\n\nIMPORTANT: If the last tool use was a replace_in_file or write_to_file that was interrupted, the file was reverted back to its original state before the interrupted edit, and you do NOT need to re-read the file as you already have its up-to-date contents."
|
||||
: ""
|
||||
}`
|
||||
@@ -241,19 +240,6 @@ Otherwise, if you have not completed the task and do not need additional informa
|
||||
|
||||
cursorRulesLocalDirectoryInstructions: (cwd: string, content: string) =>
|
||||
`# .cursor/rules\n\nThe following is provided by a root-level .cursor/rules directory where the user has specified instructions for this working directory (${cwd.toPosix()})\n\n${content}`,
|
||||
|
||||
fileContextWarning: (editedFiles: string[]): string => {
|
||||
const fileCount = editedFiles.length
|
||||
const fileVerb = fileCount === 1 ? "file has" : "files have"
|
||||
const fileDemonstrativePronoun = fileCount === 1 ? "this file" : "these files"
|
||||
const filePersonalPronoun = fileCount === 1 ? "it" : "they"
|
||||
|
||||
return (
|
||||
`<explicit_instructions>\nCRITICAL FILE STATE ALERT: ${fileCount} ${fileVerb} been externally modified since your last interaction. Your cached understanding of ${fileDemonstrativePronoun} is now stale and unreliable. Before making ANY modifications to ${fileDemonstrativePronoun}, you must execute read_file to obtain the current state, as ${filePersonalPronoun} may contain completely different content than what you expect:\n` +
|
||||
`${editedFiles.map((file) => ` ${path.resolve(file).toPosix()}`).join("\n")}\n` +
|
||||
`Failure to re-read before editing will result in replace_in_file edit errors, requiring subsequent attempts and wasting tokens. You DO NOT need to re-read these files after subsequent edits, unless instructed to do so.\n</explicit_instructions>`
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
// to avoid circular dependency
|
||||
|
||||
@@ -651,6 +651,7 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
|
||||
|
||||
|
||||
export function addUserInstructions(
|
||||
settingsCustomInstructions?: string,
|
||||
globalClineRulesFileInstructions?: string,
|
||||
localClineRulesFileInstructions?: string,
|
||||
localCursorRulesFileInstructions?: string,
|
||||
@@ -663,6 +664,9 @@ export function addUserInstructions(
|
||||
if (preferredLanguageInstructions) {
|
||||
customInstructions += preferredLanguageInstructions + "\n\n"
|
||||
}
|
||||
if (settingsCustomInstructions) {
|
||||
customInstructions += settingsCustomInstructions + "\n\n"
|
||||
}
|
||||
if (globalClineRulesFileInstructions) {
|
||||
customInstructions += globalClineRulesFileInstructions + "\n\n"
|
||||
}
|
||||
|
||||
@@ -22,8 +22,6 @@ export type SecretKey =
|
||||
| "nebiusApiKey"
|
||||
| "sambanovaApiKey"
|
||||
| "cerebrasApiKey"
|
||||
| "sapAiCoreClientId"
|
||||
| "sapAiCoreClientSecret"
|
||||
|
||||
export type GlobalStateKey =
|
||||
| "awsRegion"
|
||||
@@ -35,6 +33,7 @@ export type GlobalStateKey =
|
||||
| "vertexProjectId"
|
||||
| "vertexRegion"
|
||||
| "lastShownAnnouncementId"
|
||||
| "customInstructions"
|
||||
| "taskHistory"
|
||||
| "openAiBaseUrl"
|
||||
| "openAiModelId"
|
||||
@@ -69,17 +68,7 @@ export type GlobalStateKey =
|
||||
| "shellIntegrationTimeout"
|
||||
| "mcpResponsesCollapsed"
|
||||
| "terminalReuseEnabled"
|
||||
| "defaultTerminalProfile"
|
||||
| "isNewUser"
|
||||
| "terminalOutputLineLimit"
|
||||
| "mcpRichDisplayEnabled"
|
||||
| "sapAiCoreTokenUrl"
|
||||
| "sapAiCoreBaseUrl"
|
||||
| "sapAiResourceGroup"
|
||||
| "sapAiCoreClientId"
|
||||
| "sapAiCoreClientSecret"
|
||||
| "sapAiCoreModelId"
|
||||
| "claudeCodePath"
|
||||
|
||||
export type LocalStateKey =
|
||||
| "localClineRulesToggles"
|
||||
@@ -113,9 +102,3 @@ export type LocalStateKey =
|
||||
| "previousModeReasoningEffort"
|
||||
| "previousModeAwsBedrockCustomSelected"
|
||||
| "previousModeAwsBedrockCustomModelBaseId"
|
||||
| "previousModeSapAiCoreClientId"
|
||||
| "previousModeSapAiCoreClientSecret"
|
||||
| "previousModeSapAiCoreBaseUrl"
|
||||
| "previousModeSapAiCoreTokenUrl"
|
||||
| "previousModeSapAiCoreResourceGroup"
|
||||
| "previousModeSapAiCoreModelId"
|
||||
|
||||
+4
-120
@@ -11,9 +11,6 @@ import { ChatSettings } from "@shared/ChatSettings"
|
||||
import { TelemetrySetting } from "@shared/TelemetrySetting"
|
||||
import { UserInfo } from "@shared/UserInfo"
|
||||
import { ClineRulesToggles } from "@shared/cline-rules"
|
||||
import { ensureRulesDirectoryExists } from "./disk"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
/*
|
||||
Storage
|
||||
https://dev.to/kompotkot/how-to-use-secretstorage-in-your-vscode-extensions-2hco
|
||||
@@ -128,51 +125,6 @@ async function migrateEnableCheckpointsSetting(enableCheckpointsSettingRaw: bool
|
||||
return enableCheckpointsSettingRaw ?? true
|
||||
}
|
||||
|
||||
export async function migrateCustomInstructionsToGlobalRules(context: vscode.ExtensionContext) {
|
||||
try {
|
||||
const customInstructions = (await context.globalState.get("customInstructions")) as string | undefined
|
||||
|
||||
if (customInstructions?.trim()) {
|
||||
console.log("Migrating custom instructions to global Cline rules...")
|
||||
|
||||
// Create global .clinerules directory if it doesn't exist
|
||||
const globalRulesDir = await ensureRulesDirectoryExists()
|
||||
|
||||
// Use a fixed filename for custom instructions
|
||||
const migrationFileName = "custom_instructions.md"
|
||||
const migrationFilePath = path.join(globalRulesDir, migrationFileName)
|
||||
|
||||
try {
|
||||
// Check if file already exists to determine if we should append
|
||||
let existingContent = ""
|
||||
try {
|
||||
existingContent = await fs.readFile(migrationFilePath, "utf8")
|
||||
} catch (readError) {
|
||||
// File doesn't exist, which is fine
|
||||
}
|
||||
|
||||
// Append or create the file with custom instructions
|
||||
const contentToWrite = existingContent
|
||||
? `${existingContent}\n\n---\n\n${customInstructions.trim()}`
|
||||
: customInstructions.trim()
|
||||
|
||||
await fs.writeFile(migrationFilePath, contentToWrite)
|
||||
console.log(`Successfully ${existingContent ? "appended to" : "created"} migration file: ${migrationFilePath}`)
|
||||
} catch (fileError) {
|
||||
console.error("Failed to write migration file:", fileError)
|
||||
return
|
||||
}
|
||||
|
||||
// Remove customInstructions from global state only after successful file creation
|
||||
await context.globalState.update("customInstructions", undefined)
|
||||
console.log("Successfully migrated custom instructions to global Cline rules")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to migrate custom instructions to global rules:", error)
|
||||
// Continue execution - migration failure shouldn't break extension startup
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
const [
|
||||
isNewUser,
|
||||
@@ -209,6 +161,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
azureApiVersion,
|
||||
openRouterProviderSorting,
|
||||
lastShownAnnouncementId,
|
||||
customInstructions,
|
||||
taskHistory,
|
||||
autoApprovalSettings,
|
||||
browserSettings,
|
||||
@@ -234,19 +187,9 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
shellIntegrationTimeout,
|
||||
enableCheckpointsSettingRaw,
|
||||
mcpMarketplaceEnabledRaw,
|
||||
mcpRichDisplayEnabled,
|
||||
mcpResponsesCollapsedRaw,
|
||||
globalWorkflowToggles,
|
||||
terminalReuseEnabled,
|
||||
terminalOutputLineLimit,
|
||||
defaultTerminalProfile,
|
||||
sapAiCoreClientId,
|
||||
sapAiCoreClientSecret,
|
||||
sapAiCoreBaseUrl,
|
||||
sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup,
|
||||
sapAiCoreModelId,
|
||||
claudeCodePath,
|
||||
] = await Promise.all([
|
||||
getGlobalState(context, "isNewUser") as Promise<boolean | undefined>,
|
||||
getSecret(context, "apiKey") as Promise<string | undefined>,
|
||||
@@ -282,6 +225,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
getGlobalState(context, "azureApiVersion") as Promise<string | undefined>,
|
||||
getGlobalState(context, "openRouterProviderSorting") as Promise<string | undefined>,
|
||||
getGlobalState(context, "lastShownAnnouncementId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "customInstructions") as Promise<string | undefined>,
|
||||
getGlobalState(context, "taskHistory") as Promise<HistoryItem[] | undefined>,
|
||||
getGlobalState(context, "autoApprovalSettings") as Promise<AutoApprovalSettings | undefined>,
|
||||
getGlobalState(context, "browserSettings") as Promise<BrowserSettings | undefined>,
|
||||
@@ -307,19 +251,9 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
getGlobalState(context, "shellIntegrationTimeout") as Promise<number | undefined>,
|
||||
getGlobalState(context, "enableCheckpointsSetting") as Promise<boolean | undefined>,
|
||||
getGlobalState(context, "mcpMarketplaceEnabled") as Promise<boolean | undefined>,
|
||||
getGlobalState(context, "mcpRichDisplayEnabled") as Promise<boolean | undefined>,
|
||||
getGlobalState(context, "mcpResponsesCollapsed") as Promise<boolean | undefined>,
|
||||
getGlobalState(context, "globalWorkflowToggles") as Promise<ClineRulesToggles | undefined>,
|
||||
getGlobalState(context, "terminalReuseEnabled") as Promise<boolean | undefined>,
|
||||
getGlobalState(context, "terminalOutputLineLimit") as Promise<number | undefined>,
|
||||
getGlobalState(context, "defaultTerminalProfile") as Promise<string | undefined>,
|
||||
getSecret(context, "sapAiCoreClientId") as Promise<string | undefined>,
|
||||
getSecret(context, "sapAiCoreClientSecret") as Promise<string | undefined>,
|
||||
getGlobalState(context, "sapAiCoreBaseUrl") as Promise<string | undefined>,
|
||||
getGlobalState(context, "sapAiCoreTokenUrl") as Promise<string | undefined>,
|
||||
getGlobalState(context, "sapAiResourceGroup") as Promise<string | undefined>,
|
||||
getGlobalState(context, "sapAiCoreModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "claudeCodePath") as Promise<string | undefined>,
|
||||
])
|
||||
|
||||
const localClineRulesToggles = (await getWorkspaceState(context, "localClineRulesToggles")) as ClineRulesToggles
|
||||
@@ -353,12 +287,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
previousModeReasoningEffort,
|
||||
previousModeAwsBedrockCustomSelected,
|
||||
previousModeAwsBedrockCustomModelBaseId,
|
||||
previousModeSapAiCoreClientId,
|
||||
previousModeSapAiCoreClientSecret,
|
||||
previousModeSapAiCoreBaseUrl,
|
||||
previousModeSapAiCoreTokenUrl,
|
||||
previousModeSapAiCoreResourceGroup,
|
||||
previousModeSapAiCoreModelId,
|
||||
] = await Promise.all([
|
||||
getWorkspaceState(context, "chatSettings") as Promise<ChatSettings | undefined>,
|
||||
getWorkspaceState(context, "apiProvider") as Promise<ApiProvider | undefined>,
|
||||
@@ -388,12 +316,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
getWorkspaceState(context, "previousModeReasoningEffort") as Promise<string | undefined>,
|
||||
getWorkspaceState(context, "previousModeAwsBedrockCustomSelected") as Promise<boolean | undefined>,
|
||||
getWorkspaceState(context, "previousModeAwsBedrockCustomModelBaseId") as Promise<BedrockModelId | undefined>,
|
||||
getWorkspaceState(context, "previousModeSapAiCoreClientId") as Promise<string | undefined>,
|
||||
getWorkspaceState(context, "previousModeSapAiCoreClientSecret") as Promise<string | undefined>,
|
||||
getWorkspaceState(context, "previousModeSapAiCoreBaseUrl") as Promise<string | undefined>,
|
||||
getWorkspaceState(context, "previousModeSapAiCoreTokenUrl") as Promise<string | undefined>,
|
||||
getWorkspaceState(context, "previousModeSapAiCoreResourceGroup") as Promise<string | undefined>,
|
||||
getWorkspaceState(context, "previousModeSapAiCoreModelId") as Promise<string | undefined>,
|
||||
])
|
||||
|
||||
let apiProvider: ApiProvider
|
||||
@@ -439,7 +361,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
apiKey,
|
||||
openRouterApiKey,
|
||||
clineApiKey,
|
||||
claudeCodePath,
|
||||
awsAccessKey,
|
||||
awsSecretKey,
|
||||
awsSessionToken,
|
||||
@@ -501,15 +422,10 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
nebiusApiKey,
|
||||
favoritedModelIds,
|
||||
requestTimeoutMs,
|
||||
sapAiCoreClientId,
|
||||
sapAiCoreClientSecret,
|
||||
sapAiCoreBaseUrl,
|
||||
sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup,
|
||||
sapAiCoreModelId,
|
||||
},
|
||||
isNewUser: isNewUser ?? true,
|
||||
lastShownAnnouncementId,
|
||||
customInstructions,
|
||||
taskHistory,
|
||||
autoApprovalSettings: autoApprovalSettings || DEFAULT_AUTO_APPROVAL_SETTINGS, // default value can be 0 or empty string
|
||||
globalClineRulesToggles: globalClineRulesToggles || {},
|
||||
@@ -528,22 +444,13 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
previousModeReasoningEffort,
|
||||
previousModeAwsBedrockCustomSelected,
|
||||
previousModeAwsBedrockCustomModelBaseId,
|
||||
previousModeSapAiCoreClientId,
|
||||
previousModeSapAiCoreClientSecret,
|
||||
previousModeSapAiCoreBaseUrl,
|
||||
previousModeSapAiCoreTokenUrl,
|
||||
previousModeSapAiCoreResourceGroup,
|
||||
previousModeSapAiCoreModelId,
|
||||
mcpMarketplaceEnabled: mcpMarketplaceEnabled,
|
||||
mcpRichDisplayEnabled: mcpRichDisplayEnabled ?? true,
|
||||
mcpResponsesCollapsed: mcpResponsesCollapsed,
|
||||
telemetrySetting: telemetrySetting || "unset",
|
||||
planActSeparateModelsSetting,
|
||||
enableCheckpointsSetting: enableCheckpointsSetting,
|
||||
shellIntegrationTimeout: shellIntegrationTimeout || 4000,
|
||||
terminalReuseEnabled: terminalReuseEnabled ?? true,
|
||||
terminalOutputLineLimit: terminalOutputLineLimit ?? 500,
|
||||
defaultTerminalProfile: defaultTerminalProfile ?? "default",
|
||||
globalWorkflowToggles: globalWorkflowToggles || {},
|
||||
}
|
||||
}
|
||||
@@ -615,13 +522,6 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
fireworksModelId,
|
||||
fireworksModelMaxCompletionTokens,
|
||||
fireworksModelMaxTokens,
|
||||
sapAiCoreClientId,
|
||||
sapAiCoreClientSecret,
|
||||
sapAiCoreBaseUrl,
|
||||
sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup,
|
||||
sapAiCoreModelId,
|
||||
claudeCodePath,
|
||||
} = apiConfiguration
|
||||
// Workspace state updates
|
||||
await updateWorkspaceState(context, "apiProvider", apiProvider)
|
||||
@@ -670,13 +570,6 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
await updateGlobalState(context, "requestTimeoutMs", apiConfiguration.requestTimeoutMs)
|
||||
await updateGlobalState(context, "fireworksModelMaxCompletionTokens", fireworksModelMaxCompletionTokens)
|
||||
await updateGlobalState(context, "fireworksModelMaxTokens", fireworksModelMaxTokens)
|
||||
await updateGlobalState(context, "favoritedModelIds", favoritedModelIds)
|
||||
await updateGlobalState(context, "requestTimeoutMs", apiConfiguration.requestTimeoutMs)
|
||||
await updateGlobalState(context, "sapAiCoreBaseUrl", sapAiCoreBaseUrl)
|
||||
await updateGlobalState(context, "sapAiCoreTokenUrl", sapAiCoreTokenUrl)
|
||||
await updateGlobalState(context, "sapAiResourceGroup", sapAiResourceGroup)
|
||||
await updateGlobalState(context, "sapAiCoreModelId", sapAiCoreModelId)
|
||||
await updateGlobalState(context, "claudeCodePath", claudeCodePath)
|
||||
|
||||
// Secret updates
|
||||
await storeSecret(context, "apiKey", apiKey)
|
||||
@@ -701,18 +594,9 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
await storeSecret(context, "sambanovaApiKey", sambanovaApiKey)
|
||||
await storeSecret(context, "cerebrasApiKey", cerebrasApiKey)
|
||||
await storeSecret(context, "nebiusApiKey", nebiusApiKey)
|
||||
await storeSecret(context, "sapAiCoreClientId", sapAiCoreClientId)
|
||||
await storeSecret(context, "sapAiCoreClientSecret", sapAiCoreClientSecret)
|
||||
}
|
||||
|
||||
export async function resetWorkspaceState(context: vscode.ExtensionContext) {
|
||||
for (const key of context.workspaceState.keys()) {
|
||||
await context.workspaceState.update(key, undefined)
|
||||
}
|
||||
}
|
||||
|
||||
export async function resetGlobalState(context: vscode.ExtensionContext) {
|
||||
// TODO: Reset all workspace states?
|
||||
export async function resetExtensionState(context: vscode.ExtensionContext) {
|
||||
for (const key of context.globalState.keys()) {
|
||||
await context.globalState.update(key, undefined)
|
||||
}
|
||||
|
||||
+113
-288
@@ -113,8 +113,6 @@ import { isInTestMode } from "../../services/test/TestMode"
|
||||
import { processFilesIntoText } from "@integrations/misc/extract-text"
|
||||
import { featureFlagsService } from "@services/posthog/feature-flags/FeatureFlagsService"
|
||||
import { StreamingJsonReplacer, ChangeLocation } from "@core/assistant-message/diff-json"
|
||||
import { isClaude4ModelFamily } from "@utils/model-utils"
|
||||
import { saveClineMessagesAndUpdateHistory } from "./message-state"
|
||||
|
||||
export const USE_EXPERIMENTAL_CLAUDE4_FEATURES = false
|
||||
|
||||
@@ -141,11 +139,12 @@ export class Task {
|
||||
readonly taskId: string
|
||||
private taskIsFavorited?: boolean
|
||||
api: ApiHandler
|
||||
terminalManager: TerminalManager
|
||||
private terminalManager: TerminalManager
|
||||
private urlContentFetcher: UrlContentFetcher
|
||||
browserSession: BrowserSession
|
||||
contextManager: ContextManager
|
||||
private didEditFile: boolean = false
|
||||
customInstructions?: string
|
||||
autoApprovalSettings: AutoApprovalSettings
|
||||
browserSettings: BrowserSettings
|
||||
chatSettings: ChatSettings
|
||||
@@ -204,9 +203,8 @@ export class Task {
|
||||
chatSettings: ChatSettings,
|
||||
shellIntegrationTimeout: number,
|
||||
terminalReuseEnabled: boolean,
|
||||
terminalOutputLineLimit: number,
|
||||
defaultTerminalProfile: string,
|
||||
enableCheckpointsSetting: boolean,
|
||||
customInstructions?: string,
|
||||
task?: string,
|
||||
images?: string[],
|
||||
files?: string[],
|
||||
@@ -225,23 +223,16 @@ export class Task {
|
||||
this.terminalManager = new TerminalManager()
|
||||
this.terminalManager.setShellIntegrationTimeout(shellIntegrationTimeout)
|
||||
this.terminalManager.setTerminalReuseEnabled(terminalReuseEnabled ?? true)
|
||||
this.terminalManager.setTerminalOutputLineLimit(terminalOutputLineLimit)
|
||||
this.terminalManager.setDefaultTerminalProfile(defaultTerminalProfile)
|
||||
this.urlContentFetcher = new UrlContentFetcher(context)
|
||||
this.browserSession = new BrowserSession(context, browserSettings)
|
||||
this.contextManager = new ContextManager()
|
||||
this.diffViewProvider = new DiffViewProvider(cwd)
|
||||
this.customInstructions = customInstructions
|
||||
this.autoApprovalSettings = autoApprovalSettings
|
||||
this.browserSettings = browserSettings
|
||||
this.chatSettings = chatSettings
|
||||
this.enableCheckpoints = enableCheckpointsSetting
|
||||
|
||||
// Set up MCP notification callback for real-time notifications
|
||||
this.mcpHub.setNotificationCallback(async (serverName: string, level: string, message: string) => {
|
||||
// Display notification in chat immediately
|
||||
await this.say("mcp_notification", `[${serverName}] ${message}`)
|
||||
})
|
||||
|
||||
// Initialize taskId first
|
||||
if (historyItem) {
|
||||
this.taskId = historyItem.id
|
||||
@@ -348,28 +339,52 @@ export class Task {
|
||||
message.conversationHistoryIndex = this.apiConversationHistory.length - 1 // NOTE: this is the index of the last added message which is the user message, and once the clinemessages have been presented we update the apiconversationhistory with the completed assistant message. This means when resetting to a message, we need to +1 this index to get the correct assistant message that this tool use corresponds to
|
||||
message.conversationHistoryDeletedRange = this.conversationHistoryDeletedRange
|
||||
this.clineMessages.push(message)
|
||||
await saveClineMessagesAndUpdateHistory(
|
||||
this.getContext(),
|
||||
this.taskId,
|
||||
this.clineMessages,
|
||||
this.taskIsFavorited ?? false,
|
||||
this.conversationHistoryDeletedRange,
|
||||
this.checkpointTracker,
|
||||
this.updateTaskHistory,
|
||||
)
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
}
|
||||
|
||||
private async overwriteClineMessages(newMessages: ClineMessage[]) {
|
||||
this.clineMessages = newMessages
|
||||
await saveClineMessagesAndUpdateHistory(
|
||||
this.getContext(),
|
||||
this.taskId,
|
||||
this.clineMessages,
|
||||
this.taskIsFavorited ?? false,
|
||||
this.conversationHistoryDeletedRange,
|
||||
this.checkpointTracker,
|
||||
this.updateTaskHistory,
|
||||
)
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
}
|
||||
|
||||
private async saveClineMessagesAndUpdateHistory() {
|
||||
try {
|
||||
await saveClineMessages(this.getContext(), this.taskId, this.clineMessages)
|
||||
|
||||
// combined as they are in ChatView
|
||||
const apiMetrics = getApiMetrics(combineApiRequests(combineCommandSequences(this.clineMessages.slice(1))))
|
||||
const taskMessage = this.clineMessages[0] // first message is always the task say
|
||||
const lastRelevantMessage =
|
||||
this.clineMessages[
|
||||
findLastIndex(this.clineMessages, (m) => !(m.ask === "resume_task" || m.ask === "resume_completed_task"))
|
||||
]
|
||||
const taskDir = await ensureTaskDirectoryExists(this.getContext(), this.taskId)
|
||||
let taskDirSize = 0
|
||||
try {
|
||||
// getFolderSize.loose silently ignores errors
|
||||
// returns # of bytes, size/1000/1000 = MB
|
||||
taskDirSize = await getFolderSize.loose(taskDir)
|
||||
} catch (error) {
|
||||
console.error("Failed to get task directory size:", taskDir, error)
|
||||
}
|
||||
await this.updateTaskHistory({
|
||||
id: this.taskId,
|
||||
ts: lastRelevantMessage.ts,
|
||||
task: taskMessage.text ?? "",
|
||||
tokensIn: apiMetrics.totalTokensIn,
|
||||
tokensOut: apiMetrics.totalTokensOut,
|
||||
cacheWrites: apiMetrics.totalCacheWrites,
|
||||
cacheReads: apiMetrics.totalCacheReads,
|
||||
totalCost: apiMetrics.totalCost,
|
||||
size: taskDirSize,
|
||||
shadowGitConfigWorkTree: await this.checkpointTracker?.getShadowGitConfigWorkTree(),
|
||||
cwdOnTaskInitialization: cwd,
|
||||
conversationHistoryDeletedRange: this.conversationHistoryDeletedRange,
|
||||
isFavorited: this.taskIsFavorited,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to save cline messages:", error)
|
||||
}
|
||||
}
|
||||
|
||||
async restoreCheckpoint(messageTs: number, restoreType: ClineCheckpointRestore, offset?: number) {
|
||||
@@ -429,18 +444,6 @@ export class Task {
|
||||
vscode.window.showErrorMessage("Failed to restore offsetcheckpoint: " + errorMessage)
|
||||
didWorkspaceRestoreFail = true
|
||||
}
|
||||
} else if (!offset && lastMessageWithHash.lastCheckpointHash && this.checkpointTracker) {
|
||||
// Fallback: restore to most recent checkpoint when target message has no checkpoint hash
|
||||
console.warn(`Message ${messageTs} has no checkpoint hash, falling back to previous checkpoint`)
|
||||
try {
|
||||
await this.checkpointTracker.resetHead(lastMessageWithHash.lastCheckpointHash)
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error"
|
||||
vscode.window.showErrorMessage("Failed to restore checkpoint: " + errorMessage)
|
||||
didWorkspaceRestoreFail = true
|
||||
}
|
||||
} else {
|
||||
vscode.window.showErrorMessage("Failed to restore checkpoint")
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -466,18 +469,6 @@ export class Task {
|
||||
const deletedMessages = this.clineMessages.slice(messageIndex + 1)
|
||||
const deletedApiReqsMetrics = getApiMetrics(combineApiRequests(combineCommandSequences(deletedMessages)))
|
||||
|
||||
// Detect files edited after this message timestamp for file context warning
|
||||
// Only needed for task-only restores when a user edits a message or restores the task context, but not the files.
|
||||
if (restoreType === "task") {
|
||||
const filesEditedAfterMessage = await this.fileContextTracker.detectFilesEditedAfterMessage(
|
||||
messageTs,
|
||||
deletedMessages,
|
||||
)
|
||||
if (filesEditedAfterMessage.length > 0) {
|
||||
await this.fileContextTracker.storePendingFileContextWarning(filesEditedAfterMessage)
|
||||
}
|
||||
}
|
||||
|
||||
const newClineMessages = this.clineMessages.slice(0, messageIndex + 1)
|
||||
await this.overwriteClineMessages(newClineMessages) // calls saveClineMessages which saves historyItem
|
||||
|
||||
@@ -520,15 +511,9 @@ export class Task {
|
||||
})
|
||||
}
|
||||
|
||||
await saveClineMessagesAndUpdateHistory(
|
||||
this.getContext(),
|
||||
this.taskId,
|
||||
this.clineMessages,
|
||||
this.taskIsFavorited ?? false,
|
||||
this.conversationHistoryDeletedRange,
|
||||
this.checkpointTracker,
|
||||
this.updateTaskHistory,
|
||||
)
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
|
||||
sendRelinquishControlEvent()
|
||||
|
||||
this.cancelTask() // the task is already cancelled by the provider beforehand, but we need to re-init to get the updated messages
|
||||
} else {
|
||||
@@ -801,15 +786,7 @@ export class Task {
|
||||
// lastMessage.ts = askTs
|
||||
lastMessage.text = text
|
||||
lastMessage.partial = false
|
||||
await saveClineMessagesAndUpdateHistory(
|
||||
this.getContext(),
|
||||
this.taskId,
|
||||
this.clineMessages,
|
||||
this.taskIsFavorited ?? false,
|
||||
this.conversationHistoryDeletedRange,
|
||||
this.checkpointTracker,
|
||||
this.updateTaskHistory,
|
||||
)
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
// await this.postStateToWebview()
|
||||
const protoMessage = convertClineMessageToProto(lastMessage)
|
||||
await sendPartialMessageEvent(protoMessage)
|
||||
@@ -917,15 +894,7 @@ export class Task {
|
||||
lastMessage.partial = false
|
||||
|
||||
// instead of streaming partialMessage events, we do a save and post like normal to persist to disk
|
||||
await saveClineMessagesAndUpdateHistory(
|
||||
this.getContext(),
|
||||
this.taskId,
|
||||
this.clineMessages,
|
||||
this.taskIsFavorited ?? false,
|
||||
this.conversationHistoryDeletedRange,
|
||||
this.checkpointTracker,
|
||||
this.updateTaskHistory,
|
||||
)
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
// await this.postStateToWebview()
|
||||
const protoMessage = convertClineMessageToProto(lastMessage)
|
||||
await sendPartialMessageEvent(protoMessage) // more performant than an entire postStateToWebview
|
||||
@@ -974,15 +943,8 @@ export class Task {
|
||||
const lastMessage = this.clineMessages.at(-1)
|
||||
if (lastMessage?.partial && lastMessage.type === type && (lastMessage.ask === askOrSay || lastMessage.say === askOrSay)) {
|
||||
this.clineMessages.pop()
|
||||
await saveClineMessagesAndUpdateHistory(
|
||||
this.getContext(),
|
||||
this.taskId,
|
||||
this.clineMessages,
|
||||
this.taskIsFavorited ?? false,
|
||||
this.conversationHistoryDeletedRange,
|
||||
this.checkpointTracker,
|
||||
this.updateTaskHistory,
|
||||
)
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1155,17 +1117,12 @@ export class Task {
|
||||
|
||||
const wasRecent = lastClineMessage?.ts && Date.now() - lastClineMessage.ts < 30_000
|
||||
|
||||
// Check if there are pending file context warnings before calling taskResumption
|
||||
const pendingContextWarning = await this.fileContextTracker.retrieveAndClearPendingFileContextWarning()
|
||||
const hasPendingFileContextWarnings = pendingContextWarning && pendingContextWarning.length > 0
|
||||
|
||||
const [taskResumptionMessage, userResponseMessage] = formatResponse.taskResumption(
|
||||
this.chatSettings?.mode === "plan" ? "plan" : "act",
|
||||
agoText,
|
||||
cwd,
|
||||
wasRecent,
|
||||
responseText,
|
||||
hasPendingFileContextWarnings,
|
||||
)
|
||||
|
||||
if (taskResumptionMessage !== "") {
|
||||
@@ -1196,15 +1153,6 @@ export class Task {
|
||||
}
|
||||
}
|
||||
|
||||
// Inject file context warning if there were pending warnings from message editing
|
||||
if (pendingContextWarning && pendingContextWarning.length > 0) {
|
||||
const fileContextWarning = formatResponse.fileContextWarning(pendingContextWarning)
|
||||
newUserContent.push({
|
||||
type: "text",
|
||||
text: fileContextWarning,
|
||||
})
|
||||
}
|
||||
|
||||
await this.overwriteApiConversationHistory(modifiedApiConversationHistory)
|
||||
await this.initiateTaskLoop(newUserContent)
|
||||
}
|
||||
@@ -1248,9 +1196,6 @@ export class Task {
|
||||
this.clineIgnoreController.dispose()
|
||||
this.fileContextTracker.dispose()
|
||||
await this.diffViewProvider.revertChanges() // need to await for when we want to make sure directories/files are reverted before re-starting the task from a checkpoint
|
||||
|
||||
// Clear the notification callback when task is aborted
|
||||
this.mcpHub.clearNotificationCallback()
|
||||
}
|
||||
|
||||
// Checkpoints
|
||||
@@ -1274,43 +1219,15 @@ export class Task {
|
||||
return
|
||||
}
|
||||
|
||||
// Initialize checkpoint tracker if it doesn't exist
|
||||
if (!this.checkpointTracker && !this.checkpointTrackerErrorMessage) {
|
||||
try {
|
||||
this.checkpointTracker = await CheckpointTracker.create(
|
||||
this.taskId,
|
||||
this.context.globalStorageUri.fsPath,
|
||||
this.enableCheckpoints,
|
||||
)
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error"
|
||||
console.error("Failed to initialize checkpoint tracker:", errorMessage)
|
||||
this.checkpointTrackerErrorMessage = errorMessage
|
||||
await this.postStateToWebview()
|
||||
return
|
||||
// For non-attempt completion we just say checkpoints
|
||||
await this.say("checkpoint_created")
|
||||
this.checkpointTracker?.commit().then(async (commitHash) => {
|
||||
const lastCheckpointMessage = findLast(this.clineMessages, (m) => m.say === "checkpoint_created")
|
||||
if (lastCheckpointMessage) {
|
||||
lastCheckpointMessage.lastCheckpointHash = commitHash
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
}
|
||||
}
|
||||
|
||||
// Create a checkpoint commit and update clineMessages with a commitHash
|
||||
if (this.checkpointTracker) {
|
||||
const commitHash = await this.checkpointTracker.commit()
|
||||
if (commitHash) {
|
||||
await this.say("checkpoint_created")
|
||||
const lastCheckpointMessage = findLast(this.clineMessages, (m) => m.say === "checkpoint_created")
|
||||
if (lastCheckpointMessage) {
|
||||
lastCheckpointMessage.lastCheckpointHash = commitHash
|
||||
await saveClineMessagesAndUpdateHistory(
|
||||
this.getContext(),
|
||||
this.taskId,
|
||||
this.clineMessages,
|
||||
this.taskIsFavorited ?? false,
|
||||
this.conversationHistoryDeletedRange,
|
||||
this.checkpointTracker,
|
||||
this.updateTaskHistory,
|
||||
)
|
||||
}
|
||||
}
|
||||
} // silently fails for now
|
||||
}) // silently fails for now
|
||||
|
||||
//
|
||||
} else {
|
||||
@@ -1340,15 +1257,7 @@ export class Task {
|
||||
)
|
||||
if (lastCompletionResultMessage) {
|
||||
lastCompletionResultMessage.lastCheckpointHash = commitHash
|
||||
await saveClineMessagesAndUpdateHistory(
|
||||
this.getContext(),
|
||||
this.taskId,
|
||||
this.clineMessages,
|
||||
this.taskIsFavorited ?? false,
|
||||
this.conversationHistoryDeletedRange,
|
||||
this.checkpointTracker,
|
||||
this.updateTaskHistory,
|
||||
)
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
}
|
||||
} else {
|
||||
console.error("Checkpoint tracker does not exist and could not be initialized for attempt completion")
|
||||
@@ -1537,9 +1446,9 @@ export class Task {
|
||||
chunkTimer = setTimeout(async () => await flushBuffer(), CHUNK_DEBOUNCE_MS)
|
||||
}
|
||||
|
||||
const outputLines: string[] = []
|
||||
let result = ""
|
||||
process.on("line", async (line) => {
|
||||
outputLines.push(line)
|
||||
result += line + "\n"
|
||||
|
||||
if (!didContinue) {
|
||||
outputBuffer.push(line)
|
||||
@@ -1581,7 +1490,7 @@ export class Task {
|
||||
// grouping command_output messages despite any gaps anyways)
|
||||
await setTimeoutPromise(50)
|
||||
|
||||
let result = this.terminalManager.processOutput(outputLines)
|
||||
result = result.trim()
|
||||
|
||||
if (userFeedback) {
|
||||
await this.say("user_feedback", userFeedback.text, userFeedback.images, userFeedback.files)
|
||||
@@ -1711,6 +1620,12 @@ export class Task {
|
||||
}
|
||||
}
|
||||
|
||||
private async isClaude4ModelFamily(): Promise<boolean> {
|
||||
const model = this.api.getModel()
|
||||
const modelId = model.id
|
||||
return modelId.includes("sonnet-4") || modelId.includes("opus-4")
|
||||
}
|
||||
|
||||
async *attemptApiRequest(previousApiReqIndex: number): ApiStream {
|
||||
// Wait for MCP servers to be connected before generating system prompt
|
||||
await pWaitFor(() => this.mcpHub.isConnecting !== true, { timeout: 10_000 }).catch(() => {
|
||||
@@ -1724,9 +1639,10 @@ export class Task {
|
||||
|
||||
const supportsBrowserUse = modelSupportsBrowserUse && !disableBrowserTool // only enable browser use if the model supports it and the user hasn't disabled it
|
||||
|
||||
const isClaude4Model = isClaude4ModelFamily(this.api)
|
||||
let systemPrompt = await SYSTEM_PROMPT(cwd, supportsBrowserUse, this.mcpHub, this.browserSettings, isClaude4Model)
|
||||
const isClaude4ModelFamily = await this.isClaude4ModelFamily()
|
||||
let systemPrompt = await SYSTEM_PROMPT(cwd, supportsBrowserUse, this.mcpHub, this.browserSettings, isClaude4ModelFamily)
|
||||
|
||||
let settingsCustomInstructions = this.customInstructions?.trim()
|
||||
await this.migratePreferredLanguageToolSetting()
|
||||
const preferredLanguage = getLanguageKey(this.chatSettings.preferredLanguage as LanguageDisplay)
|
||||
const preferredLanguageInstructions =
|
||||
@@ -1754,6 +1670,7 @@ export class Task {
|
||||
}
|
||||
|
||||
if (
|
||||
settingsCustomInstructions ||
|
||||
globalClineRulesFileInstructions ||
|
||||
localClineRulesFileInstructions ||
|
||||
localCursorRulesFileInstructions ||
|
||||
@@ -1764,6 +1681,7 @@ export class Task {
|
||||
) {
|
||||
// altering the system prompt mid-task will break the prompt cache, but in the grand scheme this will not change often so it's better to not pollute user messages with it the way we have to with <potentially relevant details>
|
||||
const userInstructions = addUserInstructions(
|
||||
settingsCustomInstructions,
|
||||
globalClineRulesFileInstructions,
|
||||
localClineRulesFileInstructions,
|
||||
localCursorRulesFileInstructions,
|
||||
@@ -1785,15 +1703,7 @@ export class Task {
|
||||
|
||||
if (contextManagementMetadata.updatedConversationHistoryDeletedRange) {
|
||||
this.conversationHistoryDeletedRange = contextManagementMetadata.conversationHistoryDeletedRange
|
||||
await saveClineMessagesAndUpdateHistory(
|
||||
this.getContext(),
|
||||
this.taskId,
|
||||
this.clineMessages,
|
||||
this.taskIsFavorited ?? false,
|
||||
this.conversationHistoryDeletedRange,
|
||||
this.checkpointTracker,
|
||||
this.updateTaskHistory,
|
||||
) // saves task history item which we use to keep track of conversation history deleted range
|
||||
await this.saveClineMessagesAndUpdateHistory() // saves task history item which we use to keep track of conversation history deleted range
|
||||
}
|
||||
|
||||
let stream = this.api.createMessage(systemPrompt, contextManagementMetadata.truncatedConversationHistory)
|
||||
@@ -1818,15 +1728,7 @@ export class Task {
|
||||
this.conversationHistoryDeletedRange,
|
||||
"quarter", // Force aggressive truncation
|
||||
)
|
||||
await saveClineMessagesAndUpdateHistory(
|
||||
this.getContext(),
|
||||
this.taskId,
|
||||
this.clineMessages,
|
||||
this.taskIsFavorited ?? false,
|
||||
this.conversationHistoryDeletedRange,
|
||||
this.checkpointTracker,
|
||||
this.updateTaskHistory,
|
||||
)
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
await this.contextManager.triggerApplyStandardContextTruncationNoticeChange(
|
||||
Date.now(),
|
||||
await ensureTaskDirectoryExists(this.getContext(), this.taskId),
|
||||
@@ -1840,15 +1742,7 @@ export class Task {
|
||||
this.conversationHistoryDeletedRange,
|
||||
"quarter", // Force aggressive truncation
|
||||
)
|
||||
await saveClineMessagesAndUpdateHistory(
|
||||
this.getContext(),
|
||||
this.taskId,
|
||||
this.clineMessages,
|
||||
this.taskIsFavorited ?? false,
|
||||
this.conversationHistoryDeletedRange,
|
||||
this.checkpointTracker,
|
||||
this.updateTaskHistory,
|
||||
)
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
await this.contextManager.triggerApplyStandardContextTruncationNoticeChange(
|
||||
Date.now(),
|
||||
await ensureTaskDirectoryExists(this.getContext(), this.taskId),
|
||||
@@ -2027,6 +1921,7 @@ export class Task {
|
||||
|
||||
// Get final list of replacements
|
||||
const allReplacements = this.streamingJsonReplacer.getSuccessfullyParsedItems()
|
||||
// console.log(`Total replacements applied: ${allReplacements.length}`)
|
||||
|
||||
// Cleanup
|
||||
this.streamingJsonReplacer = undefined
|
||||
@@ -2056,6 +1951,7 @@ export class Task {
|
||||
if (this.didCompleteReadingStream) {
|
||||
this.userMessageContentReady = true
|
||||
}
|
||||
// console.log("no more content blocks to stream! this shouldn't happen?")
|
||||
this.presentAssistantMessageLocked = false
|
||||
return
|
||||
//throw new Error("No more content blocks to stream! This shouldn't happen...") // remove and just return after testing
|
||||
@@ -2191,11 +2087,11 @@ export class Task {
|
||||
break
|
||||
}
|
||||
|
||||
const pushToolResult = (content: ToolResponse, isClaude4Model: boolean = false) => {
|
||||
const pushToolResult = (content: ToolResponse, isClaude4ModelFamily: boolean = false) => {
|
||||
if (typeof content === "string") {
|
||||
const resultText = content || "(tool did not return anything)"
|
||||
|
||||
if (isClaude4Model && USE_EXPERIMENTAL_CLAUDE4_FEATURES) {
|
||||
if (isClaude4ModelFamily && USE_EXPERIMENTAL_CLAUDE4_FEATURES) {
|
||||
// Claude 4 family: Use function_results format
|
||||
this.userMessageContent.push({
|
||||
type: "text",
|
||||
@@ -2281,7 +2177,7 @@ export class Task {
|
||||
}
|
||||
}
|
||||
|
||||
const handleError = async (action: string, error: Error, isClaude4Model: boolean = false) => {
|
||||
const handleError = async (action: string, error: Error, isClaude4ModelFamily: boolean = false) => {
|
||||
if (this.abandoned) {
|
||||
console.log("Ignoring error since task was abandoned (i.e. from task cancellation after resetting)")
|
||||
return
|
||||
@@ -2292,7 +2188,7 @@ export class Task {
|
||||
`Error ${action}:\n${error.message ?? JSON.stringify(serializeError(error), null, 2)}`,
|
||||
)
|
||||
|
||||
pushToolResult(formatResponse.toolError(errorString), isClaude4Model)
|
||||
pushToolResult(formatResponse.toolError(errorString), isClaude4ModelFamily)
|
||||
}
|
||||
|
||||
// If block is partial, remove partial closing tag so its not presented to user
|
||||
@@ -2370,9 +2266,9 @@ export class Task {
|
||||
|
||||
const currentFullJson = block.params.diff
|
||||
// Check if we should use streaming (e.g., for specific models)
|
||||
const isClaude4Model = isClaude4ModelFamily(this.api)
|
||||
const isClaude4ModelFamily = await this.isClaude4ModelFamily()
|
||||
// Going through claude family of models
|
||||
if (isClaude4Model && USE_EXPERIMENTAL_CLAUDE4_FEATURES && currentFullJson) {
|
||||
if (isClaude4ModelFamily && USE_EXPERIMENTAL_CLAUDE4_FEATURES && currentFullJson) {
|
||||
console.log("[EDIT] Streaming JSON replacement")
|
||||
const streamingResult = await this.handleStreamingJsonReplacement(
|
||||
block,
|
||||
@@ -2764,7 +2660,7 @@ export class Task {
|
||||
}
|
||||
}
|
||||
case "list_files": {
|
||||
const isClaude4Model = isClaude4ModelFamily(this.api)
|
||||
const isClaude4ModelFamily = await this.isClaude4ModelFamily()
|
||||
const relDirPath: string | undefined = block.params.path
|
||||
const recursiveRaw: string | undefined = block.params.recursive
|
||||
const recursive = recursiveRaw?.toLowerCase() === "true"
|
||||
@@ -2790,7 +2686,10 @@ export class Task {
|
||||
} else {
|
||||
if (!relDirPath) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("list_files", "path"), isClaude4Model)
|
||||
pushToolResult(
|
||||
await this.sayAndCreateMissingParamError("list_files", "path"),
|
||||
isClaude4ModelFamily,
|
||||
)
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
@@ -2841,12 +2740,12 @@ export class Task {
|
||||
true,
|
||||
)
|
||||
}
|
||||
pushToolResult(result, isClaude4Model)
|
||||
pushToolResult(result, isClaude4ModelFamily)
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("listing files", error, isClaude4Model)
|
||||
await handleError("listing files", error, isClaude4ModelFamily)
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
@@ -2934,7 +2833,7 @@ export class Task {
|
||||
}
|
||||
}
|
||||
case "search_files": {
|
||||
const isClaude4Model = isClaude4ModelFamily(this.api)
|
||||
const isClaude4ModelFamily = await this.isClaude4ModelFamily()
|
||||
const relDirPath: string | undefined = block.params.path
|
||||
const regex: string | undefined = block.params.regex
|
||||
const filePattern: string | undefined = block.params.file_pattern
|
||||
@@ -2964,7 +2863,7 @@ export class Task {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(
|
||||
await this.sayAndCreateMissingParamError("search_files", "path"),
|
||||
isClaude4Model,
|
||||
isClaude4ModelFamily,
|
||||
)
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
@@ -2973,7 +2872,7 @@ export class Task {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(
|
||||
await this.sayAndCreateMissingParamError("search_files", "regex"),
|
||||
isClaude4Model,
|
||||
isClaude4ModelFamily,
|
||||
)
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
@@ -3024,12 +2923,12 @@ export class Task {
|
||||
true,
|
||||
)
|
||||
}
|
||||
pushToolResult(results, isClaude4Model)
|
||||
pushToolResult(results, isClaude4ModelFamily)
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("searching files", error, isClaude4Model)
|
||||
await handleError("searching files", error, isClaude4ModelFamily)
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
@@ -3424,21 +3323,8 @@ export class Task {
|
||||
|
||||
// now execute the tool
|
||||
await this.say("mcp_server_request_started") // same as browser_action_result
|
||||
|
||||
// Check for any pending notifications before the tool call
|
||||
const notificationsBefore = this.mcpHub.getPendingNotifications()
|
||||
for (const notification of notificationsBefore) {
|
||||
await this.say("mcp_notification", `[${notification.serverName}] ${notification.message}`)
|
||||
}
|
||||
|
||||
const toolResult = await this.mcpHub.callTool(server_name, tool_name, parsedArguments)
|
||||
|
||||
// Check for any pending notifications after the tool call
|
||||
const notificationsAfter = this.mcpHub.getPendingNotifications()
|
||||
for (const notification of notificationsAfter) {
|
||||
await this.say("mcp_notification", `[${notification.serverName}] ${notification.message}`)
|
||||
}
|
||||
|
||||
// TODO: add progress indicator
|
||||
|
||||
const toolResultImages =
|
||||
@@ -3612,15 +3498,8 @@ export class Task {
|
||||
...sharedMessage,
|
||||
selected: text,
|
||||
} satisfies ClineAskQuestion)
|
||||
await saveClineMessagesAndUpdateHistory(
|
||||
this.getContext(),
|
||||
this.taskId,
|
||||
this.clineMessages,
|
||||
this.taskIsFavorited ?? false,
|
||||
this.conversationHistoryDeletedRange,
|
||||
this.checkpointTracker,
|
||||
this.updateTaskHistory,
|
||||
)
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
telemetryService.captureOptionSelected(this.taskId, options.length, "act")
|
||||
}
|
||||
} else {
|
||||
// Option not selected, send user feedback
|
||||
@@ -3752,15 +3631,7 @@ export class Task {
|
||||
this.conversationHistoryDeletedRange,
|
||||
keepStrategy,
|
||||
)
|
||||
await saveClineMessagesAndUpdateHistory(
|
||||
this.getContext(),
|
||||
this.taskId,
|
||||
this.clineMessages,
|
||||
this.taskIsFavorited ?? false,
|
||||
this.conversationHistoryDeletedRange,
|
||||
this.checkpointTracker,
|
||||
this.updateTaskHistory,
|
||||
)
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
await this.contextManager.triggerApplyStandardContextTruncationNoticeChange(
|
||||
Date.now(),
|
||||
await ensureTaskDirectoryExists(this.getContext(), this.taskId),
|
||||
@@ -4061,15 +3932,8 @@ export class Task {
|
||||
...sharedMessage,
|
||||
selected: text,
|
||||
} satisfies ClinePlanModeResponse)
|
||||
await saveClineMessagesAndUpdateHistory(
|
||||
this.getContext(),
|
||||
this.taskId,
|
||||
this.clineMessages,
|
||||
this.taskIsFavorited ?? false,
|
||||
this.conversationHistoryDeletedRange,
|
||||
this.checkpointTracker,
|
||||
this.updateTaskHistory,
|
||||
)
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
telemetryService.captureOptionSelected(this.taskId, options.length, "plan")
|
||||
}
|
||||
} else {
|
||||
// Option not selected, send user feedback
|
||||
@@ -4171,15 +4035,7 @@ export class Task {
|
||||
) {
|
||||
lastCompletionResultMessage.text += COMPLETION_RESULT_CHANGES_FLAG
|
||||
}
|
||||
await saveClineMessagesAndUpdateHistory(
|
||||
this.getContext(),
|
||||
this.taskId,
|
||||
this.clineMessages,
|
||||
this.taskIsFavorited ?? false,
|
||||
this.conversationHistoryDeletedRange,
|
||||
this.checkpointTracker,
|
||||
this.updateTaskHistory,
|
||||
)
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -4473,8 +4329,8 @@ export class Task {
|
||||
// Now, if it's the first request AND checkpoints are enabled AND tracker was successfully initialized,
|
||||
// then say "checkpoint_created" and perform the commit.
|
||||
if (isFirstRequest && this.enableCheckpoints && this.checkpointTracker) {
|
||||
const commitHash = await this.checkpointTracker.commit() // Actual commit
|
||||
await this.say("checkpoint_created") // Now this is conditional
|
||||
const commitHash = await this.checkpointTracker.commit() // Actual commit
|
||||
const lastCheckpointMessage = findLast(this.clineMessages, (m) => m.say === "checkpoint_created")
|
||||
if (lastCheckpointMessage) {
|
||||
lastCheckpointMessage.lastCheckpointHash = commitHash
|
||||
@@ -4514,15 +4370,7 @@ export class Task {
|
||||
this.clineMessages[lastApiReqIndex].text = JSON.stringify({
|
||||
request: userContent.map((block) => formatContentBlockToMarkdown(block)).join("\n\n"),
|
||||
} satisfies ClineApiReqInfo)
|
||||
await saveClineMessagesAndUpdateHistory(
|
||||
this.getContext(),
|
||||
this.taskId,
|
||||
this.clineMessages,
|
||||
this.taskIsFavorited ?? false,
|
||||
this.conversationHistoryDeletedRange,
|
||||
this.checkpointTracker,
|
||||
this.updateTaskHistory,
|
||||
)
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
await this.postStateToWebview()
|
||||
|
||||
try {
|
||||
@@ -4593,15 +4441,7 @@ export class Task {
|
||||
|
||||
// update api_req_started to have cancelled and cost, so that we can display the cost of the partial stream
|
||||
updateApiReqMsg(cancelReason, streamingFailedMessage)
|
||||
await saveClineMessagesAndUpdateHistory(
|
||||
this.getContext(),
|
||||
this.taskId,
|
||||
this.clineMessages,
|
||||
this.taskIsFavorited ?? false,
|
||||
this.conversationHistoryDeletedRange,
|
||||
this.checkpointTracker,
|
||||
this.updateTaskHistory,
|
||||
)
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
|
||||
telemetryService.captureConversationTurnEvent(
|
||||
this.taskId,
|
||||
@@ -4663,8 +4503,9 @@ export class Task {
|
||||
assistantMessage += chunk.text
|
||||
// parse raw assistant message into content blocks
|
||||
const prevLength = this.assistantMessageContent.length
|
||||
const isClaude4Model = isClaude4ModelFamily(this.api)
|
||||
if (isClaude4Model && USE_EXPERIMENTAL_CLAUDE4_FEATURES) {
|
||||
const isClaude4ModelFamily = await this.isClaude4ModelFamily()
|
||||
|
||||
if (isClaude4ModelFamily && USE_EXPERIMENTAL_CLAUDE4_FEATURES) {
|
||||
this.assistantMessageContent = parseAssistantMessageV3(assistantMessage)
|
||||
} else {
|
||||
this.assistantMessageContent = parseAssistantMessageV2(assistantMessage)
|
||||
@@ -4727,15 +4568,7 @@ export class Task {
|
||||
totalCost = apiStreamUsage.totalCost
|
||||
}
|
||||
updateApiReqMsg()
|
||||
await saveClineMessagesAndUpdateHistory(
|
||||
this.getContext(),
|
||||
this.taskId,
|
||||
this.clineMessages,
|
||||
this.taskIsFavorited ?? false,
|
||||
this.conversationHistoryDeletedRange,
|
||||
this.checkpointTracker,
|
||||
this.updateTaskHistory,
|
||||
)
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
await this.postStateToWebview()
|
||||
})
|
||||
}
|
||||
@@ -4759,15 +4592,7 @@ export class Task {
|
||||
}
|
||||
|
||||
updateApiReqMsg()
|
||||
await saveClineMessagesAndUpdateHistory(
|
||||
this.getContext(),
|
||||
this.taskId,
|
||||
this.clineMessages,
|
||||
this.taskIsFavorited ?? false,
|
||||
this.conversationHistoryDeletedRange,
|
||||
this.checkpointTracker,
|
||||
this.updateTaskHistory,
|
||||
)
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
await this.postStateToWebview()
|
||||
|
||||
// now add to apiconversationhistory
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import { combineApiRequests } from "@/shared/combineApiRequests"
|
||||
import { ensureTaskDirectoryExists, saveClineMessages } from "../storage/disk"
|
||||
import * as vscode from "vscode"
|
||||
import { ClineMessage } from "@/shared/ExtensionMessage"
|
||||
import { getApiMetrics } from "@/shared/getApiMetrics"
|
||||
import { combineCommandSequences } from "@/shared/combineCommandSequences"
|
||||
import { findLastIndex } from "@/shared/array"
|
||||
import getFolderSize from "get-folder-size"
|
||||
import os from "os"
|
||||
import * as path from "path"
|
||||
import CheckpointTracker from "@integrations/checkpoints/CheckpointTracker"
|
||||
import { HistoryItem } from "@/shared/HistoryItem"
|
||||
|
||||
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution
|
||||
|
||||
// need to call getContext() from the task object when passing in context
|
||||
export async function saveClineMessagesAndUpdateHistory(
|
||||
context: vscode.ExtensionContext,
|
||||
taskId: string,
|
||||
clineMessages: ClineMessage[],
|
||||
taskIsFavorited: boolean,
|
||||
conversationHistoryDeletedRange: [number, number] | undefined,
|
||||
checkpointTracker: CheckpointTracker | undefined,
|
||||
updateTaskHistory: (historyItem: HistoryItem) => Promise<HistoryItem[]>,
|
||||
) {
|
||||
try {
|
||||
await saveClineMessages(context, taskId, clineMessages)
|
||||
|
||||
// combined as they are in ChatView
|
||||
const apiMetrics = getApiMetrics(combineApiRequests(combineCommandSequences(clineMessages.slice(1))))
|
||||
const taskMessage = clineMessages[0] // first message is always the task say
|
||||
const lastRelevantMessage =
|
||||
clineMessages[
|
||||
findLastIndex(
|
||||
clineMessages,
|
||||
(message) => !(message.ask === "resume_task" || message.ask === "resume_completed_task"),
|
||||
)
|
||||
]
|
||||
const taskDir = await ensureTaskDirectoryExists(context, taskId)
|
||||
let taskDirSize = 0
|
||||
try {
|
||||
// getFolderSize.loose silently ignores errors
|
||||
// returns # of bytes, size/1000/1000 = MB
|
||||
taskDirSize = await getFolderSize.loose(taskDir)
|
||||
} catch (error) {
|
||||
console.error("Failed to get task directory size:", taskDir, error)
|
||||
}
|
||||
await updateTaskHistory({
|
||||
id: taskId,
|
||||
ts: lastRelevantMessage.ts,
|
||||
task: taskMessage.text ?? "",
|
||||
tokensIn: apiMetrics.totalTokensIn,
|
||||
tokensOut: apiMetrics.totalTokensOut,
|
||||
cacheWrites: apiMetrics.totalCacheWrites,
|
||||
cacheReads: apiMetrics.totalCacheReads,
|
||||
totalCost: apiMetrics.totalCost,
|
||||
size: taskDirSize,
|
||||
shadowGitConfigWorkTree: await checkpointTracker?.getShadowGitConfigWorkTree(),
|
||||
cwdOnTaskInitialization: cwd,
|
||||
conversationHistoryDeletedRange: conversationHistoryDeletedRange,
|
||||
isFavorited: taskIsFavorited,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to save cline messages:", error)
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,13 @@ The Cline extension exposes an API that can be used by other extensions. To use
|
||||
if (cline) {
|
||||
// Now you can use the API
|
||||
|
||||
// Set custom instructions
|
||||
await cline.setCustomInstructions("Talk like a pirate")
|
||||
|
||||
// Get custom instructions
|
||||
const instructions = await cline.getCustomInstructions()
|
||||
console.log("Current custom instructions:", instructions)
|
||||
|
||||
// Start a new task with an initial message
|
||||
await cline.startNewTask("Hello, Cline! Let's make a new project...")
|
||||
|
||||
|
||||
@@ -65,6 +65,59 @@ describe("ClineAPI Core Functionality", () => {
|
||||
sandbox.restore()
|
||||
})
|
||||
|
||||
describe("setCustomInstructions", () => {
|
||||
it("should update custom instructions in controller", async () => {
|
||||
const testInstructions = "Test custom instructions"
|
||||
|
||||
await api.setCustomInstructions(testInstructions)
|
||||
|
||||
// Verify controller method was called
|
||||
sinon.assert.calledOnce(mockController.updateCustomInstructions)
|
||||
sinon.assert.calledWith(mockController.updateCustomInstructions, testInstructions)
|
||||
|
||||
// Verify output channel was updated
|
||||
sinon.assert.calledWith(mockOutputChannel.appendLine, "Custom instructions set")
|
||||
})
|
||||
|
||||
it("should handle empty instructions", async () => {
|
||||
await api.setCustomInstructions("")
|
||||
|
||||
sinon.assert.calledWith(mockController.updateCustomInstructions, "")
|
||||
sinon.assert.calledWith(mockOutputChannel.appendLine, "Custom instructions set")
|
||||
})
|
||||
|
||||
it("should handle very long instructions", async () => {
|
||||
const longInstructions = "a".repeat(10000)
|
||||
|
||||
await api.setCustomInstructions(longInstructions)
|
||||
|
||||
sinon.assert.calledWith(mockController.updateCustomInstructions, longInstructions)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getCustomInstructions", () => {
|
||||
it("should retrieve custom instructions from state", async () => {
|
||||
const testInstructions = "Retrieved instructions"
|
||||
// The real implementation uses getGlobalState from the state module
|
||||
getGlobalStateStub.resolves(testInstructions)
|
||||
|
||||
const result = await api.getCustomInstructions()
|
||||
|
||||
result!.should.equal(testInstructions)
|
||||
sinon.assert.calledWith(getGlobalStateStub, mockController.context, "customInstructions")
|
||||
})
|
||||
|
||||
it("should return undefined when no instructions set", async () => {
|
||||
// The real implementation uses getGlobalState from the state module
|
||||
getGlobalStateStub.resolves(undefined)
|
||||
|
||||
const result = await api.getCustomInstructions()
|
||||
|
||||
should.not.exist(result)
|
||||
sinon.assert.calledWith(getGlobalStateStub, mockController.context, "customInstructions")
|
||||
})
|
||||
})
|
||||
|
||||
describe("startNewTask", () => {
|
||||
it("should clear existing task and start new one with description", async () => {
|
||||
const taskDescription = "Create a test function"
|
||||
@@ -207,6 +260,17 @@ describe("ClineAPI Core Functionality", () => {
|
||||
})
|
||||
|
||||
describe("Error Handling", () => {
|
||||
it("should handle errors in setCustomInstructions", async () => {
|
||||
mockController.updateCustomInstructions.rejects(new Error("Update failed"))
|
||||
|
||||
try {
|
||||
await api.setCustomInstructions("test")
|
||||
should.fail("", "", "Should have thrown an error", "")
|
||||
} catch (error: any) {
|
||||
error.message.should.equal("Update failed")
|
||||
}
|
||||
})
|
||||
|
||||
it("should handle errors in task initialization", async () => {
|
||||
mockController.initTask.rejects(new Error("Init failed"))
|
||||
|
||||
|
||||
Vendored
+12
@@ -1,4 +1,16 @@
|
||||
export interface ClineAPI {
|
||||
/**
|
||||
* Sets the custom instructions in the global storage.
|
||||
* @param value The custom instructions to be saved.
|
||||
*/
|
||||
setCustomInstructions(value: string): Promise<void>
|
||||
|
||||
/**
|
||||
* Retrieves the custom instructions from the global storage.
|
||||
* @returns The saved custom instructions, or undefined if not set.
|
||||
*/
|
||||
getCustomInstructions(): Promise<string | undefined>
|
||||
|
||||
/**
|
||||
* Starts a new task with an optional initial message and images.
|
||||
* @param task Optional initial task message.
|
||||
|
||||
@@ -7,6 +7,15 @@ import { WebviewProviderType as WebviewProviderTypeEnum } from "@shared/proto/ui
|
||||
|
||||
export function createClineAPI(outputChannel: vscode.OutputChannel, sidebarController: Controller): ClineAPI {
|
||||
const api: ClineAPI = {
|
||||
setCustomInstructions: async (value: string) => {
|
||||
await sidebarController.updateCustomInstructions(value)
|
||||
outputChannel.appendLine("Custom instructions set")
|
||||
},
|
||||
|
||||
getCustomInstructions: async () => {
|
||||
return (await getGlobalState(sidebarController.context, "customInstructions")) as string | undefined
|
||||
},
|
||||
|
||||
startNewTask: async (task?: string, images?: string[]) => {
|
||||
outputChannel.appendLine("Starting new task")
|
||||
await sidebarController.clearTask()
|
||||
|
||||
+1
-8
@@ -22,10 +22,9 @@ import { WebviewProviderType as WebviewProviderTypeEnum } from "@shared/proto/ui
|
||||
import { WebviewProviderType } from "./shared/webview/types"
|
||||
import { sendHistoryButtonClickedEvent } from "./core/controller/ui/subscribeToHistoryButtonClicked"
|
||||
import { sendAccountButtonClickedEvent } from "./core/controller/ui/subscribeToAccountButtonClicked"
|
||||
import { migratePlanActGlobalToWorkspaceStorage, migrateCustomInstructionsToGlobalRules } from "./core/storage/state"
|
||||
import { migratePlanActGlobalToWorkspaceStorage } from "./core/storage/state"
|
||||
|
||||
import { sendFocusChatInputEvent } from "./core/controller/ui/subscribeToFocusChatInput"
|
||||
import { FileContextTracker } from "./core/context/context-tracking/FileContextTracker"
|
||||
/*
|
||||
Built using https://github.com/microsoft/vscode-webview-ui-toolkit
|
||||
|
||||
@@ -50,12 +49,6 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
// Migrate global storage values to workspace storage (one-time cleanup)
|
||||
await migratePlanActGlobalToWorkspaceStorage(context)
|
||||
|
||||
// Migrate custom instructions to global Cline rules (one-time cleanup)
|
||||
await migrateCustomInstructionsToGlobalRules(context)
|
||||
|
||||
// Clean up orphaned file context warnings (startup cleanup)
|
||||
await FileContextTracker.cleanupOrphanedWarnings(context)
|
||||
|
||||
// Version checking for autoupdate notification
|
||||
const currentVersion = context.extension.packageJSON.version
|
||||
const previousVersion = context.globalState.get<string>("clineVersion")
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { StringRequest } from "@/shared/proto/common"
|
||||
import { Uri } from "@/shared/proto/host/uri"
|
||||
import { FileChangeEvent_ChangeType, SubscribeToFileRequest } from "@/shared/proto/host/watch"
|
||||
|
||||
const UriServiceClient = {
|
||||
parse: function (_: StringRequest): Uri {
|
||||
throw Error("Unimplemented")
|
||||
},
|
||||
}
|
||||
const WatchServiceClient = {
|
||||
subscribeToFile: function (
|
||||
_r: SubscribeToFileRequest,
|
||||
_h: {
|
||||
onResponse?: (response: { type: FileChangeEvent_ChangeType }) => void | Promise<void>
|
||||
onError?: (error: any) => void
|
||||
onComplete?: () => void
|
||||
},
|
||||
) {
|
||||
throw Error("Unimplemented")
|
||||
},
|
||||
}
|
||||
|
||||
export { UriServiceClient, WatchServiceClient }
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as VscodeClient from "./vscode/client/host-grpc-client"
|
||||
import * as ExternalClient from "@/standalone/host-bridge-client-manager"
|
||||
import * as ExternalClient from "./external/client/host-bridge-client"
|
||||
|
||||
const isHostBridgeExternal = process.env.HOST_BRIDGE_ADDRESS !== undefined && process.env.HOST_BRIDGE_ADDRESS !== "vscode"
|
||||
const Client = isHostBridgeExternal ? ExternalClient : VscodeClient
|
||||
|
||||
|
||||
@@ -33,11 +33,10 @@ export function createGrpcClient<T extends ProtoService>(service: T): GrpcClient
|
||||
const grpcHandler = new GrpcHandler()
|
||||
|
||||
Object.values(service.methods).forEach((method) => {
|
||||
// Use lowercase method name as the key in the client object
|
||||
const methodKey = method.name.charAt(0).toLowerCase() + method.name.slice(1)
|
||||
|
||||
// Streaming method implementation
|
||||
if (method.responseStream) {
|
||||
// Use lowercase method name as the key in the client object
|
||||
const methodKey = method.name.charAt(0).toLowerCase() + method.name.slice(1)
|
||||
client[methodKey as keyof GrpcClientType<T>] = ((
|
||||
request: any,
|
||||
options: StreamingCallbacks<InstanceType<typeof method.responseType>>,
|
||||
@@ -76,6 +75,7 @@ export function createGrpcClient<T extends ProtoService>(service: T): GrpcClient
|
||||
}) as any
|
||||
} else {
|
||||
// Unary method implementation
|
||||
const methodKey = method.name.charAt(0).toLowerCase() + method.name.slice(1)
|
||||
client[methodKey as keyof GrpcClientType<T>] = ((request: any) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const requestId = uuidv4()
|
||||
@@ -84,12 +84,15 @@ export function createGrpcClient<T extends ProtoService>(service: T): GrpcClient
|
||||
const response = await grpcHandler.handleRequest(service.fullName, methodKey, request, requestId)
|
||||
console.log(`[DEBUG] gRPC host resp to ${service.fullName}.${methodKey} req:${requestId}`)
|
||||
|
||||
// Check if the response is a function (streaming)
|
||||
// Check if the response is a function (streaming) or an object (unary)
|
||||
if (typeof response === "function") {
|
||||
// This shouldn't happen for unary requests
|
||||
throw new Error("Received streaming response for unary request")
|
||||
} else if (response && response.message) {
|
||||
resolve(response.message)
|
||||
} else {
|
||||
throw new Error("gRPC response didn't have a message")
|
||||
}
|
||||
resolve(response)
|
||||
} catch (e) {
|
||||
console.log(`[DEBUG] gRPC host ERR to ${service.fullName}.${methodKey} req:${requestId} err:${e}`)
|
||||
reject(e)
|
||||
@@ -98,5 +101,6 @@ export function createGrpcClient<T extends ProtoService>(service: T): GrpcClient
|
||||
}) as any
|
||||
}
|
||||
})
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { HostServiceHandlerConfig, hostServiceHandlers } from "./host-grpc-service-config"
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
import { hostServiceHandlers } from "./host-grpc-service-config"
|
||||
import { GrpcRequestRegistry } from "@core/controller/grpc-request-registry"
|
||||
|
||||
/**
|
||||
@@ -19,13 +20,13 @@ export interface StreamingCallbacks<T = any> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles gRPC requests for the host bridge.
|
||||
* Handles gRPC requests from the webview
|
||||
*/
|
||||
export class GrpcHandler {
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* Handle a gRPC request for the host bridge.
|
||||
* Handle a gRPC request from the webview
|
||||
* @param service The service name
|
||||
* @param method The method name
|
||||
* @param message The request message
|
||||
@@ -36,71 +37,90 @@ export class GrpcHandler {
|
||||
async handleRequest<T = any>(
|
||||
service: string,
|
||||
method: string,
|
||||
request: any,
|
||||
message: any,
|
||||
requestId: string,
|
||||
streamingCallbacks?: StreamingCallbacks<T>,
|
||||
): Promise<any | (() => void)> {
|
||||
if (!streamingCallbacks) {
|
||||
return this.handleUnaryRequest(service, method, request)
|
||||
}
|
||||
|
||||
): Promise<
|
||||
| {
|
||||
message?: any
|
||||
error?: string
|
||||
request_id: string
|
||||
}
|
||||
| (() => void)
|
||||
> {
|
||||
// If streaming callbacks are provided, handle as a streaming request
|
||||
let completionCalled = false
|
||||
if (streamingCallbacks) {
|
||||
let completionCalled = false
|
||||
|
||||
// Create a response handler that will call the client's callbacks
|
||||
const responseHandler: StreamingResponseHandler = async (response, isLast = false, sequenceNumber) => {
|
||||
try {
|
||||
// Call the client's onResponse callback with the response
|
||||
streamingCallbacks.onResponse(response)
|
||||
// Create a response handler that will call the client's callbacks
|
||||
const responseHandler: StreamingResponseHandler = async (response, isLast = false, sequenceNumber) => {
|
||||
try {
|
||||
// Call the client's onResponse callback with the response
|
||||
streamingCallbacks.onResponse(response)
|
||||
|
||||
// If this is the last response, call the onComplete callback
|
||||
if (isLast && streamingCallbacks.onComplete && !completionCalled) {
|
||||
completionCalled = true
|
||||
streamingCallbacks.onComplete()
|
||||
// If this is the last response, call the onComplete callback
|
||||
if (isLast && streamingCallbacks.onComplete && !completionCalled) {
|
||||
completionCalled = true
|
||||
streamingCallbacks.onComplete()
|
||||
}
|
||||
} catch (error) {
|
||||
// If there's an error in the callback, call the onError callback
|
||||
if (streamingCallbacks.onError) {
|
||||
streamingCallbacks.onError(error instanceof Error ? error : new Error(String(error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register the response handler with the registry
|
||||
requestRegistry.registerRequest(
|
||||
requestId,
|
||||
() => {
|
||||
console.log(`[DEBUG] Cleaning up streaming request: ${requestId}`)
|
||||
if (streamingCallbacks.onComplete && !completionCalled) {
|
||||
completionCalled = true
|
||||
streamingCallbacks.onComplete()
|
||||
}
|
||||
},
|
||||
{ type: "streaming_request", service, method },
|
||||
responseHandler,
|
||||
)
|
||||
|
||||
// Call the streaming handler directly
|
||||
console.log(`[DEBUG] Streaming gRPC host call to ${service}.${method} req:${requestId}`)
|
||||
try {
|
||||
await this.handleStreamingRequest(service, method, message, requestId)
|
||||
} catch (error) {
|
||||
// If there's an error in the callback, call the onError callback
|
||||
if (streamingCallbacks.onError) {
|
||||
streamingCallbacks.onError(error instanceof Error ? error : new Error(String(error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register the response handler with the registry
|
||||
requestRegistry.registerRequest(
|
||||
requestId,
|
||||
() => {
|
||||
console.log(`[DEBUG] Cleaning up streaming request: ${requestId}`)
|
||||
if (streamingCallbacks.onComplete && !completionCalled) {
|
||||
completionCalled = true
|
||||
streamingCallbacks.onComplete()
|
||||
}
|
||||
},
|
||||
{ type: "streaming_request", service, method },
|
||||
responseHandler,
|
||||
)
|
||||
|
||||
// Call the streaming handler directly
|
||||
console.log(`[DEBUG] Streaming gRPC host call to ${service}.${method} req:${requestId}`)
|
||||
try {
|
||||
await this.handleStreamingRequest(service, method, request, requestId)
|
||||
} catch (error) {
|
||||
if (streamingCallbacks.onError) {
|
||||
streamingCallbacks.onError(error instanceof Error ? error : new Error(String(error)))
|
||||
// Return a function to cancel the stream
|
||||
return () => {
|
||||
console.log(`[DEBUG] Cancelling streaming request: ${requestId}`)
|
||||
this.cancelRequest(requestId)
|
||||
}
|
||||
}
|
||||
|
||||
// Return a function to cancel the stream
|
||||
return () => {
|
||||
console.log(`[DEBUG] Cancelling streaming request: ${requestId}`)
|
||||
this.cancelRequest(requestId)
|
||||
}
|
||||
}
|
||||
// Handle as a unary request
|
||||
try {
|
||||
// Get the service handler from the config
|
||||
const serviceConfig = hostServiceHandlers[service]
|
||||
if (!serviceConfig) {
|
||||
throw new Error(`Unknown service: ${service}`)
|
||||
}
|
||||
|
||||
private async handleUnaryRequest(service: string, method: string, request: any): Promise<any> {
|
||||
const serviceConfig = this.getServiceHandlerConfig(service)
|
||||
const response = await serviceConfig.requestHandler(method, request)
|
||||
return response
|
||||
// Handle unary request
|
||||
return {
|
||||
message: await serviceConfig.requestHandler(method, message),
|
||||
request_id: requestId,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
request_id: requestId,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -109,25 +129,27 @@ export class GrpcHandler {
|
||||
* @returns True if the request was found and cancelled, false otherwise
|
||||
*/
|
||||
public async cancelRequest(requestId: string): Promise<boolean> {
|
||||
const requestInfo = requestRegistry.getRequestInfo(requestId)
|
||||
if (!requestInfo) {
|
||||
return false
|
||||
const cancelled = requestRegistry.cancelRequest(requestId)
|
||||
|
||||
if (cancelled) {
|
||||
// Get the registered response handler from the registry
|
||||
const requestInfo = requestRegistry.getRequestInfo(requestId)
|
||||
if (requestInfo && requestInfo.responseStream) {
|
||||
try {
|
||||
// Send cancellation confirmation using the registered response handler
|
||||
await requestInfo.responseStream(
|
||||
{ cancelled: true },
|
||||
true, // Mark as last message
|
||||
)
|
||||
} catch (e) {
|
||||
console.error(`Error sending cancellation response for ${requestId}:`, e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(`[DEBUG] Request not found for cancellation: ${requestId}`)
|
||||
}
|
||||
|
||||
const cancelled = requestRegistry.cancelRequest(requestId)
|
||||
if (!cancelled) {
|
||||
console.log(`[DEBUG] Request not found for cancellation: ${requestId}`)
|
||||
return false
|
||||
}
|
||||
if (requestInfo.responseStream) {
|
||||
try {
|
||||
// Send cancellation confirmation using the registered response handler
|
||||
await requestInfo.responseStream({ cancelled: true }, true /* isLast */)
|
||||
} catch (e) {
|
||||
console.error(`Error sending cancellation response for ${requestId}:`, e)
|
||||
}
|
||||
}
|
||||
return true
|
||||
return cancelled
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,7 +160,11 @@ export class GrpcHandler {
|
||||
* @param requestId The request ID for response correlation
|
||||
*/
|
||||
private async handleStreamingRequest(service: string, method: string, message: any, requestId: string): Promise<void> {
|
||||
const serviceConfig = this.getServiceHandlerConfig(service)
|
||||
// Get the service handler from the config
|
||||
const serviceConfig = hostServiceHandlers[service]
|
||||
if (!serviceConfig) {
|
||||
throw new Error(`Unknown service: ${service}`)
|
||||
}
|
||||
|
||||
// Check if the service supports streaming
|
||||
if (!serviceConfig.streamingHandler) {
|
||||
@@ -160,13 +186,6 @@ export class GrpcHandler {
|
||||
// Don't send a final message here - the stream should stay open for future updates
|
||||
// The stream will be closed when the client disconnects or when the service explicitly ends it
|
||||
}
|
||||
|
||||
private getServiceHandlerConfig(serviceName: string): HostServiceHandlerConfig {
|
||||
if (!(serviceName in hostServiceHandlers)) {
|
||||
throw new Error(`Unknown service: ${serviceName}`)
|
||||
}
|
||||
return hostServiceHandlers[serviceName]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -83,7 +83,6 @@ function getBuildArtifactPatterns(): string[] {
|
||||
".sass-cache/",
|
||||
".vs/",
|
||||
".vscode/",
|
||||
".clinerules/",
|
||||
"Pods/",
|
||||
"__pycache__/",
|
||||
"bin/",
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import * as vscode from "vscode"
|
||||
import Anthropic from "@anthropic-ai/sdk"
|
||||
import { execa } from "execa"
|
||||
|
||||
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
|
||||
|
||||
export function runClaudeCode({
|
||||
systemPrompt,
|
||||
messages,
|
||||
path,
|
||||
modelId,
|
||||
}: {
|
||||
systemPrompt: string
|
||||
messages: Anthropic.Messages.MessageParam[]
|
||||
path?: string
|
||||
modelId?: string
|
||||
}) {
|
||||
const claudePath = path || "claude"
|
||||
|
||||
// TODO: Is it worh using sessions? Where do we store the session ID?
|
||||
const args = [
|
||||
"-p",
|
||||
JSON.stringify(messages),
|
||||
"--system-prompt",
|
||||
systemPrompt,
|
||||
"--verbose",
|
||||
"--output-format",
|
||||
"stream-json",
|
||||
// Cline will handle recursive calls
|
||||
"--max-turns",
|
||||
"1",
|
||||
]
|
||||
|
||||
if (modelId) {
|
||||
args.push("--model", modelId)
|
||||
}
|
||||
|
||||
return execa(claudePath, args, {
|
||||
stdin: "ignore",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
env: process.env,
|
||||
cwd,
|
||||
})
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
type InitMessage = {
|
||||
type: "system"
|
||||
subtype: "init"
|
||||
session_id: string
|
||||
tools: string[]
|
||||
mcp_servers: string[]
|
||||
}
|
||||
|
||||
type ClaudeCodeContent = {
|
||||
type: "text"
|
||||
text: string
|
||||
}
|
||||
|
||||
type AssistantMessage = {
|
||||
type: "assistant"
|
||||
message: {
|
||||
id: string
|
||||
type: "message"
|
||||
role: "assistant"
|
||||
model: string
|
||||
content: ClaudeCodeContent[]
|
||||
stop_reason: null
|
||||
stop_sequence: null
|
||||
usage: {
|
||||
input_tokens: number
|
||||
cache_creation_input_tokens?: number
|
||||
cache_read_input_tokens?: number
|
||||
output_tokens: number
|
||||
service_tier: "standard"
|
||||
}
|
||||
}
|
||||
session_id: string
|
||||
}
|
||||
|
||||
type ErrorMessage = {
|
||||
type: "error"
|
||||
}
|
||||
|
||||
type ResultMessage = {
|
||||
type: "result"
|
||||
subtype: "success"
|
||||
cost_usd: number
|
||||
is_error: boolean
|
||||
duration_ms: number
|
||||
duration_api_ms: number
|
||||
num_turns: number
|
||||
result: string
|
||||
total_cost: number
|
||||
session_id: string
|
||||
}
|
||||
|
||||
export type ClaudeCodeMessage = InitMessage | AssistantMessage | ErrorMessage | ResultMessage
|
||||
@@ -1,7 +1,6 @@
|
||||
import pWaitFor from "p-wait-for"
|
||||
import * as vscode from "vscode"
|
||||
import { arePathsEqual } from "@utils/path"
|
||||
import { getShellForProfile } from "@utils/shell"
|
||||
import { mergePromise, TerminalProcess, TerminalProcessResultPromise } from "./TerminalProcess"
|
||||
import { TerminalInfo, TerminalRegistry } from "./TerminalRegistry"
|
||||
|
||||
@@ -96,8 +95,6 @@ export class TerminalManager {
|
||||
private disposables: vscode.Disposable[] = []
|
||||
private shellIntegrationTimeout: number = 4000
|
||||
private terminalReuseEnabled: boolean = true
|
||||
private terminalOutputLineLimit: number = 500
|
||||
private defaultTerminalProfile: string = "default"
|
||||
|
||||
constructor() {
|
||||
let disposable: vscode.Disposable | undefined
|
||||
@@ -156,16 +153,12 @@ export class TerminalManager {
|
||||
}
|
||||
|
||||
runCommand(terminalInfo: TerminalInfo, command: string): TerminalProcessResultPromise {
|
||||
console.log(`[TerminalManager] Running command on terminal ${terminalInfo.id}: "${command}"`)
|
||||
console.log(`[TerminalManager] Terminal ${terminalInfo.id} busy state before: ${terminalInfo.busy}`)
|
||||
|
||||
terminalInfo.busy = true
|
||||
terminalInfo.lastCommand = command
|
||||
const process = new TerminalProcess()
|
||||
this.processes.set(terminalInfo.id, process)
|
||||
|
||||
process.once("completed", () => {
|
||||
console.log(`[TerminalManager] Terminal ${terminalInfo.id} completed, setting busy to false`)
|
||||
terminalInfo.busy = false
|
||||
})
|
||||
|
||||
@@ -225,40 +218,26 @@ export class TerminalManager {
|
||||
|
||||
async getOrCreateTerminal(cwd: string): Promise<TerminalInfo> {
|
||||
const terminals = TerminalRegistry.getAllTerminals()
|
||||
const expectedShellPath =
|
||||
this.defaultTerminalProfile !== "default" ? getShellForProfile(this.defaultTerminalProfile) : undefined
|
||||
|
||||
// Find available terminal from our pool first (created for this task)
|
||||
console.log(`[TerminalManager] Looking for terminal in cwd: ${cwd}`)
|
||||
console.log(`[TerminalManager] Available terminals: ${terminals.length}`)
|
||||
|
||||
const matchingTerminal = terminals.find((t) => {
|
||||
if (t.busy) {
|
||||
console.log(`[TerminalManager] Terminal ${t.id} is busy, skipping`)
|
||||
return false
|
||||
}
|
||||
// Check if shell path matches current configuration
|
||||
if (t.shellPath !== expectedShellPath) {
|
||||
return false
|
||||
}
|
||||
const terminalCwd = t.terminal.shellIntegration?.cwd // one of cline's commands could have changed the cwd of the terminal
|
||||
if (!terminalCwd) {
|
||||
console.log(`[TerminalManager] Terminal ${t.id} has no cwd, skipping`)
|
||||
return false
|
||||
}
|
||||
const matches = arePathsEqual(vscode.Uri.file(cwd).fsPath, terminalCwd.fsPath)
|
||||
console.log(`[TerminalManager] Terminal ${t.id} cwd: ${terminalCwd.fsPath}, matches: ${matches}`)
|
||||
return matches
|
||||
return arePathsEqual(vscode.Uri.file(cwd).fsPath, terminalCwd.fsPath)
|
||||
})
|
||||
if (matchingTerminal) {
|
||||
console.log(`[TerminalManager] Found matching terminal ${matchingTerminal.id} in correct cwd`)
|
||||
this.terminalIds.add(matchingTerminal.id)
|
||||
return matchingTerminal
|
||||
}
|
||||
|
||||
// If no non-busy terminal in the current working dir exists and terminal reuse is enabled, try to find any non-busy terminal regardless of CWD
|
||||
if (this.terminalReuseEnabled) {
|
||||
const availableTerminal = terminals.find((t) => !t.busy && t.shellPath === expectedShellPath)
|
||||
const availableTerminal = terminals.find((t) => !t.busy)
|
||||
if (availableTerminal) {
|
||||
// Set up promise and tracking for CWD change
|
||||
const cwdPromise = new Promise<void>((resolve, reject) => {
|
||||
@@ -267,13 +246,7 @@ export class TerminalManager {
|
||||
})
|
||||
|
||||
// Navigate back to the desired directory
|
||||
const cdProcess = this.runCommand(availableTerminal, `cd "${cwd}"`)
|
||||
|
||||
// Wait for the cd command to complete before proceeding
|
||||
await cdProcess
|
||||
|
||||
// Add a small delay to ensure terminal is ready after cd
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
await this.runCommand(availableTerminal, `cd "${cwd}"`)
|
||||
|
||||
// Either resolve immediately if CWD already updated or wait for event/timeout
|
||||
if (this.isCwdMatchingExpected(availableTerminal)) {
|
||||
@@ -302,8 +275,8 @@ export class TerminalManager {
|
||||
}
|
||||
}
|
||||
|
||||
// If all terminals are busy or don't match shell profile, create a new one with the configured shell
|
||||
const newTerminalInfo = TerminalRegistry.createTerminal(cwd, expectedShellPath)
|
||||
// If all terminals are busy, create a new one
|
||||
const newTerminalInfo = TerminalRegistry.createTerminal(cwd)
|
||||
this.terminalIds.add(newTerminalInfo.id)
|
||||
return newTerminalInfo
|
||||
}
|
||||
@@ -345,117 +318,4 @@ export class TerminalManager {
|
||||
setTerminalReuseEnabled(enabled: boolean): void {
|
||||
this.terminalReuseEnabled = enabled
|
||||
}
|
||||
|
||||
setTerminalOutputLineLimit(limit: number): void {
|
||||
this.terminalOutputLineLimit = limit
|
||||
}
|
||||
|
||||
public processOutput(outputLines: string[]): string {
|
||||
if (outputLines.length > this.terminalOutputLineLimit) {
|
||||
const halfLimit = Math.floor(this.terminalOutputLineLimit / 2)
|
||||
const start = outputLines.slice(0, halfLimit)
|
||||
const end = outputLines.slice(outputLines.length - halfLimit)
|
||||
return `${start.join("\n")}\n... (output truncated) ...\n${end.join("\n")}`.trim()
|
||||
}
|
||||
return outputLines.join("\n").trim()
|
||||
}
|
||||
|
||||
setDefaultTerminalProfile(profileId: string): { closedCount: number; busyTerminals: TerminalInfo[] } {
|
||||
// Only handle terminal change if profile actually changed
|
||||
if (this.defaultTerminalProfile === profileId) {
|
||||
return { closedCount: 0, busyTerminals: [] }
|
||||
}
|
||||
|
||||
const oldProfileId = this.defaultTerminalProfile
|
||||
this.defaultTerminalProfile = profileId
|
||||
|
||||
// Get the shell path for the new profile
|
||||
const newShellPath = profileId !== "default" ? getShellForProfile(profileId) : undefined
|
||||
|
||||
// Handle terminal management for the profile change
|
||||
const result = this.handleTerminalProfileChange(newShellPath)
|
||||
|
||||
// Update lastActive for any remaining terminals
|
||||
const allTerminals = TerminalRegistry.getAllTerminals()
|
||||
allTerminals.forEach((terminal) => {
|
||||
if (terminal.shellPath !== newShellPath) {
|
||||
TerminalRegistry.updateTerminal(terminal.id, { lastActive: Date.now() })
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters terminals based on a provided criteria function
|
||||
* @param filterFn Function that accepts TerminalInfo and returns boolean
|
||||
* @returns Array of terminals that match the criteria
|
||||
*/
|
||||
filterTerminals(filterFn: (terminal: TerminalInfo) => boolean): TerminalInfo[] {
|
||||
const terminals = TerminalRegistry.getAllTerminals()
|
||||
return terminals.filter(filterFn)
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes terminals that match the provided criteria
|
||||
* @param filterFn Function that accepts TerminalInfo and returns boolean for terminals to close
|
||||
* @param force If true, closes even busy terminals (with warning)
|
||||
* @returns Number of terminals closed
|
||||
*/
|
||||
closeTerminals(filterFn: (terminal: TerminalInfo) => boolean, force: boolean = false): number {
|
||||
const terminalsToClose = this.filterTerminals(filterFn)
|
||||
let closedCount = 0
|
||||
|
||||
for (const terminalInfo of terminalsToClose) {
|
||||
// Skip busy terminals unless force is true
|
||||
if (terminalInfo.busy && !force) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Remove from our tracking
|
||||
if (this.terminalIds.has(terminalInfo.id)) {
|
||||
this.terminalIds.delete(terminalInfo.id)
|
||||
}
|
||||
this.processes.delete(terminalInfo.id)
|
||||
|
||||
// Dispose the actual terminal
|
||||
terminalInfo.terminal.dispose()
|
||||
|
||||
// Remove from registry
|
||||
TerminalRegistry.removeTerminal(terminalInfo.id)
|
||||
|
||||
closedCount++
|
||||
}
|
||||
|
||||
return closedCount
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles terminal management when the terminal profile changes
|
||||
* @param newShellPath New shell path to use
|
||||
* @returns Object with information about closed terminals and remaining busy terminals
|
||||
*/
|
||||
handleTerminalProfileChange(newShellPath: string | undefined): {
|
||||
closedCount: number
|
||||
busyTerminals: TerminalInfo[]
|
||||
} {
|
||||
// Close non-busy terminals with different shell path
|
||||
const closedCount = this.closeTerminals((terminal) => !terminal.busy && terminal.shellPath !== newShellPath, false)
|
||||
|
||||
// Get remaining busy terminals with different shell path
|
||||
const busyTerminals = this.filterTerminals((terminal) => terminal.busy && terminal.shellPath !== newShellPath)
|
||||
|
||||
return {
|
||||
closedCount,
|
||||
busyTerminals,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces closure of all terminals (including busy ones)
|
||||
* @returns Number of terminals closed
|
||||
*/
|
||||
closeAllTerminals(): number {
|
||||
return this.closeTerminals(() => true, true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,84 +23,20 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
|
||||
private lastRetrievedIndex: number = 0
|
||||
isHot: boolean = false
|
||||
private hotTimer: NodeJS.Timeout | null = null
|
||||
private command: string = ""
|
||||
private gracePeriodTimer: NodeJS.Timeout | null = null
|
||||
private hasEmittedCompleted: boolean = false
|
||||
|
||||
// constructor() {
|
||||
// super()
|
||||
|
||||
async run(terminal: vscode.Terminal, command: string) {
|
||||
// Clear any existing grace period timer from previous commands
|
||||
if (this.gracePeriodTimer) {
|
||||
clearTimeout(this.gracePeriodTimer)
|
||||
this.gracePeriodTimer = null
|
||||
console.log(`[TerminalProcess] Cleared existing grace period timer before starting new command`)
|
||||
}
|
||||
|
||||
// Clear any existing hot timer
|
||||
if (this.hotTimer) {
|
||||
clearTimeout(this.hotTimer)
|
||||
this.hotTimer = null
|
||||
}
|
||||
|
||||
// Reset state for new command
|
||||
this.hasEmittedCompleted = false
|
||||
this.buffer = ""
|
||||
this.fullOutput = ""
|
||||
this.lastRetrievedIndex = 0
|
||||
this.isListening = true
|
||||
this.isHot = false
|
||||
|
||||
this.command = command
|
||||
console.log(`[TerminalProcess] Starting command: "${command}"`)
|
||||
console.log(`[TerminalProcess] Shell integration available: ${!!terminal.shellIntegration?.executeCommand}`)
|
||||
console.log(`[TerminalProcess] Terminal ID: ${terminal.name}`)
|
||||
if (terminal.shellIntegration && terminal.shellIntegration.executeCommand) {
|
||||
let execution
|
||||
let stream
|
||||
|
||||
try {
|
||||
execution = terminal.shellIntegration.executeCommand(command)
|
||||
stream = execution.read()
|
||||
} catch (error) {
|
||||
console.error(`[TerminalProcess] Failed to execute command: ${error}`)
|
||||
this.emit("error", error as Error)
|
||||
return
|
||||
}
|
||||
|
||||
const execution = terminal.shellIntegration.executeCommand(command)
|
||||
const stream = execution.read()
|
||||
// todo: need to handle errors
|
||||
let isFirstChunk = true
|
||||
let didOutputNonCommand = false
|
||||
let didEmitEmptyLine = false
|
||||
let receivedFirstChunk = false
|
||||
|
||||
// Set up a timeout to emit empty line if no output is received within 3 seconds
|
||||
// This ensures the "proceed while running" button appears even for commands with no/delayed output
|
||||
const firstChunkTimeout = setTimeout(() => {
|
||||
if (!receivedFirstChunk && !didEmitEmptyLine) {
|
||||
console.log(`[TerminalProcess] First chunk timeout fired - no output received within 3s for: "${command}"`)
|
||||
this.emit("line", "") // empty line to show proceed button
|
||||
didEmitEmptyLine = true
|
||||
|
||||
// Also emit a message indicating the command might be running without output
|
||||
this.emit("line", "[Command is running but producing no output]")
|
||||
}
|
||||
}, 3000) // 3 second timeout
|
||||
|
||||
for await (let data of stream) {
|
||||
// Clear the timeout since we received output
|
||||
if (!receivedFirstChunk) {
|
||||
clearTimeout(firstChunkTimeout)
|
||||
receivedFirstChunk = true
|
||||
console.log(`[TerminalProcess] First chunk received for command: "${command}"`)
|
||||
}
|
||||
|
||||
// Log raw data length
|
||||
console.log(`[TerminalProcess] Raw data chunk received: ${data.length} chars`)
|
||||
if (!data || data.trim() === "") {
|
||||
console.log(`[TerminalProcess] WARNING: Received empty or whitespace-only chunk`)
|
||||
}
|
||||
// 1. Process chunk and remove artifacts
|
||||
if (isFirstChunk) {
|
||||
/*
|
||||
@@ -240,79 +176,14 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
|
||||
|
||||
this.emitRemainingBufferIfListening()
|
||||
|
||||
// Clean up the first chunk timeout if it's still active
|
||||
if (!receivedFirstChunk) {
|
||||
clearTimeout(firstChunkTimeout)
|
||||
console.log(`[TerminalProcess] WARNING: Stream ended without receiving any chunks for command: "${command}"`)
|
||||
|
||||
// If we never received any chunks and haven't emitted anything yet, emit now
|
||||
if (!didEmitEmptyLine) {
|
||||
console.log(`[TerminalProcess] Emitting fallback empty line for no-output command`)
|
||||
this.emit("line", "") // empty line to show proceed button
|
||||
this.emit("line", "[Command completed with no output]")
|
||||
didEmitEmptyLine = true
|
||||
}
|
||||
}
|
||||
|
||||
// for now we don't want this delaying requests since we don't send diagnostics automatically anymore (previous: "even though the command is finished, we still want to consider it 'hot' in case so that api request stalls to let diagnostics catch up")
|
||||
if (this.hotTimer) {
|
||||
clearTimeout(this.hotTimer)
|
||||
}
|
||||
this.isHot = false
|
||||
|
||||
console.log(`[TerminalProcess] Stream ended for command: "${command}"`)
|
||||
console.log(`[TerminalProcess] Final output length: ${this.fullOutput.length} characters`)
|
||||
|
||||
// Check if this looks like a command that completed vs one that's still running
|
||||
const quickCommands = ["cd ", "pwd", "ls ", "echo ", "mkdir ", "touch ", "rm ", "cp ", "mv "]
|
||||
const isQuickCommand = quickCommands.some((cmd) => command.startsWith(cmd) || command.includes(" && " + cmd))
|
||||
|
||||
// Check if output suggests a long-running process
|
||||
const longRunningIndicators = [
|
||||
"listening on",
|
||||
"server running",
|
||||
"started on",
|
||||
"watching for",
|
||||
"compiled successfully",
|
||||
"webpack",
|
||||
"vite",
|
||||
"nodemon",
|
||||
"dev server",
|
||||
"press ctrl",
|
||||
"to quit",
|
||||
"to exit",
|
||||
"to stop",
|
||||
]
|
||||
const hasLongRunningOutput = longRunningIndicators.some((indicator) =>
|
||||
this.fullOutput.toLowerCase().includes(indicator),
|
||||
)
|
||||
|
||||
// Check if this is likely a command that starts a server or long-running process
|
||||
const longRunningCommands = ["npm run", "npm start", "yarn", "node ", "python ", "serve", "dev", "watch"]
|
||||
const isLongRunningCommand = longRunningCommands.some((cmd) => command.includes(cmd))
|
||||
|
||||
if (this.fullOutput.length === 0) {
|
||||
console.log(`[TerminalProcess] WARNING: Process completed but no output was captured`)
|
||||
// Ensure we emit at least one line for UI feedback
|
||||
if (!didEmitEmptyLine) {
|
||||
this.emit("line", "[Command completed silently]")
|
||||
}
|
||||
}
|
||||
|
||||
// Only skip grace period for truly quick commands that have no output or are known to complete instantly
|
||||
if ((this.fullOutput.length === 0 || isQuickCommand) && !isLongRunningCommand && !hasLongRunningOutput) {
|
||||
console.log(`[TerminalProcess] Command appears to have completed immediately, skipping grace period`)
|
||||
this.emit("completed")
|
||||
this.emit("continue")
|
||||
} else {
|
||||
console.log(
|
||||
`[TerminalProcess] Command may still be running (longRunningCommand: ${isLongRunningCommand}, longRunningOutput: ${hasLongRunningOutput})`,
|
||||
)
|
||||
console.log(`[TerminalProcess] Starting grace period to detect true completion...`)
|
||||
// Start grace period - wait 2.5 seconds to see if more output comes
|
||||
// This prevents premature "proceed while running" for commands that clearly finished
|
||||
this.startGracePeriod()
|
||||
}
|
||||
this.emit("completed")
|
||||
this.emit("continue")
|
||||
} else {
|
||||
terminal.sendText(command, true)
|
||||
// For terminals without shell integration, we can't know when the command completes
|
||||
@@ -331,24 +202,14 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
|
||||
private emitIfEol(chunk: string) {
|
||||
this.buffer += chunk
|
||||
let lineEndIndex: number
|
||||
let lineCount = 0
|
||||
while ((lineEndIndex = this.buffer.indexOf("\n")) !== -1) {
|
||||
let line = this.buffer.slice(0, lineEndIndex).trimEnd() // removes trailing \r
|
||||
// Remove \r if present (for Windows-style line endings)
|
||||
// if (line.endsWith("\r")) {
|
||||
// line = line.slice(0, -1)
|
||||
// }
|
||||
if (!line || line.trim() === "") {
|
||||
console.log(`[TerminalProcess] Emitting empty line`)
|
||||
} else {
|
||||
console.log(`[TerminalProcess] Emitting line: ${line.substring(0, 100)}${line.length > 100 ? "..." : ""}`)
|
||||
}
|
||||
this.emit("line", line)
|
||||
this.buffer = this.buffer.slice(lineEndIndex + 1)
|
||||
lineCount++
|
||||
}
|
||||
if (lineCount === 0 && chunk.length > 0) {
|
||||
console.log(`[TerminalProcess] Buffering partial line, buffer size: ${this.buffer.length}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,41 +224,7 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
|
||||
}
|
||||
}
|
||||
|
||||
private startGracePeriod() {
|
||||
// Clear any existing grace period timer
|
||||
if (this.gracePeriodTimer) {
|
||||
clearTimeout(this.gracePeriodTimer)
|
||||
}
|
||||
|
||||
// Emit completed event for UI to show "proceed while running" button
|
||||
console.log(`[TerminalProcess] Emitting completed event for UI (grace period active)`)
|
||||
this.emit("completed")
|
||||
|
||||
// Wait 2.5 seconds to see if the command is truly finished
|
||||
this.gracePeriodTimer = setTimeout(() => {
|
||||
// Double-check the timer hasn't been cleared
|
||||
if (this.gracePeriodTimer && !this.hasEmittedCompleted) {
|
||||
console.log(`[TerminalProcess] Grace period completed - command appears truly finished: "${this.command}"`)
|
||||
console.log(`[TerminalProcess] Auto-continuing without user intervention`)
|
||||
this.hasEmittedCompleted = true
|
||||
this.gracePeriodTimer = null
|
||||
// Only emit continue after the grace period, not immediately
|
||||
this.emit("continue")
|
||||
}
|
||||
}, 2500) // 2.5 second grace period
|
||||
}
|
||||
|
||||
continue() {
|
||||
console.log(`[TerminalProcess] Manual continue() called for: "${this.command}"`)
|
||||
|
||||
// Clear grace period since user manually continued
|
||||
if (this.gracePeriodTimer) {
|
||||
console.log(`[TerminalProcess] Clearing grace period timer due to manual continue`)
|
||||
clearTimeout(this.gracePeriodTimer)
|
||||
this.gracePeriodTimer = null
|
||||
}
|
||||
|
||||
this.hasEmittedCompleted = true
|
||||
this.emitRemainingBufferIfListening()
|
||||
this.isListening = false
|
||||
this.removeAllListeners("line")
|
||||
|
||||
@@ -5,8 +5,6 @@ export interface TerminalInfo {
|
||||
busy: boolean
|
||||
lastCommand: string
|
||||
id: number
|
||||
shellPath?: string
|
||||
lastActive: number
|
||||
pendingCwdChange?: string
|
||||
cwdResolved?: {
|
||||
resolve: () => void
|
||||
@@ -20,26 +18,17 @@ export class TerminalRegistry {
|
||||
private static terminals: TerminalInfo[] = []
|
||||
private static nextTerminalId = 1
|
||||
|
||||
static createTerminal(cwd?: string | vscode.Uri | undefined, shellPath?: string): TerminalInfo {
|
||||
const terminalOptions: vscode.TerminalOptions = {
|
||||
static createTerminal(cwd?: string | vscode.Uri | undefined): TerminalInfo {
|
||||
const terminal = vscode.window.createTerminal({
|
||||
cwd,
|
||||
name: "Cline",
|
||||
iconPath: new vscode.ThemeIcon("robot"),
|
||||
}
|
||||
|
||||
// If a specific shell path is provided, use it
|
||||
if (shellPath) {
|
||||
terminalOptions.shellPath = shellPath
|
||||
}
|
||||
|
||||
const terminal = vscode.window.createTerminal(terminalOptions)
|
||||
})
|
||||
const newInfo: TerminalInfo = {
|
||||
terminal,
|
||||
busy: false,
|
||||
lastCommand: "",
|
||||
id: this.nextTerminalId++,
|
||||
shellPath,
|
||||
lastActive: Date.now(),
|
||||
}
|
||||
this.terminals.push(newInfo)
|
||||
return newInfo
|
||||
|
||||
@@ -1,81 +1,55 @@
|
||||
import { globby, Options } from "globby"
|
||||
import * as os from "os"
|
||||
import os from "os"
|
||||
import * as path from "path"
|
||||
import { arePathsEqual } from "@utils/path"
|
||||
|
||||
// Constants
|
||||
const DEFAULT_IGNORE_DIRECTORIES = [
|
||||
"node_modules",
|
||||
"__pycache__",
|
||||
"env",
|
||||
"venv",
|
||||
"target/dependency",
|
||||
"build/dependencies",
|
||||
"dist",
|
||||
"out",
|
||||
"bundle",
|
||||
"vendor",
|
||||
"tmp",
|
||||
"temp",
|
||||
"deps",
|
||||
"pkg",
|
||||
"Pods",
|
||||
]
|
||||
|
||||
// Helper functions
|
||||
function isRestrictedPath(absolutePath: string): boolean {
|
||||
export async function listFiles(dirPath: string, recursive: boolean, limit: number): Promise<[string[], boolean]> {
|
||||
// First resolve the path normally - path.resolve doesn't care about glob special characters
|
||||
const absolutePath = path.resolve(dirPath)
|
||||
// Do not allow listing files in root or home directory, which cline tends to want to do when the user's prompt is vague.
|
||||
const root = process.platform === "win32" ? path.parse(absolutePath).root : "/"
|
||||
const isRoot = arePathsEqual(absolutePath, root)
|
||||
if (isRoot) {
|
||||
return true
|
||||
return [[root], false]
|
||||
}
|
||||
|
||||
const homeDir = os.homedir()
|
||||
const isHomeDir = arePathsEqual(absolutePath, homeDir)
|
||||
if (isHomeDir) {
|
||||
return true
|
||||
return [[homeDir], false]
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function isTargetingHiddenDirectory(absolutePath: string): boolean {
|
||||
const dirName = path.basename(absolutePath)
|
||||
return dirName.startsWith(".")
|
||||
}
|
||||
|
||||
function buildIgnorePatterns(absolutePath: string): string[] {
|
||||
const isTargetHidden = isTargetingHiddenDirectory(absolutePath)
|
||||
|
||||
const patterns = [...DEFAULT_IGNORE_DIRECTORIES]
|
||||
|
||||
// Only ignore hidden directories if we're not explicitly targeting a hidden directory
|
||||
if (!isTargetHidden) {
|
||||
patterns.push(".*")
|
||||
}
|
||||
|
||||
return patterns.map((dir) => `**/${dir}/**`)
|
||||
}
|
||||
|
||||
export async function listFiles(dirPath: string, recursive: boolean, limit: number): Promise<[string[], boolean]> {
|
||||
const absolutePath = path.resolve(dirPath)
|
||||
|
||||
// Do not allow listing files in root or home directory
|
||||
if (isRestrictedPath(absolutePath)) {
|
||||
return [[], false]
|
||||
}
|
||||
const dirsToIgnore = [
|
||||
"node_modules",
|
||||
"__pycache__",
|
||||
"env",
|
||||
"venv",
|
||||
"target/dependency",
|
||||
"build/dependencies",
|
||||
"dist",
|
||||
"out",
|
||||
"bundle",
|
||||
"vendor",
|
||||
"tmp",
|
||||
"temp",
|
||||
"deps",
|
||||
"pkg",
|
||||
"Pods",
|
||||
".*", // '!**/.*' excludes hidden directories, while '!**/.*/**' excludes only their contents. This way we are at least aware of the existence of hidden directories.
|
||||
].map((dir) => `**/${dir}/**`)
|
||||
|
||||
const options: Options = {
|
||||
cwd: dirPath,
|
||||
dot: true, // do not ignore hidden files/directories
|
||||
absolute: true,
|
||||
markDirectories: true, // Append a / on any directories matched
|
||||
markDirectories: true, // Append a / on any directories matched (/ is used on windows as well, so dont use path.sep)
|
||||
gitignore: recursive, // globby ignores any files that are gitignored
|
||||
ignore: recursive ? buildIgnorePatterns(absolutePath) : undefined,
|
||||
onlyFiles: false, // include directories in results
|
||||
ignore: recursive ? dirsToIgnore : undefined, // just in case there is no gitignore, we ignore sensible defaults
|
||||
onlyFiles: false, // true by default, false means it will list directories on their own too
|
||||
suppressErrors: true,
|
||||
}
|
||||
|
||||
// * globs all files in one dir, ** globs files in nested directories
|
||||
// For non-recursive listing, we still use a simple pattern
|
||||
const filePaths = recursive ? await globbyLevelByLevel(limit, options) : (await globby("*", options)).slice(0, limit)
|
||||
|
||||
return [filePaths, filePaths.length >= limit]
|
||||
|
||||
@@ -54,17 +54,6 @@ export class McpHub {
|
||||
connections: McpConnection[] = []
|
||||
isConnecting: boolean = false
|
||||
|
||||
// Store notifications for display in chat
|
||||
private pendingNotifications: Array<{
|
||||
serverName: string
|
||||
level: string
|
||||
message: string
|
||||
timestamp: number
|
||||
}> = []
|
||||
|
||||
// Callback for sending notifications to active task
|
||||
private notificationCallback?: (serverName: string, level: string, message: string) => void
|
||||
|
||||
constructor(
|
||||
getMcpServersPath: () => Promise<string>,
|
||||
getSettingsDirectoryPath: () => Promise<string>,
|
||||
@@ -332,88 +321,6 @@ export class McpHub {
|
||||
connection.server.status = "connected"
|
||||
connection.server.error = ""
|
||||
|
||||
// Register notification handler for real-time messages
|
||||
console.log(`[MCP Debug] Setting up notification handlers for server: ${name}`)
|
||||
console.log(`[MCP Debug] Client instance:`, connection.client)
|
||||
console.log(`[MCP Debug] Transport type:`, config.type)
|
||||
|
||||
// Try to set notification handler using the client's method
|
||||
try {
|
||||
// Import the notification schema from MCP SDK
|
||||
const { z } = await import("zod")
|
||||
|
||||
// Define the notification schema for notifications/message
|
||||
const NotificationMessageSchema = z.object({
|
||||
method: z.literal("notifications/message"),
|
||||
params: z
|
||||
.object({
|
||||
level: z.enum(["debug", "info", "warning", "error"]).optional(),
|
||||
logger: z.string().optional(),
|
||||
data: z.string().optional(),
|
||||
message: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
// Set the notification handler
|
||||
connection.client.setNotificationHandler(NotificationMessageSchema as any, async (notification: any) => {
|
||||
console.log(`[MCP Notification] ${name}:`, JSON.stringify(notification, null, 2))
|
||||
|
||||
const params = notification.params || {}
|
||||
const level = params.level || "info"
|
||||
const data = params.data || params.message || ""
|
||||
const logger = params.logger || ""
|
||||
|
||||
console.log(`[MCP Message Notification] ${name}: level=${level}, data=${data}, logger=${logger}`)
|
||||
|
||||
// Format the message
|
||||
const message = logger ? `[${logger}] ${data}` : data
|
||||
|
||||
// Send notification directly to active task if callback is set
|
||||
if (this.notificationCallback) {
|
||||
console.log(`[MCP Debug] Sending notification to active task: ${message}`)
|
||||
this.notificationCallback(name, level, message)
|
||||
} else {
|
||||
// Fallback: store for later retrieval
|
||||
console.log(`[MCP Debug] No active task, storing notification: ${message}`)
|
||||
this.pendingNotifications.push({
|
||||
serverName: name,
|
||||
level,
|
||||
message,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
}
|
||||
|
||||
// Forward to webview if available
|
||||
if (this.postMessageToWebview) {
|
||||
await this.postMessageToWebview({
|
||||
type: "mcpNotification",
|
||||
serverName: name,
|
||||
notification: {
|
||||
level,
|
||||
data,
|
||||
logger,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
} as any)
|
||||
}
|
||||
})
|
||||
console.log(`[MCP Debug] Successfully set notifications/message handler for ${name}`)
|
||||
|
||||
// Also set a fallback handler for any other notification types
|
||||
connection.client.fallbackNotificationHandler = async (notification: any) => {
|
||||
console.log(`[MCP Fallback Notification] ${name}:`, JSON.stringify(notification, null, 2))
|
||||
|
||||
// Show in VS Code for visibility
|
||||
vscode.window.showInformationMessage(
|
||||
`MCP ${name}: ${notification.method || "unknown"} - ${JSON.stringify(notification.params || {})}`,
|
||||
)
|
||||
}
|
||||
console.log(`[MCP Debug] Successfully set fallback notification handler for ${name}`)
|
||||
} catch (error) {
|
||||
console.error(`[MCP Debug] Error setting notification handlers for ${name}:`, error)
|
||||
}
|
||||
|
||||
// Initial fetch of tools and resources
|
||||
connection.server.tools = await this.fetchToolsList(name)
|
||||
connection.server.resources = await this.fetchResourcesList(name)
|
||||
@@ -1042,38 +949,6 @@ export class McpHub {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get and clear pending notifications
|
||||
* @returns Array of pending notifications
|
||||
*/
|
||||
getPendingNotifications(): Array<{
|
||||
serverName: string
|
||||
level: string
|
||||
message: string
|
||||
timestamp: number
|
||||
}> {
|
||||
const notifications = [...this.pendingNotifications]
|
||||
this.pendingNotifications = []
|
||||
return notifications
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the notification callback for real-time notifications
|
||||
* @param callback Function to call when notifications arrive
|
||||
*/
|
||||
setNotificationCallback(callback: (serverName: string, level: string, message: string) => void): void {
|
||||
this.notificationCallback = callback
|
||||
console.log("[MCP Debug] Notification callback set")
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the notification callback
|
||||
*/
|
||||
clearNotificationCallback(): void {
|
||||
this.notificationCallback = undefined
|
||||
console.log("[MCP Debug] Notification callback cleared")
|
||||
}
|
||||
|
||||
async dispose(): Promise<void> {
|
||||
this.removeAllFileWatchers()
|
||||
for (const connection of this.connections) {
|
||||
|
||||
+26
-48
@@ -16,71 +16,49 @@ const createServerTypeSchema = () => {
|
||||
// Stdio config (has command field)
|
||||
BaseConfigSchema.extend({
|
||||
type: z.literal("stdio").optional(),
|
||||
transportType: z.string().optional(), // Support legacy field
|
||||
command: z.string(),
|
||||
args: z.array(z.string()).optional(),
|
||||
cwd: z.string().optional(),
|
||||
env: z.record(z.string()).optional(),
|
||||
// Allow other fields for backward compatibility
|
||||
url: z.string().optional(),
|
||||
headers: z.record(z.string()).optional(),
|
||||
// Explicitly disallow other types' fields
|
||||
url: z.undefined().optional(),
|
||||
headers: z.undefined().optional(),
|
||||
})
|
||||
.transform((data) => {
|
||||
// Support both type and transportType fields
|
||||
const finalType = data.type || (data.transportType === "stdio" ? "stdio" : undefined) || "stdio"
|
||||
return {
|
||||
...data,
|
||||
type: finalType as "stdio",
|
||||
// Remove the legacy field after transformation
|
||||
transportType: undefined,
|
||||
}
|
||||
})
|
||||
.refine((data) => data.type === "stdio", { message: TYPE_ERROR_MESSAGE }),
|
||||
.transform((data) => ({
|
||||
...data,
|
||||
type: "stdio" as const,
|
||||
}))
|
||||
.refine((data) => data.type === undefined || data.type === "stdio", { message: TYPE_ERROR_MESSAGE }),
|
||||
// SSE config (has url field)
|
||||
BaseConfigSchema.extend({
|
||||
type: z.literal("sse").optional(),
|
||||
transportType: z.string().optional(), // Support legacy field
|
||||
url: z.string().url("URL must be a valid URL format"),
|
||||
headers: z.record(z.string()).optional(),
|
||||
// Allow other fields for backward compatibility
|
||||
command: z.string().optional(),
|
||||
args: z.array(z.string()).optional(),
|
||||
env: z.record(z.string()).optional(),
|
||||
// Explicitly disallow other types' fields
|
||||
command: z.undefined().optional(),
|
||||
args: z.undefined().optional(),
|
||||
env: z.undefined().optional(),
|
||||
})
|
||||
.transform((data) => {
|
||||
// Support both type and transportType fields
|
||||
const finalType = data.type || (data.transportType === "sse" ? "sse" : undefined) || "sse"
|
||||
return {
|
||||
...data,
|
||||
type: finalType as "sse",
|
||||
// Remove the legacy field after transformation
|
||||
transportType: undefined,
|
||||
}
|
||||
})
|
||||
.refine((data) => data.type === "sse", { message: TYPE_ERROR_MESSAGE }),
|
||||
.transform((data) => ({
|
||||
...data,
|
||||
type: "sse" as const,
|
||||
}))
|
||||
.refine((data) => data.type === undefined || data.type === "sse", { message: TYPE_ERROR_MESSAGE }),
|
||||
// Streamable HTTP config (has url field)
|
||||
BaseConfigSchema.extend({
|
||||
type: z.literal("streamableHttp").optional(),
|
||||
transportType: z.string().optional(), // Support legacy field
|
||||
url: z.string().url("URL must be a valid URL format"),
|
||||
headers: z.record(z.string()).optional(),
|
||||
// Allow other fields for backward compatibility
|
||||
command: z.string().optional(),
|
||||
args: z.array(z.string()).optional(),
|
||||
env: z.record(z.string()).optional(),
|
||||
// Explicitly disallow other types' fields
|
||||
command: z.undefined().optional(),
|
||||
args: z.undefined().optional(),
|
||||
env: z.undefined().optional(),
|
||||
})
|
||||
.transform((data) => {
|
||||
// Support both type and transportType fields
|
||||
// Note: legacy transportType was "http" not "streamableHttp"
|
||||
const finalType = data.type || (data.transportType === "http" ? "streamableHttp" : undefined) || "streamableHttp"
|
||||
return {
|
||||
...data,
|
||||
type: finalType as "streamableHttp",
|
||||
// Remove the legacy field after transformation
|
||||
transportType: undefined,
|
||||
}
|
||||
})
|
||||
.refine((data) => data.type === "streamableHttp", {
|
||||
.transform((data) => ({
|
||||
...data,
|
||||
type: "streamableHttp" as const,
|
||||
}))
|
||||
.refine((data) => data.type === undefined || data.type === "streamableHttp", {
|
||||
message: TYPE_ERROR_MESSAGE,
|
||||
}),
|
||||
])
|
||||
|
||||
@@ -81,8 +81,8 @@ export interface ExtensionState {
|
||||
checkpointTrackerErrorMessage?: string
|
||||
clineMessages: ClineMessage[]
|
||||
currentTaskItem?: HistoryItem
|
||||
customInstructions?: string
|
||||
mcpMarketplaceEnabled?: boolean
|
||||
mcpRichDisplayEnabled: boolean
|
||||
planActSeparateModelsSetting: boolean
|
||||
enableCheckpointsSetting?: boolean
|
||||
platform: Platform
|
||||
@@ -91,8 +91,6 @@ export interface ExtensionState {
|
||||
telemetrySetting: TelemetrySetting
|
||||
shellIntegrationTimeout: number
|
||||
terminalReuseEnabled?: boolean
|
||||
terminalOutputLineLimit: number
|
||||
defaultTerminalProfile?: string
|
||||
uriScheme?: string
|
||||
userInfo?: {
|
||||
displayName: string | null
|
||||
@@ -165,7 +163,6 @@ export type ClineSay =
|
||||
| "browser_action_result"
|
||||
| "mcp_server_request_started"
|
||||
| "mcp_server_response"
|
||||
| "mcp_notification"
|
||||
| "use_mcp_server"
|
||||
| "diff_error"
|
||||
| "deleted_api_reqs"
|
||||
|
||||
@@ -8,6 +8,7 @@ import { McpViewTab } from "./mcp"
|
||||
|
||||
export interface WebviewMessage {
|
||||
type:
|
||||
| "apiConfiguration"
|
||||
| "requestVsCodeLmModels"
|
||||
| "authStateChanged"
|
||||
| "fetchMcpMarketplace"
|
||||
@@ -45,7 +46,7 @@ export interface WebviewMessage {
|
||||
mcpMarketplaceEnabled?: boolean
|
||||
mcpResponsesCollapsed?: boolean
|
||||
telemetrySetting?: TelemetrySetting
|
||||
mcpRichDisplayEnabled?: boolean
|
||||
customInstructionsSetting?: string
|
||||
mentionsRequestId?: string
|
||||
query?: string
|
||||
// For toggleFavoriteModel
|
||||
@@ -70,7 +71,6 @@ export interface WebviewMessage {
|
||||
offset?: number
|
||||
shellIntegrationTimeout?: number
|
||||
terminalReuseEnabled?: boolean
|
||||
defaultTerminalProfile?: string
|
||||
}
|
||||
|
||||
export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse"
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { LanguageModelChatSelector } from "../api/providers/types"
|
||||
|
||||
export type ApiProvider =
|
||||
| "anthropic"
|
||||
| "claude-code"
|
||||
| "openrouter"
|
||||
| "bedrock"
|
||||
| "vertex"
|
||||
@@ -26,7 +25,6 @@ export type ApiProvider =
|
||||
| "xai"
|
||||
| "sambanova"
|
||||
| "cerebras"
|
||||
| "sapaicore"
|
||||
|
||||
export interface ApiHandlerOptions {
|
||||
apiModelId?: string
|
||||
@@ -55,7 +53,6 @@ export interface ApiHandlerOptions {
|
||||
awsBedrockEndpoint?: string
|
||||
awsBedrockCustomSelected?: boolean
|
||||
awsBedrockCustomModelBaseId?: BedrockModelId
|
||||
claudeCodePath?: string
|
||||
vertexProjectId?: string
|
||||
vertexRegion?: string
|
||||
openAiBaseUrl?: string
|
||||
@@ -95,12 +92,6 @@ export interface ApiHandlerOptions {
|
||||
sambanovaApiKey?: string
|
||||
cerebrasApiKey?: string
|
||||
requestTimeoutMs?: number
|
||||
sapAiCoreClientId?: string
|
||||
sapAiCoreClientSecret?: string
|
||||
sapAiResourceGroup?: string
|
||||
sapAiCoreTokenUrl?: string
|
||||
sapAiCoreBaseUrl?: string
|
||||
sapAiCoreModelId?: string
|
||||
onRetryAttempt?: (attempt: number, maxRetries: number, delay: number, error: any) => void
|
||||
}
|
||||
|
||||
@@ -226,17 +217,6 @@ export const anthropicModels = {
|
||||
},
|
||||
} as const satisfies Record<string, ModelInfo> // as const assertion makes the object deeply readonly
|
||||
|
||||
// Claude Code
|
||||
export type ClaudeCodeModelId = keyof typeof claudeCodeModels
|
||||
export const claudeCodeDefaultModelId: ClaudeCodeModelId = "claude-sonnet-4-20250514"
|
||||
export const claudeCodeModels = {
|
||||
"claude-sonnet-4-20250514": anthropicModels["claude-sonnet-4-20250514"],
|
||||
"claude-opus-4-20250514": anthropicModels["claude-opus-4-20250514"],
|
||||
"claude-3-7-sonnet-20250219": anthropicModels["claude-3-7-sonnet-20250219"],
|
||||
"claude-3-5-sonnet-20241022": anthropicModels["claude-3-5-sonnet-20241022"],
|
||||
"claude-3-5-haiku-20241022": anthropicModels["claude-3-5-haiku-20241022"],
|
||||
} as const satisfies Record<string, ModelInfo>
|
||||
|
||||
// AWS Bedrock
|
||||
// https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference.html
|
||||
export type BedrockModelId = keyof typeof bedrockModels
|
||||
@@ -618,9 +598,6 @@ export const vertexModels = {
|
||||
cacheReadsPrice: 0.625,
|
||||
},
|
||||
],
|
||||
thinkingConfig: {
|
||||
maxBudget: 32768,
|
||||
},
|
||||
},
|
||||
"gemini-2.5-flash-preview-04-17": {
|
||||
maxTokens: 65536,
|
||||
@@ -789,9 +766,6 @@ export const geminiModels = {
|
||||
cacheReadsPrice: 0.625,
|
||||
},
|
||||
],
|
||||
thinkingConfig: {
|
||||
maxBudget: 32768,
|
||||
},
|
||||
},
|
||||
"gemini-2.5-flash-preview-05-20": {
|
||||
maxTokens: 65536,
|
||||
@@ -2427,125 +2401,3 @@ export const requestyDefaultModelInfo: ModelInfo = {
|
||||
cacheReadsPrice: 0.3,
|
||||
description: "Anthropic's most intelligent model. Highest level of intelligence and capability.",
|
||||
}
|
||||
|
||||
// SAP AI Core
|
||||
export type SapAiCoreModelId = keyof typeof sapAiCoreModels
|
||||
export const sapAiCoreDefaultModelId: SapAiCoreModelId = "anthropic--claude-3.5-sonnet"
|
||||
export const sapAiCoreModels = {
|
||||
"anthropic--claude-3.7-sonnet": {
|
||||
maxTokens: 64_000,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
},
|
||||
"anthropic--claude-3.5-sonnet": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
},
|
||||
"anthropic--claude-3-sonnet": {
|
||||
maxTokens: 4096,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
},
|
||||
"anthropic--claude-3-haiku": {
|
||||
maxTokens: 4096,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
},
|
||||
"anthropic--claude-3-opus": {
|
||||
maxTokens: 4096,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
},
|
||||
"gpt-4o": {
|
||||
maxTokens: 4096,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
},
|
||||
"gpt-4o-mini": {
|
||||
maxTokens: 4096,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
},
|
||||
"gpt-4": {
|
||||
maxTokens: 4096,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
},
|
||||
o1: {
|
||||
maxTokens: 4096,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
},
|
||||
"o3-mini": {
|
||||
maxTokens: 4096,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
},
|
||||
"gpt-4.1": {
|
||||
maxTokens: 32_768,
|
||||
contextWindow: 1_047_576,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 2,
|
||||
outputPrice: 8,
|
||||
cacheReadsPrice: 0.5,
|
||||
},
|
||||
"gpt-4.1-nano": {
|
||||
maxTokens: 32_768,
|
||||
contextWindow: 1_047_576,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 0.1,
|
||||
outputPrice: 0.4,
|
||||
cacheReadsPrice: 0.025,
|
||||
},
|
||||
o3: {
|
||||
maxTokens: 100_000,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 10.0,
|
||||
outputPrice: 40.0,
|
||||
cacheReadsPrice: 2.5,
|
||||
},
|
||||
"o4-mini": {
|
||||
maxTokens: 100_000,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 1.1,
|
||||
outputPrice: 4.4,
|
||||
cacheReadsPrice: 0.275,
|
||||
},
|
||||
} as const satisfies Record<string, ModelInfo>
|
||||
|
||||
@@ -89,7 +89,6 @@ function convertClineSayToProtoEnum(say: AppClineSay | undefined): ClineSay | un
|
||||
browser_action_result: ClineSay.BROWSER_ACTION_RESULT,
|
||||
mcp_server_request_started: ClineSay.MCP_SERVER_REQUEST_STARTED,
|
||||
mcp_server_response: ClineSay.MCP_SERVER_RESPONSE,
|
||||
mcp_notification: ClineSay.MCP_NOTIFICATION,
|
||||
use_mcp_server: ClineSay.USE_MCP_SERVER_SAY,
|
||||
diff_error: ClineSay.DIFF_ERROR,
|
||||
deleted_api_reqs: ClineSay.DELETED_API_REQS,
|
||||
@@ -133,7 +132,6 @@ function convertProtoEnumToClineSay(say: ClineSay): AppClineSay | undefined {
|
||||
[ClineSay.BROWSER_ACTION_RESULT]: "browser_action_result",
|
||||
[ClineSay.MCP_SERVER_REQUEST_STARTED]: "mcp_server_request_started",
|
||||
[ClineSay.MCP_SERVER_RESPONSE]: "mcp_server_response",
|
||||
[ClineSay.MCP_NOTIFICATION]: "mcp_notification",
|
||||
[ClineSay.USE_MCP_SERVER_SAY]: "use_mcp_server",
|
||||
[ClineSay.DIFF_ERROR]: "diff_error",
|
||||
[ClineSay.DELETED_API_REQS]: "deleted_api_reqs",
|
||||
|
||||
@@ -1,462 +0,0 @@
|
||||
import {
|
||||
ApiConfiguration,
|
||||
ApiProvider,
|
||||
BedrockModelId,
|
||||
ModelInfo,
|
||||
OpenAiCompatibleModelInfo as AppOpenAiCompatibleModelInfo,
|
||||
LiteLLMModelInfo as AppLiteLLMModelInfo,
|
||||
} from "../../api"
|
||||
import {
|
||||
ModelsApiConfiguration as ProtoApiConfiguration,
|
||||
ApiProvider as ProtoApiProvider,
|
||||
LiteLLMModelInfo,
|
||||
OpenAiCompatibleModelInfo,
|
||||
OpenRouterModelInfo,
|
||||
ThinkingConfig,
|
||||
} from "../../proto/models"
|
||||
|
||||
// Convert application ThinkingConfig to proto ThinkingConfig
|
||||
function convertThinkingConfigToProto(config: ModelInfo["thinkingConfig"]): ThinkingConfig | undefined {
|
||||
if (!config) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
maxBudget: config.maxBudget,
|
||||
outputPrice: config.outputPrice,
|
||||
outputPriceTiers: config.outputPriceTiers || [], // Provide empty array if undefined
|
||||
}
|
||||
}
|
||||
|
||||
// Convert proto ThinkingConfig to application ThinkingConfig
|
||||
function convertProtoToThinkingConfig(config: ThinkingConfig | undefined): ModelInfo["thinkingConfig"] | undefined {
|
||||
if (!config) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
maxBudget: config.maxBudget,
|
||||
outputPrice: config.outputPrice,
|
||||
outputPriceTiers: config.outputPriceTiers.length > 0 ? config.outputPriceTiers : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
// Convert application ModelInfo to proto OpenRouterModelInfo
|
||||
function convertModelInfoToProtoOpenRouter(info: ModelInfo | undefined): OpenRouterModelInfo | undefined {
|
||||
if (!info) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
maxTokens: info.maxTokens,
|
||||
contextWindow: info.contextWindow,
|
||||
supportsImages: info.supportsImages,
|
||||
supportsPromptCache: info.supportsPromptCache ?? false,
|
||||
inputPrice: info.inputPrice,
|
||||
outputPrice: info.outputPrice,
|
||||
cacheWritesPrice: info.cacheWritesPrice,
|
||||
cacheReadsPrice: info.cacheReadsPrice,
|
||||
description: info.description,
|
||||
thinkingConfig: convertThinkingConfigToProto(info.thinkingConfig),
|
||||
supportsGlobalEndpoint: info.supportsGlobalEndpoint,
|
||||
tiers: info.tiers || [],
|
||||
}
|
||||
}
|
||||
|
||||
// Convert proto OpenRouterModelInfo to application ModelInfo
|
||||
function convertProtoToModelInfo(info: OpenRouterModelInfo | undefined): ModelInfo | undefined {
|
||||
if (!info) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
maxTokens: info.maxTokens,
|
||||
contextWindow: info.contextWindow,
|
||||
supportsImages: info.supportsImages,
|
||||
supportsPromptCache: info.supportsPromptCache,
|
||||
inputPrice: info.inputPrice,
|
||||
outputPrice: info.outputPrice,
|
||||
cacheWritesPrice: info.cacheWritesPrice,
|
||||
cacheReadsPrice: info.cacheReadsPrice,
|
||||
description: info.description,
|
||||
thinkingConfig: convertProtoToThinkingConfig(info.thinkingConfig),
|
||||
supportsGlobalEndpoint: info.supportsGlobalEndpoint,
|
||||
tiers: info.tiers.length > 0 ? info.tiers : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
// Convert application LiteLLMModelInfo to proto LiteLLMModelInfo
|
||||
function convertLiteLLMModelInfoToProto(info: AppLiteLLMModelInfo | undefined): LiteLLMModelInfo | undefined {
|
||||
if (!info) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
maxTokens: info.maxTokens,
|
||||
contextWindow: info.contextWindow,
|
||||
supportsImages: info.supportsImages,
|
||||
supportsPromptCache: info.supportsPromptCache ?? false,
|
||||
inputPrice: info.inputPrice,
|
||||
outputPrice: info.outputPrice,
|
||||
thinkingConfig: convertThinkingConfigToProto(info.thinkingConfig),
|
||||
supportsGlobalEndpoint: info.supportsGlobalEndpoint,
|
||||
cacheWritesPrice: info.cacheWritesPrice,
|
||||
cacheReadsPrice: info.cacheReadsPrice,
|
||||
description: info.description,
|
||||
tiers: info.tiers || [],
|
||||
temperature: info.temperature,
|
||||
}
|
||||
}
|
||||
|
||||
// Convert proto LiteLLMModelInfo to application LiteLLMModelInfo
|
||||
function convertProtoToLiteLLMModelInfo(info: LiteLLMModelInfo | undefined): AppLiteLLMModelInfo | undefined {
|
||||
if (!info) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
maxTokens: info.maxTokens,
|
||||
contextWindow: info.contextWindow,
|
||||
supportsImages: info.supportsImages,
|
||||
supportsPromptCache: info.supportsPromptCache,
|
||||
inputPrice: info.inputPrice,
|
||||
outputPrice: info.outputPrice,
|
||||
thinkingConfig: convertProtoToThinkingConfig(info.thinkingConfig),
|
||||
supportsGlobalEndpoint: info.supportsGlobalEndpoint,
|
||||
cacheWritesPrice: info.cacheWritesPrice,
|
||||
cacheReadsPrice: info.cacheReadsPrice,
|
||||
description: info.description,
|
||||
tiers: info.tiers.length > 0 ? info.tiers : undefined,
|
||||
temperature: info.temperature,
|
||||
}
|
||||
}
|
||||
|
||||
// Convert application OpenAiCompatibleModelInfo to proto OpenAiCompatibleModelInfo
|
||||
function convertOpenAiCompatibleModelInfoToProto(
|
||||
info: AppOpenAiCompatibleModelInfo | undefined,
|
||||
): OpenAiCompatibleModelInfo | undefined {
|
||||
if (!info) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
maxTokens: info.maxTokens,
|
||||
contextWindow: info.contextWindow,
|
||||
supportsImages: info.supportsImages,
|
||||
supportsPromptCache: info.supportsPromptCache ?? false,
|
||||
inputPrice: info.inputPrice,
|
||||
outputPrice: info.outputPrice,
|
||||
thinkingConfig: convertThinkingConfigToProto(info.thinkingConfig),
|
||||
supportsGlobalEndpoint: info.supportsGlobalEndpoint,
|
||||
cacheWritesPrice: info.cacheWritesPrice,
|
||||
cacheReadsPrice: info.cacheReadsPrice,
|
||||
description: info.description,
|
||||
tiers: info.tiers || [],
|
||||
temperature: info.temperature,
|
||||
isR1FormatRequired: info.isR1FormatRequired,
|
||||
}
|
||||
}
|
||||
|
||||
// Convert proto OpenAiCompatibleModelInfo to application OpenAiCompatibleModelInfo
|
||||
function convertProtoToOpenAiCompatibleModelInfo(
|
||||
info: OpenAiCompatibleModelInfo | undefined,
|
||||
): AppOpenAiCompatibleModelInfo | undefined {
|
||||
if (!info) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
maxTokens: info.maxTokens,
|
||||
contextWindow: info.contextWindow,
|
||||
supportsImages: info.supportsImages,
|
||||
supportsPromptCache: info.supportsPromptCache,
|
||||
inputPrice: info.inputPrice,
|
||||
outputPrice: info.outputPrice,
|
||||
thinkingConfig: convertProtoToThinkingConfig(info.thinkingConfig),
|
||||
supportsGlobalEndpoint: info.supportsGlobalEndpoint,
|
||||
cacheWritesPrice: info.cacheWritesPrice,
|
||||
cacheReadsPrice: info.cacheReadsPrice,
|
||||
description: info.description,
|
||||
tiers: info.tiers.length > 0 ? info.tiers : undefined,
|
||||
temperature: info.temperature,
|
||||
isR1FormatRequired: info.isR1FormatRequired,
|
||||
}
|
||||
}
|
||||
|
||||
// Convert application ApiProvider to proto ApiProvider
|
||||
function convertApiProviderToProto(provider: string | undefined): ProtoApiProvider {
|
||||
switch (provider) {
|
||||
case "anthropic":
|
||||
return ProtoApiProvider.ANTHROPIC
|
||||
case "openrouter":
|
||||
return ProtoApiProvider.OPENROUTER
|
||||
case "bedrock":
|
||||
return ProtoApiProvider.BEDROCK
|
||||
case "vertex":
|
||||
return ProtoApiProvider.VERTEX
|
||||
case "openai":
|
||||
return ProtoApiProvider.OPENAI
|
||||
case "ollama":
|
||||
return ProtoApiProvider.OLLAMA
|
||||
case "lmstudio":
|
||||
return ProtoApiProvider.LMSTUDIO
|
||||
case "gemini":
|
||||
return ProtoApiProvider.GEMINI
|
||||
case "openai-native":
|
||||
return ProtoApiProvider.OPENAI_NATIVE
|
||||
case "requesty":
|
||||
return ProtoApiProvider.REQUESTY
|
||||
case "together":
|
||||
return ProtoApiProvider.TOGETHER
|
||||
case "deepseek":
|
||||
return ProtoApiProvider.DEEPSEEK
|
||||
case "qwen":
|
||||
return ProtoApiProvider.QWEN
|
||||
case "doubao":
|
||||
return ProtoApiProvider.DOUBAO
|
||||
case "mistral":
|
||||
return ProtoApiProvider.MISTRAL
|
||||
case "vscode-lm":
|
||||
return ProtoApiProvider.VSCODE_LM
|
||||
case "cline":
|
||||
return ProtoApiProvider.CLINE
|
||||
case "litellm":
|
||||
return ProtoApiProvider.LITELLM
|
||||
case "nebius":
|
||||
return ProtoApiProvider.NEBIUS
|
||||
case "fireworks":
|
||||
return ProtoApiProvider.FIREWORKS
|
||||
case "asksage":
|
||||
return ProtoApiProvider.ASKSAGE
|
||||
case "xai":
|
||||
return ProtoApiProvider.XAI
|
||||
case "sambanova":
|
||||
return ProtoApiProvider.SAMBANOVA
|
||||
case "cerebras":
|
||||
return ProtoApiProvider.CEREBRAS
|
||||
case "sapaicore":
|
||||
return ProtoApiProvider.SAPAICORE
|
||||
case "claude-code":
|
||||
return ProtoApiProvider.CLAUDE_CODE
|
||||
default:
|
||||
return ProtoApiProvider.ANTHROPIC
|
||||
}
|
||||
}
|
||||
|
||||
// Convert proto ApiProvider to application ApiProvider
|
||||
function convertProtoToApiProvider(provider: ProtoApiProvider): ApiProvider {
|
||||
switch (provider) {
|
||||
case ProtoApiProvider.ANTHROPIC:
|
||||
return "anthropic"
|
||||
case ProtoApiProvider.OPENROUTER:
|
||||
return "openrouter"
|
||||
case ProtoApiProvider.BEDROCK:
|
||||
return "bedrock"
|
||||
case ProtoApiProvider.VERTEX:
|
||||
return "vertex"
|
||||
case ProtoApiProvider.OPENAI:
|
||||
return "openai"
|
||||
case ProtoApiProvider.OLLAMA:
|
||||
return "ollama"
|
||||
case ProtoApiProvider.LMSTUDIO:
|
||||
return "lmstudio"
|
||||
case ProtoApiProvider.GEMINI:
|
||||
return "gemini"
|
||||
case ProtoApiProvider.OPENAI_NATIVE:
|
||||
return "openai-native"
|
||||
case ProtoApiProvider.REQUESTY:
|
||||
return "requesty"
|
||||
case ProtoApiProvider.TOGETHER:
|
||||
return "together"
|
||||
case ProtoApiProvider.DEEPSEEK:
|
||||
return "deepseek"
|
||||
case ProtoApiProvider.QWEN:
|
||||
return "qwen"
|
||||
case ProtoApiProvider.DOUBAO:
|
||||
return "doubao"
|
||||
case ProtoApiProvider.MISTRAL:
|
||||
return "mistral"
|
||||
case ProtoApiProvider.VSCODE_LM:
|
||||
return "vscode-lm"
|
||||
case ProtoApiProvider.CLINE:
|
||||
return "cline"
|
||||
case ProtoApiProvider.LITELLM:
|
||||
return "litellm"
|
||||
case ProtoApiProvider.NEBIUS:
|
||||
return "nebius"
|
||||
case ProtoApiProvider.FIREWORKS:
|
||||
return "fireworks"
|
||||
case ProtoApiProvider.ASKSAGE:
|
||||
return "asksage"
|
||||
case ProtoApiProvider.XAI:
|
||||
return "xai"
|
||||
case ProtoApiProvider.SAMBANOVA:
|
||||
return "sambanova"
|
||||
case ProtoApiProvider.CEREBRAS:
|
||||
return "cerebras"
|
||||
case ProtoApiProvider.SAPAICORE:
|
||||
return "sapaicore"
|
||||
case ProtoApiProvider.CLAUDE_CODE:
|
||||
return "claude-code"
|
||||
default:
|
||||
return "anthropic"
|
||||
}
|
||||
}
|
||||
|
||||
// Converts application ApiConfiguration to proto ApiConfiguration
|
||||
export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoApiConfiguration {
|
||||
return {
|
||||
apiModelId: config.apiModelId,
|
||||
apiKey: config.apiKey,
|
||||
clineApiKey: config.clineApiKey,
|
||||
taskId: config.taskId,
|
||||
liteLlmBaseUrl: config.liteLlmBaseUrl,
|
||||
liteLlmModelId: config.liteLlmModelId,
|
||||
liteLlmApiKey: config.liteLlmApiKey,
|
||||
liteLlmUsePromptCache: config.liteLlmUsePromptCache,
|
||||
openAiHeaders: config.openAiHeaders || {},
|
||||
liteLlmModelInfo: convertLiteLLMModelInfoToProto(config.liteLlmModelInfo),
|
||||
anthropicBaseUrl: config.anthropicBaseUrl,
|
||||
openRouterApiKey: config.openRouterApiKey,
|
||||
openRouterModelId: config.openRouterModelId,
|
||||
openRouterModelInfo: convertModelInfoToProtoOpenRouter(config.openRouterModelInfo),
|
||||
openRouterProviderSorting: config.openRouterProviderSorting,
|
||||
awsAccessKey: config.awsAccessKey,
|
||||
awsSecretKey: config.awsSecretKey,
|
||||
awsSessionToken: config.awsSessionToken,
|
||||
awsRegion: config.awsRegion,
|
||||
awsUseCrossRegionInference: config.awsUseCrossRegionInference,
|
||||
awsBedrockUsePromptCache: config.awsBedrockUsePromptCache,
|
||||
awsUseProfile: config.awsUseProfile,
|
||||
awsProfile: config.awsProfile,
|
||||
awsBedrockEndpoint: config.awsBedrockEndpoint,
|
||||
awsBedrockCustomSelected: config.awsBedrockCustomSelected,
|
||||
awsBedrockCustomModelBaseId: config.awsBedrockCustomModelBaseId as string | undefined,
|
||||
vertexProjectId: config.vertexProjectId,
|
||||
vertexRegion: config.vertexRegion,
|
||||
openAiBaseUrl: config.openAiBaseUrl,
|
||||
openAiApiKey: config.openAiApiKey,
|
||||
openAiModelId: config.openAiModelId,
|
||||
openAiModelInfo: convertOpenAiCompatibleModelInfoToProto(config.openAiModelInfo),
|
||||
ollamaModelId: config.ollamaModelId,
|
||||
ollamaBaseUrl: config.ollamaBaseUrl,
|
||||
ollamaApiOptionsCtxNum: config.ollamaApiOptionsCtxNum,
|
||||
lmStudioModelId: config.lmStudioModelId,
|
||||
lmStudioBaseUrl: config.lmStudioBaseUrl,
|
||||
geminiApiKey: config.geminiApiKey,
|
||||
geminiBaseUrl: config.geminiBaseUrl,
|
||||
openAiNativeApiKey: config.openAiNativeApiKey,
|
||||
deepSeekApiKey: config.deepSeekApiKey,
|
||||
requestyApiKey: config.requestyApiKey,
|
||||
requestyModelId: config.requestyModelId,
|
||||
requestyModelInfo: convertModelInfoToProtoOpenRouter(config.requestyModelInfo),
|
||||
togetherApiKey: config.togetherApiKey,
|
||||
togetherModelId: config.togetherModelId,
|
||||
fireworksApiKey: config.fireworksApiKey,
|
||||
fireworksModelId: config.fireworksModelId,
|
||||
fireworksModelMaxCompletionTokens: config.fireworksModelMaxCompletionTokens,
|
||||
fireworksModelMaxTokens: config.fireworksModelMaxTokens,
|
||||
qwenApiKey: config.qwenApiKey,
|
||||
doubaoApiKey: config.doubaoApiKey,
|
||||
mistralApiKey: config.mistralApiKey,
|
||||
azureApiVersion: config.azureApiVersion,
|
||||
vsCodeLmModelSelector: config.vsCodeLmModelSelector,
|
||||
qwenApiLine: config.qwenApiLine,
|
||||
nebiusApiKey: config.nebiusApiKey,
|
||||
asksageApiUrl: config.asksageApiUrl,
|
||||
asksageApiKey: config.asksageApiKey,
|
||||
xaiApiKey: config.xaiApiKey,
|
||||
thinkingBudgetTokens: config.thinkingBudgetTokens,
|
||||
reasoningEffort: config.reasoningEffort,
|
||||
sambanovaApiKey: config.sambanovaApiKey,
|
||||
cerebrasApiKey: config.cerebrasApiKey,
|
||||
requestTimeoutMs: config.requestTimeoutMs,
|
||||
apiProvider: config.apiProvider ? convertApiProviderToProto(config.apiProvider) : undefined,
|
||||
favoritedModelIds: config.favoritedModelIds || [],
|
||||
sapAiCoreClientId: config.sapAiCoreClientId,
|
||||
sapAiCoreClientSecret: config.sapAiCoreClientSecret,
|
||||
sapAiResourceGroup: config.sapAiResourceGroup,
|
||||
sapAiCoreTokenUrl: config.sapAiCoreTokenUrl,
|
||||
sapAiCoreBaseUrl: config.sapAiCoreBaseUrl,
|
||||
claudeCodePath: config.claudeCodePath,
|
||||
}
|
||||
}
|
||||
|
||||
// Converts proto ApiConfiguration to application ApiConfiguration
|
||||
export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguration): ApiConfiguration {
|
||||
return {
|
||||
apiModelId: protoConfig.apiModelId,
|
||||
apiKey: protoConfig.apiKey,
|
||||
clineApiKey: protoConfig.clineApiKey,
|
||||
taskId: protoConfig.taskId,
|
||||
liteLlmBaseUrl: protoConfig.liteLlmBaseUrl,
|
||||
liteLlmModelId: protoConfig.liteLlmModelId,
|
||||
liteLlmApiKey: protoConfig.liteLlmApiKey,
|
||||
liteLlmUsePromptCache: protoConfig.liteLlmUsePromptCache,
|
||||
openAiHeaders: Object.keys(protoConfig.openAiHeaders).length > 0 ? protoConfig.openAiHeaders : undefined,
|
||||
liteLlmModelInfo: convertProtoToLiteLLMModelInfo(protoConfig.liteLlmModelInfo),
|
||||
anthropicBaseUrl: protoConfig.anthropicBaseUrl,
|
||||
openRouterApiKey: protoConfig.openRouterApiKey,
|
||||
openRouterModelId: protoConfig.openRouterModelId,
|
||||
openRouterModelInfo: convertProtoToModelInfo(protoConfig.openRouterModelInfo),
|
||||
openRouterProviderSorting: protoConfig.openRouterProviderSorting,
|
||||
awsAccessKey: protoConfig.awsAccessKey,
|
||||
awsSecretKey: protoConfig.awsSecretKey,
|
||||
awsSessionToken: protoConfig.awsSessionToken,
|
||||
awsRegion: protoConfig.awsRegion,
|
||||
awsUseCrossRegionInference: protoConfig.awsUseCrossRegionInference,
|
||||
awsBedrockUsePromptCache: protoConfig.awsBedrockUsePromptCache,
|
||||
awsUseProfile: protoConfig.awsUseProfile,
|
||||
awsProfile: protoConfig.awsProfile,
|
||||
awsBedrockEndpoint: protoConfig.awsBedrockEndpoint,
|
||||
awsBedrockCustomSelected: protoConfig.awsBedrockCustomSelected,
|
||||
awsBedrockCustomModelBaseId: protoConfig.awsBedrockCustomModelBaseId as BedrockModelId | undefined,
|
||||
vertexProjectId: protoConfig.vertexProjectId,
|
||||
vertexRegion: protoConfig.vertexRegion,
|
||||
openAiBaseUrl: protoConfig.openAiBaseUrl,
|
||||
openAiApiKey: protoConfig.openAiApiKey,
|
||||
openAiModelId: protoConfig.openAiModelId,
|
||||
openAiModelInfo: convertProtoToOpenAiCompatibleModelInfo(protoConfig.openAiModelInfo),
|
||||
ollamaModelId: protoConfig.ollamaModelId,
|
||||
ollamaBaseUrl: protoConfig.ollamaBaseUrl,
|
||||
ollamaApiOptionsCtxNum: protoConfig.ollamaApiOptionsCtxNum,
|
||||
lmStudioModelId: protoConfig.lmStudioModelId,
|
||||
lmStudioBaseUrl: protoConfig.lmStudioBaseUrl,
|
||||
geminiApiKey: protoConfig.geminiApiKey,
|
||||
geminiBaseUrl: protoConfig.geminiBaseUrl,
|
||||
openAiNativeApiKey: protoConfig.openAiNativeApiKey,
|
||||
deepSeekApiKey: protoConfig.deepSeekApiKey,
|
||||
requestyApiKey: protoConfig.requestyApiKey,
|
||||
requestyModelId: protoConfig.requestyModelId,
|
||||
requestyModelInfo: convertProtoToModelInfo(protoConfig.requestyModelInfo),
|
||||
togetherApiKey: protoConfig.togetherApiKey,
|
||||
togetherModelId: protoConfig.togetherModelId,
|
||||
fireworksApiKey: protoConfig.fireworksApiKey,
|
||||
fireworksModelId: protoConfig.fireworksModelId,
|
||||
fireworksModelMaxCompletionTokens: protoConfig.fireworksModelMaxCompletionTokens,
|
||||
fireworksModelMaxTokens: protoConfig.fireworksModelMaxTokens,
|
||||
qwenApiKey: protoConfig.qwenApiKey,
|
||||
doubaoApiKey: protoConfig.doubaoApiKey,
|
||||
mistralApiKey: protoConfig.mistralApiKey,
|
||||
azureApiVersion: protoConfig.azureApiVersion,
|
||||
vsCodeLmModelSelector: protoConfig.vsCodeLmModelSelector,
|
||||
qwenApiLine: protoConfig.qwenApiLine,
|
||||
nebiusApiKey: protoConfig.nebiusApiKey,
|
||||
asksageApiUrl: protoConfig.asksageApiUrl,
|
||||
asksageApiKey: protoConfig.asksageApiKey,
|
||||
xaiApiKey: protoConfig.xaiApiKey,
|
||||
thinkingBudgetTokens: protoConfig.thinkingBudgetTokens,
|
||||
reasoningEffort: protoConfig.reasoningEffort,
|
||||
sambanovaApiKey: protoConfig.sambanovaApiKey,
|
||||
cerebrasApiKey: protoConfig.cerebrasApiKey,
|
||||
requestTimeoutMs: protoConfig.requestTimeoutMs,
|
||||
apiProvider: protoConfig.apiProvider !== undefined ? convertProtoToApiProvider(protoConfig.apiProvider) : undefined,
|
||||
favoritedModelIds: protoConfig.favoritedModelIds.length > 0 ? protoConfig.favoritedModelIds : undefined,
|
||||
sapAiCoreClientId: protoConfig.sapAiCoreClientId,
|
||||
sapAiCoreClientSecret: protoConfig.sapAiCoreClientSecret,
|
||||
sapAiResourceGroup: protoConfig.sapAiResourceGroup,
|
||||
sapAiCoreTokenUrl: protoConfig.sapAiCoreTokenUrl,
|
||||
sapAiCoreBaseUrl: protoConfig.sapAiCoreBaseUrl,
|
||||
claudeCodePath: protoConfig.claudeCodePath,
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LanguageModelChatSelector } from "../../proto/models"
|
||||
import { VsCodeLmModel } from "../../proto/models"
|
||||
|
||||
/**
|
||||
* Represents a VS Code language model in the native VS Code format
|
||||
@@ -13,7 +13,7 @@ export interface VsCodeNativeModel {
|
||||
/**
|
||||
* Converts VS Code native model format to protobuf format
|
||||
*/
|
||||
export function convertVsCodeNativeModelsToProtoModels(models: VsCodeNativeModel[]): LanguageModelChatSelector[] {
|
||||
export function convertVsCodeNativeModelsToProtoModels(models: VsCodeNativeModel[]): VsCodeLmModel[] {
|
||||
return (models || []).map((model) => ({
|
||||
vendor: model.vendor || "",
|
||||
family: model.family || "",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiConfiguration, ApiProvider, BedrockModelId } from "@shared/api"
|
||||
import { ApiConfiguration } from "@shared/api"
|
||||
import { ChatSettings } from "@shared/ChatSettings"
|
||||
import {
|
||||
ApiConfiguration as ProtoApiConfiguration,
|
||||
@@ -102,13 +102,6 @@ export function convertApiConfigurationToProtoApiConfiguration(config: ApiConfig
|
||||
// OpenRouter specific
|
||||
openrouterProviderSorting: config.openRouterProviderSorting,
|
||||
|
||||
// SAP AI Core specific
|
||||
sapAiCoreClientId: config.sapAiCoreClientId,
|
||||
sapAiCoreClientSecret: config.sapAiCoreClientSecret,
|
||||
sapAiCoreBaseUrl: config.sapAiCoreBaseUrl,
|
||||
sapAiCoreTokenUrl: config.sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup: config.sapAiResourceGroup,
|
||||
|
||||
// Complex objects stored as JSON strings
|
||||
vscodeLmModelSelector: config.vsCodeLmModelSelector ? JSON.stringify(config.vsCodeLmModelSelector) : undefined,
|
||||
openrouterModelInfo: config.openRouterModelInfo ? JSON.stringify(config.openRouterModelInfo) : undefined,
|
||||
@@ -117,9 +110,6 @@ export function convertApiConfigurationToProtoApiConfiguration(config: ApiConfig
|
||||
litellmModelInfo: config.liteLlmModelInfo ? JSON.stringify(config.liteLlmModelInfo) : undefined,
|
||||
openaiHeaders: config.openAiHeaders ? JSON.stringify(config.openAiHeaders) : undefined,
|
||||
|
||||
// Claude Code specific
|
||||
claudeCodePath: config.claudeCodePath,
|
||||
|
||||
// Arrays
|
||||
favoritedModelIds: config.favoritedModelIds || [],
|
||||
})
|
||||
@@ -132,7 +122,7 @@ export function convertProtoApiConfigurationToApiConfiguration(protoConfig: Prot
|
||||
// eslint-disable-next-line eslint-rules/no-protobuf-object-literals
|
||||
const config: ApiConfiguration = {
|
||||
// Core API fields
|
||||
apiProvider: protoConfig.apiProvider as ApiProvider,
|
||||
apiProvider: protoConfig.apiProvider as any,
|
||||
apiModelId: protoConfig.apiModelId,
|
||||
apiKey: protoConfig.apiKey,
|
||||
|
||||
@@ -168,7 +158,7 @@ export function convertProtoApiConfigurationToApiConfiguration(protoConfig: Prot
|
||||
|
||||
// AWS Bedrock fields
|
||||
awsBedrockCustomSelected: protoConfig.awsBedrockCustomSelected,
|
||||
awsBedrockCustomModelBaseId: protoConfig.awsBedrockCustomModelBaseId as BedrockModelId | undefined,
|
||||
awsBedrockCustomModelBaseId: protoConfig.awsBedrockCustomModelBaseId as any,
|
||||
awsAccessKey: protoConfig.awsAccessKey,
|
||||
awsSecretKey: protoConfig.awsSecretKey,
|
||||
awsSessionToken: protoConfig.awsSessionToken,
|
||||
@@ -218,16 +208,6 @@ export function convertProtoApiConfigurationToApiConfiguration(protoConfig: Prot
|
||||
// OpenRouter specific
|
||||
openRouterProviderSorting: protoConfig.openrouterProviderSorting,
|
||||
|
||||
// SAP AI Core specific
|
||||
sapAiCoreClientId: protoConfig.sapAiCoreClientId,
|
||||
sapAiCoreClientSecret: protoConfig.sapAiCoreClientSecret,
|
||||
sapAiCoreBaseUrl: protoConfig.sapAiCoreBaseUrl,
|
||||
sapAiCoreTokenUrl: protoConfig.sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup: protoConfig.sapAiResourceGroup,
|
||||
|
||||
// Claude Code specific
|
||||
claudeCodePath: protoConfig.claudeCodePath,
|
||||
|
||||
// Arrays
|
||||
favoritedModelIds: protoConfig.favoritedModelIds || [],
|
||||
}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { Channel, createChannel, createClient } from "nice-grpc"
|
||||
import * as host from "@generated/nice-grpc/index.host"
|
||||
|
||||
/**
|
||||
* Singleton class to hold the gRPC clients for the host bridge. The clients should be re-used to avoid
|
||||
* creating a new TCP connection every time a rpc is made.
|
||||
*/
|
||||
class HostBridgeClientManager {
|
||||
private static instance: HostBridgeClientManager | null
|
||||
private channel: Channel
|
||||
uriClient: host.UriServiceClient
|
||||
//watchClient: host.WatchServiceClient
|
||||
|
||||
private constructor() {
|
||||
const address = process.env.HOST_BRIDGE_ADDRESS || "localhost:50052"
|
||||
this.channel = createChannel(address)
|
||||
this.uriClient = createClient(host.UriServiceDefinition, this.channel)
|
||||
//this.watchClient =
|
||||
}
|
||||
|
||||
public static getInstance(): HostBridgeClientManager {
|
||||
if (!HostBridgeClientManager.instance) {
|
||||
HostBridgeClientManager.instance = new HostBridgeClientManager()
|
||||
}
|
||||
return HostBridgeClientManager.instance
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
this.channel.close()
|
||||
HostBridgeClientManager.instance = null
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(sjf) Replace this with nice-grpc client.
|
||||
const StubWatchServiceClient = {
|
||||
subscribeToFile: function (
|
||||
_r: host.SubscribeToFileRequest,
|
||||
_h: {
|
||||
onResponse?: (response: { type: host.FileChangeEvent_ChangeType }) => void | Promise<void>
|
||||
onError?: (error: any) => void
|
||||
onComplete?: () => void
|
||||
},
|
||||
) {
|
||||
throw Error("Unimplemented")
|
||||
},
|
||||
}
|
||||
|
||||
const clientManager = HostBridgeClientManager.getInstance()
|
||||
|
||||
export const UriServiceClient = clientManager.uriClient
|
||||
export const WatchServiceClient = StubWatchServiceClient
|
||||
@@ -5,14 +5,12 @@ import * as health from "grpc-health-check"
|
||||
import { activate } from "../extension"
|
||||
import { Controller } from "../core/controller"
|
||||
import { extensionContext, outputChannel, postMessage } from "./vscode-context"
|
||||
import { getPackageDefinition, log } from "./utils"
|
||||
import { packageDefinition, proto, log, camelToSnakeCase, snakeToCamelCase } from "./utils"
|
||||
import { GrpcHandler, GrpcStreamingResponseHandler } from "./grpc-types"
|
||||
import { addProtobusServices } from "@/generated/standalone/server-setup"
|
||||
import { addServices } from "./server-setup"
|
||||
import { StreamingResponseHandler } from "@/core/controller/grpc-handler"
|
||||
import { UriServiceClient } from "@/hosts/host-bridge-client"
|
||||
import { StringRequest } from "@/shared/proto/common"
|
||||
|
||||
async function main() {
|
||||
function main() {
|
||||
log("Starting service...")
|
||||
|
||||
activate(extensionContext)
|
||||
@@ -24,10 +22,10 @@ async function main() {
|
||||
healthImpl.addToServer(server)
|
||||
|
||||
// Add all the handlers for the ProtoBus services to the server.
|
||||
addProtobusServices(server, controller, wrapHandler, wrapStreamingResponseHandler)
|
||||
addServices(server, proto, controller, wrapHandler, wrapStreamingResponseHandler)
|
||||
|
||||
// Set up reflection.
|
||||
const reflection = new ReflectionService(getPackageDefinition())
|
||||
const reflection = new ReflectionService(packageDefinition)
|
||||
reflection.addToServer(server)
|
||||
|
||||
// Start the server.
|
||||
@@ -60,8 +58,10 @@ function wrapHandler<TRequest, TResponse>(
|
||||
return async (call: grpc.ServerUnaryCall<TRequest, TResponse>, callback: grpc.sendUnaryData<TResponse>) => {
|
||||
try {
|
||||
log(`gRPC request: ${call.getPath()}`)
|
||||
const result = await handler(controller, call.request)
|
||||
callback(null, result)
|
||||
const result = await handler(controller, snakeToCamelCase(call.request))
|
||||
// The grpc-js serializer expects the proto message to be in the same
|
||||
// case as the proto file. This is a work around until we find a solution.
|
||||
callback(null, camelToSnakeCase(result))
|
||||
} catch (err: any) {
|
||||
log(`gRPC handler error: ${call.getPath()}\n${err.stack}`)
|
||||
callback({
|
||||
@@ -83,7 +83,9 @@ function wrapStreamingResponseHandler<TRequest, TResponse>(
|
||||
|
||||
const responseHandler: StreamingResponseHandler = (response, isLast, sequenceNumber) => {
|
||||
try {
|
||||
call.write(response) // Use a bound version of call.write to maintain proper 'this' context
|
||||
// The grpc-js serializer expects the proto message to be in the same
|
||||
// case as the proto file. This is a work around until we find a solution.
|
||||
call.write(camelToSnakeCase(response)) // Use a bound version of call.write to maintain proper 'this' context
|
||||
|
||||
if (isLast === true) {
|
||||
log(`Closing stream for ${requestId}`)
|
||||
@@ -94,7 +96,7 @@ function wrapStreamingResponseHandler<TRequest, TResponse>(
|
||||
return Promise.reject(error)
|
||||
}
|
||||
}
|
||||
await handler(controller, call.request, responseHandler, requestId)
|
||||
await handler(controller, snakeToCamelCase(call.request), responseHandler, requestId)
|
||||
} catch (err: any) {
|
||||
log(`gRPC handler error: ${call.getPath()}\n${err.stack}`)
|
||||
call.destroy({
|
||||
|
||||
+54
-8
@@ -8,12 +8,58 @@ const log = (...args: unknown[]) => {
|
||||
console.log(`[${timestamp}]`, "#bot.cline.server.ts", ...args)
|
||||
}
|
||||
|
||||
function getPackageDefinition() {
|
||||
// Load service definitions.
|
||||
const descriptorSet = fs.readFileSync("proto/descriptor_set.pb")
|
||||
const clineDef = protoLoader.loadFileDescriptorSetFromBuffer(descriptorSet)
|
||||
const healthDef = protoLoader.loadSync(health.protoPath)
|
||||
const packageDefinition = { ...clineDef, ...healthDef }
|
||||
return packageDefinition
|
||||
// Load service definitions.
|
||||
const descriptorSet = fs.readFileSync("proto/descriptor_set.pb")
|
||||
const clineDef = protoLoader.loadFileDescriptorSetFromBuffer(descriptorSet)
|
||||
const healthDef = protoLoader.loadSync(health.protoPath)
|
||||
const packageDefinition = { ...clineDef, ...healthDef }
|
||||
const proto = grpc.loadPackageDefinition(packageDefinition) as unknown
|
||||
|
||||
// Helper function to convert camelCase to snake_case
|
||||
function camelToSnakeCase(obj: any): any {
|
||||
if (obj === null || typeof obj !== "object") {
|
||||
return obj
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(camelToSnakeCase)
|
||||
}
|
||||
|
||||
return Object.keys(obj).reduce((acc: any, key: string) => {
|
||||
// Convert key from camelCase to snake_case
|
||||
const snakeKey = key
|
||||
.replace(/([A-Z])/g, "_$1")
|
||||
.replace(/^_+/, "")
|
||||
.toLowerCase()
|
||||
|
||||
// Convert value recursively if it's an object
|
||||
const value = obj[key]
|
||||
acc[snakeKey] = camelToSnakeCase(value)
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
export { getPackageDefinition, log }
|
||||
|
||||
// Helper function to convert snake_case to camelCase
|
||||
function snakeToCamelCase(obj: any): any {
|
||||
if (obj === null || typeof obj !== "object") {
|
||||
return obj
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(snakeToCamelCase)
|
||||
}
|
||||
|
||||
return Object.keys(obj).reduce((acc: any, key: string) => {
|
||||
// Convert key from snake_case to camelCase
|
||||
const camelKey = key.replace(/_([a-z0-9])/g, (_, char) => char.toUpperCase())
|
||||
|
||||
// Convert value recursively if it's an object
|
||||
const value = obj[key]
|
||||
acc[camelKey] = snakeToCamelCase(value)
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
export { packageDefinition, proto, log, camelToSnakeCase, snakeToCamelCase }
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { ApiHandler } from "@api/index"
|
||||
|
||||
export function isClaude4ModelFamily(api: ApiHandler): boolean {
|
||||
const model = api.getModel()
|
||||
const modelId = model.id
|
||||
return modelId.includes("sonnet-4") || modelId.includes("opus-4")
|
||||
}
|
||||
+1
-107
@@ -179,113 +179,7 @@ function getShellFromEnv(): string | null {
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
// 4) Terminal Profile Interface and Utilities
|
||||
// -----------------------------------------------------
|
||||
|
||||
import { TerminalProfile } from "../shared/proto/state"
|
||||
|
||||
/** Gets available terminal profiles for the current platform */
|
||||
export function getAvailableTerminalProfiles(): TerminalProfile[] {
|
||||
const profiles: TerminalProfile[] = [
|
||||
{
|
||||
id: "default",
|
||||
name: "Default",
|
||||
description: "Use VSCode's default terminal configuration",
|
||||
},
|
||||
]
|
||||
|
||||
if (process.platform === "win32") {
|
||||
// Windows terminal profiles
|
||||
profiles.push(
|
||||
{
|
||||
id: "powershell-7",
|
||||
name: "PowerShell 7",
|
||||
path: SHELL_PATHS.POWERSHELL_7,
|
||||
description: "PowerShell 7 (pwsh.exe)",
|
||||
},
|
||||
{
|
||||
id: "powershell-legacy",
|
||||
name: "Windows PowerShell",
|
||||
path: SHELL_PATHS.POWERSHELL_LEGACY,
|
||||
description: "Windows PowerShell 5.x",
|
||||
},
|
||||
{
|
||||
id: "cmd",
|
||||
name: "Command Prompt",
|
||||
path: SHELL_PATHS.CMD,
|
||||
description: "Command Prompt (cmd.exe)",
|
||||
},
|
||||
{
|
||||
id: "wsl-bash",
|
||||
name: "WSL Bash",
|
||||
path: SHELL_PATHS.WSL_BASH,
|
||||
description: "Windows Subsystem for Linux Bash",
|
||||
},
|
||||
)
|
||||
} else if (process.platform === "darwin") {
|
||||
// macOS terminal profiles
|
||||
profiles.push(
|
||||
{
|
||||
id: "zsh",
|
||||
name: "zsh",
|
||||
path: SHELL_PATHS.ZSH,
|
||||
description: "Z shell (default on macOS)",
|
||||
},
|
||||
{
|
||||
id: "bash",
|
||||
name: "bash",
|
||||
path: SHELL_PATHS.BASH,
|
||||
description: "Bourne Again Shell",
|
||||
},
|
||||
)
|
||||
} else if (process.platform === "linux") {
|
||||
// Linux terminal profiles
|
||||
profiles.push(
|
||||
{
|
||||
id: "bash",
|
||||
name: "bash",
|
||||
path: SHELL_PATHS.BASH,
|
||||
description: "Bourne Again Shell (default on most Linux)",
|
||||
},
|
||||
{
|
||||
id: "zsh",
|
||||
name: "zsh",
|
||||
path: SHELL_PATHS.ZSH,
|
||||
description: "Z shell",
|
||||
},
|
||||
{
|
||||
id: "dash",
|
||||
name: "dash",
|
||||
path: SHELL_PATHS.DASH,
|
||||
description: "Debian Almquist Shell",
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return profiles
|
||||
}
|
||||
|
||||
/** Gets the shell path for a specific terminal profile */
|
||||
export function getShellForProfile(profileId: string): string {
|
||||
// If it's the default profile, use the existing getShell() logic
|
||||
if (profileId === "default") {
|
||||
return getShell()
|
||||
}
|
||||
|
||||
// Find the profile
|
||||
const profiles = getAvailableTerminalProfiles()
|
||||
const profile = profiles.find((p) => p.id === profileId)
|
||||
|
||||
if (profile?.path) {
|
||||
return profile.path
|
||||
}
|
||||
|
||||
// Fallback to default shell if profile not found
|
||||
return getShell()
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
// 5) Publicly Exposed Shell Getter
|
||||
// 4) Publicly Exposed Shell Getter
|
||||
// -----------------------------------------------------
|
||||
|
||||
export function getShell(): string {
|
||||
|
||||
+2
-3
@@ -24,13 +24,12 @@
|
||||
"@/*": ["src/*"],
|
||||
"@api/*": ["src/api/*"],
|
||||
"@core/*": ["src/core/*"],
|
||||
"@generated/*": ["src/generated/*"],
|
||||
"@hosts/*": ["src/hosts/*"],
|
||||
"@integrations/*": ["src/integrations/*"],
|
||||
"@packages/*": ["src/packages/*"],
|
||||
"@services/*": ["src/services/*"],
|
||||
"@shared/*": ["src/shared/*"],
|
||||
"@utils/*": ["src/utils/*"]
|
||||
"@utils/*": ["src/utils/*"],
|
||||
"@packages/*": ["src/packages/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*", "scripts/**/*"],
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { VSCodeBadge, VSCodeButton, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
|
||||
import deepEqual from "fast-deep-equal"
|
||||
import React, { memo, MouseEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import styled from "styled-components"
|
||||
import { useEvent, useSize } from "react-use"
|
||||
|
||||
import CreditLimitError from "@/components/chat/CreditLimitError"
|
||||
import { OptionsButtons } from "@/components/chat/OptionsButtons"
|
||||
@@ -11,8 +9,6 @@ import { CheckmarkControl } from "@/components/common/CheckmarkControl"
|
||||
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
|
||||
import MarkdownBlock from "@/components/common/MarkdownBlock"
|
||||
import SuccessButton from "@/components/common/SuccessButton"
|
||||
import { WithCopyButton } from "@/components/common/CopyButton"
|
||||
import Thumbnails from "@/components/common/Thumbnails"
|
||||
import McpResponseDisplay from "@/components/mcp/chat-display/McpResponseDisplay"
|
||||
import McpResourceRow from "@/components/mcp/configuration/tabs/installed/server-row/McpResourceRow"
|
||||
import McpToolRow from "@/components/mcp/configuration/tabs/installed/server-row/McpToolRow"
|
||||
@@ -32,19 +28,76 @@ import {
|
||||
} from "@shared/ExtensionMessage"
|
||||
import { COMMAND_OUTPUT_STRING, COMMAND_REQ_APP_STRING } from "@shared/combineCommandSequences"
|
||||
import { Int64Request, StringRequest } from "@shared/proto/common"
|
||||
|
||||
import CodeAccordian, { cleanPathPrefix } from "../common/CodeAccordian"
|
||||
import { useEvent, useSize } from "react-use"
|
||||
import styled from "styled-components"
|
||||
import { CheckpointControls } from "../common/CheckpointControls"
|
||||
import CodeAccordian, { cleanPathPrefix } from "../common/CodeAccordian"
|
||||
import NewTaskPreview from "./NewTaskPreview"
|
||||
import QuoteButton from "./QuoteButton"
|
||||
import ReportBugPreview from "./ReportBugPreview"
|
||||
import UserMessage from "./UserMessage"
|
||||
import QuoteButton from "./QuoteButton"
|
||||
|
||||
interface CopyButtonProps {
|
||||
textToCopy: string | undefined
|
||||
}
|
||||
|
||||
const normalColor = "var(--vscode-foreground)"
|
||||
const errorColor = "var(--vscode-errorForeground)"
|
||||
const successColor = "var(--vscode-charts-green)"
|
||||
const cancelledColor = "var(--vscode-descriptionForeground)"
|
||||
|
||||
const CopyButtonStyled = styled(VSCodeButton)`
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
right: 2px;
|
||||
z-index: 1;
|
||||
opacity: 0;
|
||||
`
|
||||
|
||||
interface WithCopyButtonProps {
|
||||
children: React.ReactNode
|
||||
textToCopy?: string
|
||||
style?: React.CSSProperties
|
||||
ref?: React.Ref<HTMLDivElement>
|
||||
onMouseUp?: (event: MouseEvent<HTMLDivElement>) => void
|
||||
}
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
position: relative;
|
||||
|
||||
&:hover ${CopyButtonStyled} {
|
||||
opacity: 1;
|
||||
}
|
||||
`
|
||||
|
||||
const WithCopyButton = React.forwardRef<HTMLDivElement, WithCopyButtonProps>(
|
||||
({ children, textToCopy, style, onMouseUp, ...props }, ref) => {
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const handleCopy = () => {
|
||||
if (!textToCopy) return
|
||||
|
||||
navigator.clipboard.writeText(textToCopy).then(() => {
|
||||
setCopied(true)
|
||||
setTimeout(() => {
|
||||
setCopied(false)
|
||||
}, 1500)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledContainer ref={ref} onMouseUp={onMouseUp} style={style} {...props}>
|
||||
{children}
|
||||
{textToCopy && (
|
||||
<CopyButtonStyled appearance="icon" onClick={handleCopy} aria-label={copied ? "Copied" : "Copy"}>
|
||||
<span className={`codicon codicon-${copied ? "check" : "copy"}`}></span>
|
||||
</CopyButtonStyled>
|
||||
)}
|
||||
</StyledContainer>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
const ChatRowContainer = styled.div`
|
||||
padding: 10px 6px 10px 15px;
|
||||
position: relative;
|
||||
@@ -1001,43 +1054,9 @@ export const ChatRowContent = ({
|
||||
return null // we should never see this message type
|
||||
case "mcp_server_response":
|
||||
return <McpResponseDisplay responseText={message.text || ""} />
|
||||
case "mcp_notification":
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
gap: "8px",
|
||||
padding: "8px 12px",
|
||||
backgroundColor: "var(--vscode-textBlockQuote-background)",
|
||||
borderRadius: "4px",
|
||||
fontSize: "13px",
|
||||
color: "var(--vscode-foreground)",
|
||||
opacity: 0.9,
|
||||
marginBottom: "8px",
|
||||
}}>
|
||||
<i
|
||||
className="codicon codicon-bell"
|
||||
style={{
|
||||
marginTop: "2px",
|
||||
fontSize: "14px",
|
||||
color: "var(--vscode-notificationsInfoIcon-foreground)",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
<div style={{ flex: 1, wordBreak: "break-word" }}>
|
||||
<span style={{ fontWeight: 500 }}>MCP Notification: </span>
|
||||
<span className="ph-no-capture">{message.text}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
case "text":
|
||||
return (
|
||||
<WithCopyButton
|
||||
ref={contentRef}
|
||||
onMouseUp={handleMouseUp}
|
||||
textToCopy={message.text}
|
||||
position="bottom-right">
|
||||
<WithCopyButton ref={contentRef} onMouseUp={handleMouseUp} textToCopy={message.text}>
|
||||
<Markdown markdown={message.text} />
|
||||
{quoteButtonState.visible && (
|
||||
<QuoteButton
|
||||
@@ -1274,7 +1293,6 @@ export const ChatRowContent = ({
|
||||
ref={contentRef}
|
||||
onMouseUp={handleMouseUp}
|
||||
textToCopy={text}
|
||||
position="bottom-right"
|
||||
style={{
|
||||
color: "var(--vscode-charts-green)",
|
||||
paddingTop: 10,
|
||||
@@ -1439,7 +1457,6 @@ export const ChatRowContent = ({
|
||||
ref={contentRef}
|
||||
onMouseUp={handleMouseUp}
|
||||
textToCopy={text}
|
||||
position="bottom-right"
|
||||
style={{
|
||||
color: "var(--vscode-charts-green)",
|
||||
paddingTop: 10,
|
||||
@@ -1510,7 +1527,6 @@ export const ChatRowContent = ({
|
||||
ref={contentRef}
|
||||
onMouseUp={handleMouseUp}
|
||||
textToCopy={question}
|
||||
position="bottom-right"
|
||||
style={{ paddingTop: 10 }}>
|
||||
<Markdown markdown={question} />
|
||||
<OptionsButtons
|
||||
@@ -1594,7 +1610,7 @@ export const ChatRowContent = ({
|
||||
response = message.text
|
||||
}
|
||||
return (
|
||||
<WithCopyButton ref={contentRef} onMouseUp={handleMouseUp} textToCopy={response} position="bottom-right">
|
||||
<WithCopyButton ref={contentRef} onMouseUp={handleMouseUp} textToCopy={response}>
|
||||
<Markdown markdown={response} />
|
||||
<OptionsButtons
|
||||
options={options}
|
||||
|
||||
@@ -6,7 +6,7 @@ import Thumbnails from "@/components/common/Thumbnails"
|
||||
import Tooltip from "@/components/common/Tooltip"
|
||||
import ApiOptions, { normalizeApiConfiguration } from "@/components/settings/ApiOptions"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { FileServiceClient, StateServiceClient, ModelsServiceClient } from "@/services/grpc-client"
|
||||
import { FileServiceClient, StateServiceClient } from "@/services/grpc-client"
|
||||
import {
|
||||
ContextMenuOptionType,
|
||||
getContextMenuOptions,
|
||||
@@ -34,8 +34,6 @@ import { mentionRegex, mentionRegexGlobal } from "@shared/context-mentions"
|
||||
import { ExtensionMessage } from "@shared/ExtensionMessage"
|
||||
import { EmptyRequest, StringRequest } from "@shared/proto/common"
|
||||
import { FileSearchRequest, RelativePathsRequest } from "@shared/proto/file"
|
||||
import { UpdateApiConfigurationRequest } from "@shared/proto/models"
|
||||
import { convertApiConfigurationToProto } from "@shared/proto-conversions/models/api-configuration-conversion"
|
||||
import { PlanActMode, TogglePlanActModeRequest } from "@shared/proto/state"
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import React, { forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"
|
||||
@@ -81,7 +79,6 @@ interface ChatTextAreaProps {
|
||||
shouldDisableFilesAndImages: boolean
|
||||
onHeightChange?: (height: number) => void
|
||||
onFocusChange?: (isFocused: boolean) => void
|
||||
isTaskView: boolean
|
||||
}
|
||||
|
||||
interface GitCommit {
|
||||
@@ -269,7 +266,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
shouldDisableFilesAndImages,
|
||||
onHeightChange,
|
||||
onFocusChange,
|
||||
isTaskView,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
@@ -966,20 +962,12 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
)
|
||||
|
||||
// Separate the API config submission logic
|
||||
const submitApiConfig = useCallback(async () => {
|
||||
const submitApiConfig = useCallback(() => {
|
||||
const apiValidationResult = validateApiConfiguration(apiConfiguration)
|
||||
const modelIdValidationResult = validateModelId(apiConfiguration, openRouterModels)
|
||||
|
||||
if (!apiValidationResult && !modelIdValidationResult && apiConfiguration) {
|
||||
try {
|
||||
await ModelsServiceClient.updateApiConfigurationProto(
|
||||
UpdateApiConfigurationRequest.create({
|
||||
apiConfiguration: convertApiConfigurationToProto(apiConfiguration),
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Failed to update API configuration:", error)
|
||||
}
|
||||
if (!apiValidationResult && !modelIdValidationResult) {
|
||||
vscode.postMessage({ type: "apiConfiguration", apiConfiguration })
|
||||
} else {
|
||||
StateServiceClient.getLatestState(EmptyRequest.create())
|
||||
.then(() => {
|
||||
@@ -1017,9 +1005,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
)
|
||||
// Focus the textarea after mode toggle with slight delay
|
||||
setTimeout(() => {
|
||||
if (isTaskView) {
|
||||
setInputValue("")
|
||||
}
|
||||
textAreaRef.current?.focus()
|
||||
}, 100)
|
||||
}, changeModeDelay)
|
||||
@@ -1581,7 +1566,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
/>
|
||||
{!inputValue && selectedImages.length === 0 && selectedFiles.length === 0 && (
|
||||
<div className="absolute bottom-4 left-[25px] right-[60px] text-[10px] text-[var(--vscode-input-placeholderForeground)] opacity-70 whitespace-nowrap overflow-hidden text-ellipsis pointer-events-none z-[1]">
|
||||
Type @ for context, / for slash commands & workflows, hold shift to drag in files/images
|
||||
Type @ for context, / for slash commands & workflows
|
||||
</div>
|
||||
)}
|
||||
{(selectedImages.length > 0 || selectedFiles.length > 0) && (
|
||||
|
||||
@@ -132,6 +132,8 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
const [selectedImages, setSelectedImages] = useState<string[]>([])
|
||||
const [selectedFiles, setSelectedFiles] = useState<string[]>([])
|
||||
|
||||
// we need to hold on to the ask because useEffect > lastMessage will always let us know when an ask comes in and handle it, but by the time handleMessage is called, the last message might not be the ask anymore (it could be a say that followed)
|
||||
const [clineAsk, setClineAsk] = useState<ClineAsk | undefined>(undefined)
|
||||
const [enableButtons, setEnableButtons] = useState<boolean>(false)
|
||||
const [primaryButtonText, setPrimaryButtonText] = useState<string | undefined>("Approve")
|
||||
const [secondaryButtonText, setSecondaryButtonText] = useState<string | undefined>("Reject")
|
||||
@@ -144,14 +146,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
const [isAtBottom, setIsAtBottom] = useState(false)
|
||||
const [pendingScrollToMessage, setPendingScrollToMessage] = useState<number | null>(null)
|
||||
|
||||
// UI layout depends on the last 2 messages
|
||||
// (since it relies on the content of these messages, we are deep comparing. i.e. the button state after hitting button sets enableButtons to false, and this effect otherwise would have to true again even if messages didn't change
|
||||
const lastMessage = useMemo(() => messages.at(-1), [messages])
|
||||
const secondLastMessage = useMemo(() => messages.at(-2), [messages])
|
||||
|
||||
// Derive clineAsk directly from lastMessage to avoid race conditions
|
||||
const clineAsk = useMemo(() => (lastMessage?.type === "ask" ? lastMessage.ask : undefined), [lastMessage])
|
||||
|
||||
useEffect(() => {
|
||||
const handleCopy = async (e: ClipboardEvent) => {
|
||||
const targetElement = e.target as HTMLElement | null
|
||||
@@ -238,6 +232,11 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
document.removeEventListener("copy", handleCopy)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// UI layout depends on the last 2 messages
|
||||
// (since it relies on the content of these messages, we are deep comparing. i.e. the button state after hitting button sets enableButtons to false, and this effect otherwise would have to true again even if messages didn't change
|
||||
const lastMessage = useMemo(() => messages.at(-1), [messages])
|
||||
const secondLastMessage = useMemo(() => messages.at(-2), [messages])
|
||||
useDeepCompareEffect(() => {
|
||||
// if last message is an ask, show user ask UI
|
||||
// if user finished a task, then start a new task with a new conversation history since in this moment that the extension is waiting for user response, the user could close the extension and the conversation history would be lost.
|
||||
@@ -249,36 +248,42 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
switch (lastMessage.ask) {
|
||||
case "api_req_failed":
|
||||
setSendingDisabled(true)
|
||||
setClineAsk("api_req_failed")
|
||||
setEnableButtons(true)
|
||||
setPrimaryButtonText("Retry")
|
||||
setSecondaryButtonText("Start New Task")
|
||||
break
|
||||
case "mistake_limit_reached":
|
||||
setSendingDisabled(false)
|
||||
setClineAsk("mistake_limit_reached")
|
||||
setEnableButtons(true)
|
||||
setPrimaryButtonText("Proceed Anyways")
|
||||
setSecondaryButtonText("Start New Task")
|
||||
break
|
||||
case "auto_approval_max_req_reached":
|
||||
setSendingDisabled(true)
|
||||
setClineAsk("auto_approval_max_req_reached")
|
||||
setEnableButtons(true)
|
||||
setPrimaryButtonText("Proceed")
|
||||
setSecondaryButtonText("Start New Task")
|
||||
break
|
||||
case "followup":
|
||||
setSendingDisabled(isPartial)
|
||||
setClineAsk("followup")
|
||||
setEnableButtons(false)
|
||||
// setPrimaryButtonText(undefined)
|
||||
// setSecondaryButtonText(undefined)
|
||||
break
|
||||
case "plan_mode_respond":
|
||||
setSendingDisabled(isPartial)
|
||||
setClineAsk("plan_mode_respond")
|
||||
setEnableButtons(false)
|
||||
// setPrimaryButtonText(undefined)
|
||||
// setSecondaryButtonText(undefined)
|
||||
break
|
||||
case "tool":
|
||||
setSendingDisabled(isPartial)
|
||||
setClineAsk("tool")
|
||||
setEnableButtons(!isPartial)
|
||||
const tool = JSON.parse(lastMessage.text || "{}") as ClineSayTool
|
||||
switch (tool.tool) {
|
||||
@@ -295,24 +300,28 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
break
|
||||
case "browser_action_launch":
|
||||
setSendingDisabled(isPartial)
|
||||
setClineAsk("browser_action_launch")
|
||||
setEnableButtons(!isPartial)
|
||||
setPrimaryButtonText("Approve")
|
||||
setSecondaryButtonText("Reject")
|
||||
break
|
||||
case "command":
|
||||
setSendingDisabled(isPartial)
|
||||
setClineAsk("command")
|
||||
setEnableButtons(!isPartial)
|
||||
setPrimaryButtonText("Run Command")
|
||||
setSecondaryButtonText("Reject")
|
||||
break
|
||||
case "command_output":
|
||||
setSendingDisabled(false)
|
||||
setClineAsk("command_output")
|
||||
setEnableButtons(true)
|
||||
setPrimaryButtonText("Proceed While Running")
|
||||
setSecondaryButtonText(undefined)
|
||||
break
|
||||
case "use_mcp_server":
|
||||
setSendingDisabled(isPartial)
|
||||
setClineAsk("use_mcp_server")
|
||||
setEnableButtons(!isPartial)
|
||||
setPrimaryButtonText("Approve")
|
||||
setSecondaryButtonText("Reject")
|
||||
@@ -320,12 +329,14 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
case "completion_result":
|
||||
// extension waiting for feedback. but we can just present a new task button
|
||||
setSendingDisabled(isPartial)
|
||||
setClineAsk("completion_result")
|
||||
setEnableButtons(!isPartial)
|
||||
setPrimaryButtonText("Start New Task")
|
||||
setSecondaryButtonText(undefined)
|
||||
break
|
||||
case "resume_task":
|
||||
setSendingDisabled(false)
|
||||
setClineAsk("resume_task")
|
||||
setEnableButtons(true)
|
||||
setPrimaryButtonText("Resume Task")
|
||||
setSecondaryButtonText(undefined)
|
||||
@@ -333,6 +344,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
break
|
||||
case "resume_completed_task":
|
||||
setSendingDisabled(false)
|
||||
setClineAsk("resume_completed_task")
|
||||
setEnableButtons(true)
|
||||
setPrimaryButtonText("Start New Task")
|
||||
setSecondaryButtonText(undefined)
|
||||
@@ -340,18 +352,21 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
break
|
||||
case "new_task":
|
||||
setSendingDisabled(isPartial)
|
||||
setClineAsk("new_task")
|
||||
setEnableButtons(!isPartial)
|
||||
setPrimaryButtonText("Start New Task with Context")
|
||||
setSecondaryButtonText(undefined)
|
||||
break
|
||||
case "condense":
|
||||
setSendingDisabled(isPartial)
|
||||
setClineAsk("condense")
|
||||
setEnableButtons(!isPartial)
|
||||
setPrimaryButtonText("Condense Conversation")
|
||||
setSecondaryButtonText(undefined)
|
||||
break
|
||||
case "report_bug":
|
||||
setSendingDisabled(isPartial)
|
||||
setClineAsk("report_bug")
|
||||
setEnableButtons(!isPartial)
|
||||
setPrimaryButtonText("Report GitHub issue")
|
||||
setSecondaryButtonText(undefined)
|
||||
@@ -368,6 +383,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
setSendingDisabled(true)
|
||||
setSelectedImages([])
|
||||
setSelectedFiles([])
|
||||
setClineAsk(undefined)
|
||||
setEnableButtons(false)
|
||||
}
|
||||
break
|
||||
@@ -403,6 +419,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
useEffect(() => {
|
||||
if (messages.length === 0) {
|
||||
setSendingDisabled(false)
|
||||
setClineAsk(undefined)
|
||||
setEnableButtons(false)
|
||||
setPrimaryButtonText("Approve")
|
||||
setSecondaryButtonText("Reject")
|
||||
@@ -487,6 +504,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
setSendingDisabled(true)
|
||||
setSelectedImages([])
|
||||
setSelectedFiles([])
|
||||
setClineAsk(undefined)
|
||||
setEnableButtons(false)
|
||||
// setPrimaryButtonText(undefined)
|
||||
// setSecondaryButtonText(undefined)
|
||||
@@ -566,6 +584,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
break
|
||||
}
|
||||
setSendingDisabled(true)
|
||||
setClineAsk(undefined)
|
||||
setEnableButtons(false)
|
||||
// setPrimaryButtonText(undefined)
|
||||
// setSecondaryButtonText(undefined)
|
||||
@@ -618,6 +637,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
break
|
||||
}
|
||||
setSendingDisabled(true)
|
||||
setClineAsk(undefined)
|
||||
setEnableButtons(false)
|
||||
// setPrimaryButtonText(undefined)
|
||||
// setSecondaryButtonText(undefined)
|
||||
@@ -1286,7 +1306,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
scrollToBottomAuto()
|
||||
}
|
||||
}}
|
||||
isTaskView={!!task}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ import { CheckpointRestoreRequest } from "@shared/proto/checkpoints"
|
||||
import { Int64Request } from "@shared/proto/common"
|
||||
import { ClineCheckpointRestore } from "@shared/WebviewMessage"
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useCallback, useEffect, useRef, useState } from "react"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { createPortal } from "react-dom"
|
||||
import styled from "styled-components"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
@@ -21,43 +21,11 @@ export const CheckmarkControl = ({ messageTs, isCheckpointCheckedOut }: Checkmar
|
||||
const [restoreWorkspaceDisabled, setRestoreWorkspaceDisabled] = useState(false)
|
||||
const [restoreBothDisabled, setRestoreBothDisabled] = useState(false)
|
||||
const [showRestoreConfirm, setShowRestoreConfirm] = useState(false)
|
||||
const [hasMouseEntered, setHasMouseEntered] = useState(false)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const tooltipRef = useRef<HTMLDivElement>(null)
|
||||
const { onRelinquishControl } = useExtensionState()
|
||||
|
||||
// Debounce
|
||||
const closeMenuTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const scheduleCloseRestore = useCallback(() => {
|
||||
if (closeMenuTimeoutRef.current) {
|
||||
clearTimeout(closeMenuTimeoutRef.current)
|
||||
}
|
||||
closeMenuTimeoutRef.current = setTimeout(() => {
|
||||
setShowRestoreConfirm(false)
|
||||
}, 350)
|
||||
}, [])
|
||||
|
||||
const cancelCloseRestore = useCallback(() => {
|
||||
if (closeMenuTimeoutRef.current) {
|
||||
clearTimeout(closeMenuTimeoutRef.current)
|
||||
closeMenuTimeoutRef.current = null
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Debounce cleanup
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (closeMenuTimeoutRef.current) {
|
||||
clearTimeout(closeMenuTimeoutRef.current)
|
||||
closeMenuTimeoutRef.current = null
|
||||
}
|
||||
}
|
||||
}, [showRestoreConfirm])
|
||||
|
||||
// Clear "Restore Files" button when checkpoint is no longer checked out
|
||||
useEffect(() => {
|
||||
if (!isCheckpointCheckedOut && restoreWorkspaceDisabled) {
|
||||
setRestoreWorkspaceDisabled(false)
|
||||
}
|
||||
}, [isCheckpointCheckedOut, restoreWorkspaceDisabled])
|
||||
|
||||
const { refs, floatingStyles, update, placement } = useFloating({
|
||||
placement: "bottom-end",
|
||||
middleware: [
|
||||
@@ -144,27 +112,38 @@ export const CheckmarkControl = ({ messageTs, isCheckpointCheckedOut }: Checkmar
|
||||
}
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
cancelCloseRestore()
|
||||
setHasMouseEntered(true)
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
scheduleCloseRestore()
|
||||
if (hasMouseEntered) {
|
||||
setShowRestoreConfirm(false)
|
||||
setHasMouseEntered(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleControlsMouseEnter = () => {
|
||||
cancelCloseRestore()
|
||||
}
|
||||
const handleControlsMouseLeave = (e: React.MouseEvent) => {
|
||||
const tooltipElement = tooltipRef.current
|
||||
|
||||
const handleControlsMouseLeave = () => {
|
||||
scheduleCloseRestore()
|
||||
if (tooltipElement && showRestoreConfirm) {
|
||||
const tooltipRect = tooltipElement.getBoundingClientRect()
|
||||
|
||||
if (
|
||||
e.clientY >= tooltipRect.top &&
|
||||
e.clientY <= tooltipRect.bottom &&
|
||||
e.clientX >= tooltipRect.left &&
|
||||
e.clientX <= tooltipRect.right
|
||||
) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
setShowRestoreConfirm(false)
|
||||
setHasMouseEntered(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container
|
||||
isMenuOpen={showRestoreConfirm}
|
||||
$isCheckedOut={isCheckpointCheckedOut}
|
||||
onMouseEnter={handleControlsMouseEnter}
|
||||
onMouseLeave={handleControlsMouseLeave}>
|
||||
<Container isMenuOpen={showRestoreConfirm} $isCheckedOut={isCheckpointCheckedOut} onMouseLeave={handleControlsMouseLeave}>
|
||||
<i
|
||||
className="codicon codicon-bookmark"
|
||||
style={{
|
||||
@@ -217,9 +196,9 @@ export const CheckmarkControl = ({ messageTs, isCheckpointCheckedOut }: Checkmar
|
||||
<RestoreOption>
|
||||
<VSCodeButton
|
||||
onClick={handleRestoreWorkspace}
|
||||
disabled={restoreWorkspaceDisabled || isCheckpointCheckedOut}
|
||||
disabled={restoreWorkspaceDisabled}
|
||||
style={{
|
||||
cursor: restoreWorkspaceDisabled || isCheckpointCheckedOut ? "wait" : "pointer",
|
||||
cursor: restoreWorkspaceDisabled ? "wait" : "pointer",
|
||||
width: "100%",
|
||||
marginBottom: "10px",
|
||||
}}>
|
||||
@@ -339,9 +318,9 @@ const CustomButton = styled.button<{ disabled?: boolean; isActive?: boolean; $is
|
||||
props.isActive || props.disabled
|
||||
? "none"
|
||||
: `linear-gradient(to right, ${props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)"} 50%, transparent 50%),
|
||||
linear-gradient(to bottom, ${props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)"} 50%, transparent 50%),
|
||||
linear-gradient(to right, ${props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)"} 50%, transparent 50%),
|
||||
linear-gradient(to bottom, ${props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)"} 50%, transparent 50%)`};
|
||||
linear-gradient(to bottom, ${props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)"} 50%, transparent 50%),
|
||||
linear-gradient(to right, ${props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)"} 50%, transparent 50%),
|
||||
linear-gradient(to bottom, ${props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)"} 50%, transparent 50%)`};
|
||||
background-size: ${(props) => (props.isActive || props.disabled ? "auto" : `4px 1px, 1px 4px, 4px 1px, 1px 4px`)};
|
||||
background-repeat: repeat-x, repeat-y, repeat-x, repeat-y;
|
||||
background-position:
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
import React, { useState, useRef, forwardRef } from "react"
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import styled from "styled-components"
|
||||
|
||||
// ======== Interfaces ========
|
||||
|
||||
interface CopyButtonProps {
|
||||
textToCopy?: string
|
||||
onCopy?: () => string | void | null
|
||||
className?: string
|
||||
ariaLabel?: string
|
||||
}
|
||||
|
||||
interface WithCopyButtonProps {
|
||||
children: React.ReactNode
|
||||
textToCopy?: string
|
||||
onCopy?: () => string | void | null
|
||||
position?: "top-right" | "bottom-right"
|
||||
style?: React.CSSProperties
|
||||
className?: string
|
||||
onMouseUp?: (event: React.MouseEvent<HTMLDivElement>) => void
|
||||
ariaLabel?: string
|
||||
}
|
||||
|
||||
// ======== Styled Components ========
|
||||
|
||||
const StyledButton = styled(VSCodeButton)`
|
||||
z-index: 1;
|
||||
`
|
||||
|
||||
// Unified container component
|
||||
const ContentContainer = styled.div`
|
||||
position: relative;
|
||||
`
|
||||
|
||||
// Unified button container with flexible positioning
|
||||
const ButtonContainer = styled.div<{ $position?: "top-right" | "bottom-right" }>`
|
||||
position: absolute;
|
||||
${(props) => {
|
||||
switch (props.$position) {
|
||||
case "bottom-right":
|
||||
return "bottom: 2px; right: 2px;"
|
||||
case "top-right":
|
||||
default:
|
||||
return "top: 5px; right: 5px;"
|
||||
}
|
||||
}}
|
||||
z-index: 1;
|
||||
opacity: 0;
|
||||
|
||||
${ContentContainer}:hover & {
|
||||
opacity: 1;
|
||||
}
|
||||
`
|
||||
|
||||
// ======== Component Implementations ========
|
||||
|
||||
/**
|
||||
* Base copy button component with clipboard functionality
|
||||
*/
|
||||
export const CopyButton: React.FC<CopyButtonProps> = ({ textToCopy, onCopy, className = "", ariaLabel }) => {
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const handleCopy = () => {
|
||||
if (!textToCopy && !onCopy) return
|
||||
|
||||
let textToCopyFinal = textToCopy
|
||||
|
||||
if (onCopy) {
|
||||
const result = onCopy()
|
||||
if (typeof result === "string") {
|
||||
textToCopyFinal = result
|
||||
}
|
||||
}
|
||||
|
||||
if (textToCopyFinal) {
|
||||
navigator.clipboard
|
||||
.writeText(textToCopyFinal)
|
||||
.then(() => {
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 1500)
|
||||
})
|
||||
.catch((err) => console.error("Copy failed", err))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledButton
|
||||
appearance="icon"
|
||||
onClick={handleCopy}
|
||||
className={className}
|
||||
aria-label={copied ? "Copied" : ariaLabel || "Copy"}>
|
||||
<span className={`codicon codicon-${copied ? "check" : "copy"}`}></span>
|
||||
</StyledButton>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Container component that wraps content with a copy button
|
||||
*/
|
||||
export const WithCopyButton = forwardRef<HTMLDivElement, WithCopyButtonProps>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
textToCopy,
|
||||
onCopy,
|
||||
position = "top-right",
|
||||
style,
|
||||
className,
|
||||
onMouseUp,
|
||||
ariaLabel, // Destructure ariaLabel
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
return (
|
||||
<ContentContainer ref={ref} onMouseUp={onMouseUp} style={style} className={className} {...props}>
|
||||
{children}
|
||||
{(textToCopy || onCopy) && (
|
||||
<ButtonContainer $position={position}>
|
||||
<CopyButton
|
||||
textToCopy={textToCopy}
|
||||
onCopy={onCopy}
|
||||
ariaLabel={ariaLabel} // Pass through the ariaLabel prop directly
|
||||
/>
|
||||
</ButtonContainer>
|
||||
)}
|
||||
</ContentContainer>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
// Default export for convenience if needed, though named exports are preferred for clarity
|
||||
const CopyButtonComponents = {
|
||||
CopyButton,
|
||||
WithCopyButton,
|
||||
}
|
||||
export default CopyButtonComponents
|
||||
@@ -1,18 +1,17 @@
|
||||
import React, { memo, useEffect, useRef, useState } from "react"
|
||||
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
|
||||
import MermaidBlock from "@/components/common/MermaidBlock"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { StateServiceClient } from "@/services/grpc-client"
|
||||
import { PlanActMode, TogglePlanActModeRequest } from "@shared/proto/state"
|
||||
import type { ComponentProps } from "react"
|
||||
import React, { memo, useEffect, useRef, useState } from "react"
|
||||
import { useRemark } from "react-remark"
|
||||
import rehypeHighlight, { Options } from "rehype-highlight"
|
||||
import rehypeKatex from "rehype-katex"
|
||||
import remarkMath from "remark-math"
|
||||
import styled from "styled-components"
|
||||
import { visit } from "unist-util-visit"
|
||||
import type { Node } from "unist"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
|
||||
import MermaidBlock from "@/components/common/MermaidBlock"
|
||||
import { WithCopyButton } from "./CopyButton"
|
||||
import { StateServiceClient } from "@/services/grpc-client"
|
||||
import { PlanActMode, TogglePlanActModeRequest } from "@shared/proto/state"
|
||||
import { visit } from "unist-util-visit"
|
||||
|
||||
// Styled component for Act Mode text with more specific styling
|
||||
const ActModeHighlight: React.FC = () => (
|
||||
@@ -179,6 +178,24 @@ const remarkPreventBoldFilenames = () => {
|
||||
}
|
||||
}
|
||||
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
const CopyButton = styled(VSCodeButton)`
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
z-index: 1;
|
||||
opacity: 0;
|
||||
`
|
||||
|
||||
const CodeBlockContainer = styled.div`
|
||||
position: relative;
|
||||
|
||||
&:hover ${CopyButton} {
|
||||
opacity: 1;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledMarkdown = styled.div`
|
||||
pre {
|
||||
background-color: ${CODE_BLOCK_BG_COLOR};
|
||||
@@ -320,6 +337,7 @@ const PreWithCopyButton = ({
|
||||
...preProps
|
||||
}: { theme: Record<string, string> } & React.HTMLAttributes<HTMLPreElement>) => {
|
||||
const preRef = useRef<HTMLPreElement>(null)
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const handleCopy = () => {
|
||||
if (preRef.current) {
|
||||
@@ -327,19 +345,22 @@ const PreWithCopyButton = ({
|
||||
const textToCopy = codeElement ? codeElement.textContent : preRef.current.textContent
|
||||
|
||||
if (!textToCopy) return
|
||||
return textToCopy
|
||||
navigator.clipboard.writeText(textToCopy).then(() => {
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 1500)
|
||||
})
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const styledPreProps = theme ? { ...preProps, theme } : preProps
|
||||
|
||||
return (
|
||||
<WithCopyButton onCopy={handleCopy} position="top-right" ariaLabel="Copy code">
|
||||
<StyledPre {...styledPreProps} ref={preRef}>
|
||||
<CodeBlockContainer>
|
||||
<CopyButton appearance="icon" onClick={handleCopy} aria-label={copied ? "Copied" : "Copy"}>
|
||||
<span className={`codicon codicon-${copied ? "check" : "copy"}`}></span>
|
||||
</CopyButton>
|
||||
<StyledPre {...preProps} theme={theme} ref={preRef}>
|
||||
{children}
|
||||
</StyledPre>
|
||||
</WithCopyButton>
|
||||
</CodeBlockContainer>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -117,19 +117,36 @@ interface UrlMatch {
|
||||
}
|
||||
|
||||
const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText }) => {
|
||||
const { mcpResponsesCollapsed, mcpRichDisplayEnabled } = useExtensionState() // Get setting from context
|
||||
const { mcpResponsesCollapsed } = useExtensionState() // Get setting from context
|
||||
const [isExpanded, setIsExpanded] = useState(!mcpResponsesCollapsed) // Initialize with context setting
|
||||
const [isLoading, setIsLoading] = useState(false) // Initial loading state for rich content
|
||||
const [displayMode, setDisplayMode] = useState<"rich" | "plain">(() => {
|
||||
// Initialize directly from the global setting.
|
||||
return mcpRichDisplayEnabled ? "rich" : "plain"
|
||||
// Get saved preference from localStorage, default to 'rich'
|
||||
const savedMode = localStorage.getItem("mcpDisplayMode")
|
||||
return savedMode === "plain" ? "plain" : "rich"
|
||||
})
|
||||
const [urlMatches, setUrlMatches] = useState<UrlMatch[]>([])
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
// Add a counter state for forcing re-renders to make toggling run smoother
|
||||
const [forceUpdateCounter, setForceUpdateCounter] = useState(0)
|
||||
|
||||
const toggleDisplayMode = useCallback(() => {
|
||||
setDisplayMode((prevMode) => (prevMode === "rich" ? "plain" : "rich"))
|
||||
}, [])
|
||||
const newMode = displayMode === "rich" ? "plain" : "rich"
|
||||
// Force an immediate re-render
|
||||
setForceUpdateCounter((prev) => prev + 1)
|
||||
// Update display mode and save preference
|
||||
setDisplayMode(newMode)
|
||||
localStorage.setItem("mcpDisplayMode", newMode)
|
||||
// If switching to plain mode, cancel any ongoing processing
|
||||
if (newMode === "plain") {
|
||||
console.log("Switching to plain mode - cancelling URL processing")
|
||||
setUrlMatches([]) // Clear any existing matches when switching to plain mode
|
||||
} else {
|
||||
// If switching to rich mode, the useEffect will re-run and fetch data
|
||||
console.log("Switching to rich mode - will start URL processing")
|
||||
setUrlMatches([])
|
||||
}
|
||||
}, [displayMode])
|
||||
|
||||
const toggleExpand = useCallback(() => {
|
||||
setIsExpanded((prev) => !prev)
|
||||
@@ -138,16 +155,14 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
|
||||
// Effect to update isExpanded if mcpResponsesCollapsed changes from context
|
||||
useEffect(() => {
|
||||
setIsExpanded(!mcpResponsesCollapsed)
|
||||
}, [mcpResponsesCollapsed])
|
||||
}, [])
|
||||
|
||||
// Find all URLs in the text and determine if they're images
|
||||
useEffect(() => {
|
||||
// Skip all processing if in plain mode
|
||||
if (!isExpanded || displayMode === "plain") {
|
||||
setIsLoading(false)
|
||||
if (urlMatches.length > 0) {
|
||||
setUrlMatches([]) // Clear any existing matches when in plain mode
|
||||
}
|
||||
setUrlMatches([]) // Clear any existing matches when in plain mode
|
||||
return
|
||||
}
|
||||
|
||||
@@ -265,7 +280,7 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
|
||||
processingCanceled = true
|
||||
console.log("Cleaning up URL processing")
|
||||
}
|
||||
}, [responseText, displayMode, isExpanded])
|
||||
}, [responseText, displayMode, forceUpdateCounter, isExpanded])
|
||||
|
||||
// Function to render content based on display mode
|
||||
const renderContent = () => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user