mirror of
https://github.com/cline/cline.git
synced 2026-09-06 20:41:02 +08:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f146f5f179 | |||
| c3326973c7 | |||
| b0de6390f4 | |||
| a6c33afb11 | |||
| ec26a912bc | |||
| 6a11c78288 | |||
| d27b199cea | |||
| 7ba4c9e15e | |||
| 802e72f1bf | |||
| 10a223f27d | |||
| 0fade12e8e | |||
| a4bf34f73b | |||
| 227c7195f6 | |||
| 5e55a7a095 | |||
| 741b1edf73 | |||
| 8b4e8ce37f | |||
| 95ad8d879b | |||
| 2a0d60f642 | |||
| 9f605a1f6c |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
updating o3 model pricing
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Fix list_files tool to return files if the targeted directory is a hidden directory
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Sorting mcp marketplace by newest listings by default
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
add ability to constrain size of terminal output
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Migrate focusChatInput message to protobus
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
chore(bedrock): remove @anthropic-ai/bedrock-sdk
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix(bedrock): remove Anthropic-Bedrock SDK
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Added a configurable default terminal profile setting
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Remove ‘-beta’ from grok model id
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix menu for setting terminal timeout
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add prompt caching indicator to grok 3
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
update the copy button functionality
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Supporting Notifications MCP with Cline
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Add SAP AI Core as a provider for Cline
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"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: true
|
||||
blank_issues_enabled: false
|
||||
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,6 +6,3 @@ 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
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
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,16 +1,17 @@
|
||||
<!-- ⚠️ Important: Discussion Required Before PR (Community Contributors) -->
|
||||
<!--
|
||||
Thank you for contributing to Cline!
|
||||
|
||||
**For community contributors, before submitting this PR, please ensure you have:**
|
||||
⚠️ 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
|
||||
|
||||
- [ ] **Opened an issue** to discuss your proposed changes with the community
|
||||
- [ ] **Received approval** from a core Cline contributor to proceed with the implementation
|
||||
- [ ] **Linked the issue below** 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.
|
||||
|
||||
**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.
|
||||
|
||||
---
|
||||
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
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ coverage
|
||||
*evals.env
|
||||
|
||||
# Generated proto files
|
||||
src/generated/
|
||||
src/core/controller/*/methods.ts
|
||||
src/core/controller/*/index.ts
|
||||
src/core/controller/grpc-service-config.ts
|
||||
|
||||
+4
-4
@@ -9,9 +9,9 @@ npm run lint || {
|
||||
|
||||
# Run Prettier
|
||||
echo "Running Prettier..."
|
||||
npm run format || {
|
||||
echo "❌ Prettier check failed. Run 'npm run format:fix' to automatically fix formatting issues."
|
||||
exit 1
|
||||
}
|
||||
npx lint-staged --verbose || {
|
||||
echo "❌ Prettier failed. Please fix the errors and try committing again."
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "✅ All checks passed!"
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# 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,6 +18,18 @@ 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**
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
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
+3146
-8548
File diff suppressed because it is too large
Load Diff
+12
-5
@@ -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.12",
|
||||
"version": "3.17.13",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
@@ -336,7 +336,7 @@
|
||||
"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 && cd webview-ui && npm run lint",
|
||||
"lint": "eslint src --ext ts && eslint webview-ui/src --ext ts && buf lint && cd webview-ui && npm run lint",
|
||||
"format": "prettier . --check",
|
||||
"format:fix": "prettier . --write",
|
||||
"test": "npm-run-all test:unit test:integration",
|
||||
@@ -358,7 +358,13 @@
|
||||
"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",
|
||||
@@ -384,6 +390,7 @@
|
||||
"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",
|
||||
@@ -397,14 +404,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.821.0",
|
||||
"@aws-sdk/client-bedrock-runtime": "^3.826.0",
|
||||
"@aws-sdk/credential-providers": "^3.826.0",
|
||||
"@bufbuild/protobuf": "^2.2.5",
|
||||
"@cerebras/cerebras_cloud_sdk": "^1.35.0",
|
||||
"@google-cloud/vertexai": "^1.9.3",
|
||||
"@google/genai": "^0.13.0",
|
||||
"@google/genai": "1.0.0",
|
||||
"@grpc/grpc-js": "^1.9.15",
|
||||
"@grpc/reflection": "^1.0.4",
|
||||
"@mistralai/mistralai": "^1.5.0",
|
||||
|
||||
+72
-59
@@ -10,25 +10,24 @@ 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 __filename = fileURLToPath(import.meta.url)
|
||||
const SCRIPT_DIR = path.dirname(__filename)
|
||||
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url))
|
||||
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 DESCRIPTOR_OUT_DIR = path.join(ROOT_DIR, "dist-standalone", "proto")
|
||||
|
||||
const isWindows = process.platform === "win32"
|
||||
const tsProtoPlugin = isWindows
|
||||
const TS_PROTO_PLUGIN = 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",
|
||||
|
||||
"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.
|
||||
]
|
||||
@@ -70,53 +69,40 @@ async function main() {
|
||||
checkAppleSiliconCompatibility()
|
||||
|
||||
// Create output directories if they don't exist
|
||||
await fs.mkdir(TS_OUT_DIR, { recursive: true })
|
||||
for (const dir of [TS_OUT_DIR, GRPC_JS_OUT_DIR, DESCRIPTOR_OUT_DIR]) {
|
||||
await fs.mkdir(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)
|
||||
|
||||
// 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)
|
||||
}
|
||||
tsProtoc(TS_OUT_DIR, protoFiles, ["outputServices=generic-definitions", ...TS_PROTO_OPTIONS])
|
||||
tsProtoc(GRPC_JS_OUT_DIR, protoFiles, ["outputServices=grpc-js", ...TS_PROTO_OPTIONS])
|
||||
|
||||
const descriptorOutDir = path.join(ROOT_DIR, "dist-standalone", "proto")
|
||||
await fs.mkdir(descriptorOutDir, { recursive: true })
|
||||
const descriptorFile = path.join(descriptorOutDir, "descriptor_set.pb")
|
||||
const descriptorFile = path.join(DESCRIPTOR_OUT_DIR, "descriptor_set.pb")
|
||||
const descriptorProtocCommand = [
|
||||
protoc,
|
||||
PROTOC,
|
||||
`--proto_path="${SCRIPT_DIR}"`,
|
||||
`--descriptor_set_out="${descriptorFile}"`,
|
||||
"--include_imports",
|
||||
...protoFiles,
|
||||
].join(" ")
|
||||
try {
|
||||
console.log(chalk.cyan("Generating descriptor set..."))
|
||||
log_verbose(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)
|
||||
}
|
||||
|
||||
console.log(chalk.green("Protocol Buffer code generation completed successfully."))
|
||||
console.log(chalk.green(`TypeScript files generated in: ${TS_OUT_DIR}`))
|
||||
log_verbose(chalk.green("Protocol Buffer code generation completed successfully."))
|
||||
log_verbose(chalk.green(`TypeScript files generated in: ${TS_OUT_DIR}`))
|
||||
|
||||
await generateMethodRegistrations()
|
||||
await generateHostMethodRegistrations()
|
||||
@@ -124,6 +110,27 @@ 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 tsProtocCommand = [
|
||||
PROTOC,
|
||||
`--proto_path="${SCRIPT_DIR}"`,
|
||||
`--plugin=protoc-gen-ts_proto="${TS_PROTO_PLUGIN}"`,
|
||||
`--ts_proto_out="${outDir}"`,
|
||||
`--ts_proto_opt=${protoOptions.join(",")} `,
|
||||
...protoFiles,
|
||||
].join(" ")
|
||||
try {
|
||||
log_verbose(chalk.cyan(`Generating TypeScript code in ${outDir} 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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,14 +138,14 @@ async function main() {
|
||||
* This eliminates the need for manual imports and client creation in grpc-client.ts
|
||||
*/
|
||||
async function generateGrpcClientConfig() {
|
||||
console.log(chalk.cyan("Generating gRPC client configuration..."))
|
||||
log_verbose(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
|
||||
@@ -168,7 +175,7 @@ export {
|
||||
|
||||
const configPath = path.join(ROOT_DIR, "webview-ui", "src", "services", "grpc-client.ts")
|
||||
await fs.writeFile(configPath, content)
|
||||
console.log(chalk.green(`Generated gRPC client at ${configPath}`))
|
||||
log_verbose(chalk.green(`Generated gRPC client at ${configPath}`))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -178,7 +185,7 @@ export {
|
||||
* @returns Map of service names to their streaming methods
|
||||
*/
|
||||
async function parseProtoForStreamingMethods(protoFiles, scriptDir) {
|
||||
console.log(chalk.cyan("Parsing proto files for streaming methods..."))
|
||||
log_verbose(chalk.cyan("Parsing proto files for streaming methods..."))
|
||||
|
||||
// Map of service name to array of streaming method names
|
||||
const streamingMethodsMap = new Map()
|
||||
@@ -230,7 +237,7 @@ async function parseProtoForStreamingMethods(protoFiles, scriptDir) {
|
||||
}
|
||||
|
||||
async function generateMethodRegistrations() {
|
||||
console.log(chalk.cyan("Generating method registration files..."))
|
||||
log_verbose(chalk.cyan("Generating method registration files..."))
|
||||
|
||||
// Parse proto files for streaming methods
|
||||
const protoFiles = await globby("*.proto", { cwd: SCRIPT_DIR })
|
||||
@@ -240,7 +247,7 @@ async function generateMethodRegistrations() {
|
||||
try {
|
||||
await fs.access(serviceDir)
|
||||
} catch (error) {
|
||||
console.log(chalk.cyan(`Creating directory ${serviceDir} for new service`))
|
||||
log_verbose(chalk.cyan(`Creating directory ${serviceDir} for new service`))
|
||||
await fs.mkdir(serviceDir, { recursive: true })
|
||||
}
|
||||
|
||||
@@ -251,7 +258,7 @@ async function generateMethodRegistrations() {
|
||||
const fullServiceName = serviceNameMap[serviceName]
|
||||
const streamingMethods = streamingMethodsMap.get(fullServiceName) || []
|
||||
|
||||
console.log(chalk.cyan(`Generating method registrations for ${serviceName}...`))
|
||||
log_verbose(chalk.cyan(`Generating method registrations for ${serviceName}...`))
|
||||
|
||||
// Get all TypeScript files in the service directory
|
||||
const files = await globby("*.ts", { cwd: serviceDir })
|
||||
@@ -304,7 +311,7 @@ export function registerAllMethods(): void {
|
||||
|
||||
// Write the methods.ts file
|
||||
await fs.writeFile(registryFile, methodsContent)
|
||||
console.log(chalk.green(`Generated ${registryFile}`))
|
||||
log_verbose(chalk.green(`Generated ${registryFile}`))
|
||||
|
||||
// Generate index.ts file
|
||||
const capitalizedServiceName = serviceName.charAt(0).toUpperCase() + serviceName.slice(1)
|
||||
@@ -333,10 +340,10 @@ registerAllMethods()`
|
||||
|
||||
// Write the index.ts file
|
||||
await fs.writeFile(indexFile, indexContent)
|
||||
console.log(chalk.green(`Generated ${indexFile}`))
|
||||
log_verbose(chalk.green(`Generated ${indexFile}`))
|
||||
}
|
||||
|
||||
console.log(chalk.green("Method registration files generated successfully."))
|
||||
log_verbose(chalk.green("Method registration files generated successfully."))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -344,7 +351,7 @@ registerAllMethods()`
|
||||
* This eliminates the need for manual switch/case statements in grpc-handler.ts
|
||||
*/
|
||||
async function generateServiceConfig() {
|
||||
console.log(chalk.cyan("Generating service configuration file..."))
|
||||
log_verbose(chalk.cyan("Generating service configuration file..."))
|
||||
|
||||
const serviceImports = []
|
||||
const serviceConfigs = []
|
||||
@@ -385,7 +392,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)
|
||||
console.log(chalk.green(`Generated service configuration at ${configPath}`))
|
||||
log_verbose(chalk.green(`Generated service configuration at ${configPath}`))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -393,7 +400,7 @@ export const serviceHandlers: Record<string, ServiceHandlerConfig> = {${serviceC
|
||||
* If a .proto file doesn't exist, create a template file
|
||||
*/
|
||||
async function ensureProtoFilesExist() {
|
||||
console.log(chalk.cyan("Checking for missing proto files..."))
|
||||
log_verbose(chalk.cyan("Checking for missing proto files..."))
|
||||
|
||||
// Get existing proto files
|
||||
const existingProtoFiles = await globby("*.proto", { cwd: SCRIPT_DIR })
|
||||
@@ -402,7 +409,7 @@ async function ensureProtoFilesExist() {
|
||||
// Check each service in serviceNameMap
|
||||
for (const [serviceName, fullServiceName] of Object.entries(serviceNameMap)) {
|
||||
if (!existingProtoServices.includes(serviceName)) {
|
||||
console.log(chalk.yellow(`Creating template proto file for ${serviceName}...`))
|
||||
log_verbose(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()
|
||||
@@ -435,7 +442,7 @@ service ${serviceClassName} {
|
||||
// Write the template proto file
|
||||
const protoFilePath = path.join(SCRIPT_DIR, `${serviceName}.proto`)
|
||||
await fs.writeFile(protoFilePath, protoContent)
|
||||
console.log(chalk.green(`Created template proto file at ${protoFilePath}`))
|
||||
log_verbose(chalk.green(`Created template proto file at ${protoFilePath}`))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -444,7 +451,7 @@ service ${serviceClassName} {
|
||||
* Generate method registration files for host services
|
||||
*/
|
||||
async function generateHostMethodRegistrations() {
|
||||
console.log(chalk.cyan("Generating host method registration files..."))
|
||||
log_verbose(chalk.cyan("Generating host method registration files..."))
|
||||
|
||||
// Parse proto files for streaming methods
|
||||
const hostProtoFiles = await globby("*.proto", { cwd: path.join(SCRIPT_DIR, "host") })
|
||||
@@ -454,7 +461,7 @@ async function generateHostMethodRegistrations() {
|
||||
try {
|
||||
await fs.access(serviceDir)
|
||||
} catch (error) {
|
||||
console.log(chalk.cyan(`Creating directory ${serviceDir} for new host service`))
|
||||
log_verbose(chalk.cyan(`Creating directory ${serviceDir} for new host service`))
|
||||
await fs.mkdir(serviceDir, { recursive: true })
|
||||
}
|
||||
|
||||
@@ -465,7 +472,7 @@ async function generateHostMethodRegistrations() {
|
||||
const fullServiceName = hostServiceNameMap[serviceName]
|
||||
const streamingMethods = streamingMethodsMap.get(fullServiceName) || []
|
||||
|
||||
console.log(chalk.cyan(`Generating method registrations for host ${serviceName}...`))
|
||||
log_verbose(chalk.cyan(`Generating method registrations for host ${serviceName}...`))
|
||||
|
||||
// Get all TypeScript files in the service directory
|
||||
const files = await globby("*.ts", { cwd: serviceDir })
|
||||
@@ -518,7 +525,7 @@ export function registerAllMethods(): void {
|
||||
|
||||
// Write the methods.ts file
|
||||
await fs.writeFile(registryFile, methodsContent)
|
||||
console.log(chalk.green(`Generated ${registryFile}`))
|
||||
log_verbose(chalk.green(`Generated ${registryFile}`))
|
||||
|
||||
// Generate index.ts file
|
||||
const capitalizedServiceName = serviceName.charAt(0).toUpperCase() + serviceName.slice(1)
|
||||
@@ -547,17 +554,17 @@ registerAllMethods()`
|
||||
|
||||
// Write the index.ts file
|
||||
await fs.writeFile(indexFile, indexContent)
|
||||
console.log(chalk.green(`Generated ${indexFile}`))
|
||||
log_verbose(chalk.green(`Generated ${indexFile}`))
|
||||
}
|
||||
|
||||
console.log(chalk.green("Host method registration files generated successfully."))
|
||||
log_verbose(chalk.green("Host method registration files generated successfully."))
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a service configuration file for host services
|
||||
*/
|
||||
async function generateHostServiceConfig() {
|
||||
console.log(chalk.cyan("Generating host service configuration file..."))
|
||||
log_verbose(chalk.cyan("Generating host service configuration file..."))
|
||||
|
||||
const serviceImports = []
|
||||
const serviceConfigs = []
|
||||
@@ -598,21 +605,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)
|
||||
console.log(chalk.green(`Generated host service configuration at ${configPath}`))
|
||||
log_verbose(chalk.green(`Generated host service configuration at ${configPath}`))
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a gRPC client configuration file for host services
|
||||
*/
|
||||
async function generateHostGrpcClientConfig() {
|
||||
console.log(chalk.cyan("Generating host gRPC client configuration..."))
|
||||
log_verbose(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
|
||||
@@ -643,12 +650,12 @@ 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)
|
||||
console.log(chalk.green(`Generated host gRPC client at ${configPath}`))
|
||||
log_verbose(chalk.green(`Generated host gRPC client at ${configPath}`))
|
||||
}
|
||||
|
||||
async function cleanup() {
|
||||
// Clean up existing generated files
|
||||
console.log(chalk.cyan("Cleaning up existing generated TypeScript files..."))
|
||||
log_verbose(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))
|
||||
@@ -705,6 +712,12 @@ 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 fsPath = 6;
|
||||
string fs_path = 6;
|
||||
}
|
||||
|
||||
// Request for joining path segments to a URI
|
||||
message JoinPathRequest {
|
||||
cline.Metadata metadata = 1;
|
||||
Uri base = 2;
|
||||
repeated string pathSegments = 3;
|
||||
repeated string path_segments = 3;
|
||||
}
|
||||
|
||||
+2
-2
@@ -85,8 +85,8 @@ message OpenRouterCompatibleModelInfo {
|
||||
// Request for fetching OpenAI models
|
||||
message OpenAiModelsRequest {
|
||||
Metadata metadata = 1;
|
||||
string baseUrl = 2;
|
||||
string apiKey = 3;
|
||||
string base_url = 2;
|
||||
string api_key = 3;
|
||||
}
|
||||
|
||||
// Request for updating API configuration
|
||||
|
||||
+30
-3
@@ -7,6 +7,10 @@ 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(EmptyRequest) returns (Empty);
|
||||
@@ -19,6 +23,23 @@ 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;
|
||||
@@ -42,10 +63,8 @@ message ChatContent {
|
||||
repeated string files = 3;
|
||||
}
|
||||
|
||||
// Message for auto approval settings
|
||||
message AutoApprovalSettingsRequest {
|
||||
Metadata metadata = 1;
|
||||
|
||||
message Actions {
|
||||
bool read_files = 1;
|
||||
bool read_files_externally = 2;
|
||||
@@ -56,7 +75,6 @@ message AutoApprovalSettingsRequest {
|
||||
bool use_browser = 7;
|
||||
bool use_mcp = 8;
|
||||
}
|
||||
|
||||
int32 version = 2;
|
||||
bool enabled = 3;
|
||||
Actions actions = 4;
|
||||
@@ -77,6 +95,8 @@ message UpdateSettingsRequest {
|
||||
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;
|
||||
}
|
||||
|
||||
// Complete API Configuration message
|
||||
@@ -185,4 +205,11 @@ 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;
|
||||
}
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ enum WebviewProviderType {
|
||||
// Define a new message type for webview provider info
|
||||
message WebviewProviderTypeRequest {
|
||||
Metadata metadata = 1;
|
||||
WebviewProviderType providerType = 2;
|
||||
WebviewProviderType provider_type = 2;
|
||||
}
|
||||
|
||||
// Enum for ClineMessage type
|
||||
|
||||
@@ -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(proto.cline.${name}.service, {`)
|
||||
handlerSetup.push(` server.addService(cline.${name}Service, {`)
|
||||
for (const [rpcName, rpc] of Object.entries(def.service)) {
|
||||
imports.push(`import { ${rpcName} } from "../core/controller/${dir}/${rpcName}"`)
|
||||
const requestType = "proto.cline." + rpc.requestType.type.name
|
||||
const requestType = "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 = "proto.cline." + rpc.responseType.type.name
|
||||
const responseType = "cline." + rpc.responseType.type.name
|
||||
handlerSetup.push(` ${rpcName}: wrapper<${requestType},${responseType}>(${rpcName}, controller),`)
|
||||
}
|
||||
}
|
||||
@@ -60,14 +60,13 @@ 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 * as proto from "@/shared/proto"
|
||||
import { cline } from "../generated/grpc-js"
|
||||
import { Controller } from "../core/controller"
|
||||
import { GrpcHandlerWrapper, GrpcStreamingResponseHandlerWrapper } from "./grpc-types"
|
||||
|
||||
${imports}
|
||||
export function addServices(
|
||||
server: grpc.Server,
|
||||
proto: any,
|
||||
controller: Controller,
|
||||
wrapper: GrpcHandlerWrapper,
|
||||
wrapStreamingResponse: GrpcStreamingResponseHandlerWrapper,
|
||||
|
||||
@@ -25,6 +25,7 @@ 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"
|
||||
|
||||
export interface ApiHandler {
|
||||
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream
|
||||
@@ -87,6 +88,8 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
|
||||
return new SambanovaHandler(options)
|
||||
case "cerebras":
|
||||
return new CerebrasHandler(options)
|
||||
case "sapaicore":
|
||||
return new SapAiCoreHandler(options)
|
||||
default:
|
||||
return new AnthropicHandler(options)
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
this.options = options
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
@withRetry({ maxRetries: 4 })
|
||||
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()
|
||||
|
||||
@@ -2,6 +2,7 @@ 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"
|
||||
@@ -96,9 +97,10 @@ export class GeminiHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
// Add thinking config if the model supports it
|
||||
if (info.thinkingConfig?.outputPrice !== undefined && maxBudget > 0) {
|
||||
if (thinkingBudget > 0) {
|
||||
requestConfig.thinkingConfig = {
|
||||
thinkingBudget: thinkingBudget,
|
||||
includeThoughts: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +113,7 @@ 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 {
|
||||
@@ -130,6 +133,31 @@ 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",
|
||||
@@ -141,6 +169,7 @@ export class GeminiHandler implements ApiHandler {
|
||||
lastUsageMetadata = chunk.usageMetadata
|
||||
promptTokens = lastUsageMetadata.promptTokenCount ?? promptTokens
|
||||
outputTokens = lastUsageMetadata.candidatesTokenCount ?? outputTokens
|
||||
thoughtsTokenCount = lastUsageMetadata.thoughtsTokenCount ?? thoughtsTokenCount
|
||||
cacheReadTokens = lastUsageMetadata.cachedContentTokenCount ?? cacheReadTokens
|
||||
}
|
||||
}
|
||||
@@ -151,12 +180,14 @@ export class GeminiHandler implements ApiHandler {
|
||||
info,
|
||||
inputTokens: promptTokens,
|
||||
outputTokens,
|
||||
thoughtsTokenCount,
|
||||
cacheReadTokens,
|
||||
})
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: promptTokens,
|
||||
outputTokens,
|
||||
thoughtsTokenCount,
|
||||
cacheReadTokens,
|
||||
cacheWriteTokens: 0,
|
||||
totalCost,
|
||||
@@ -239,11 +270,13 @@ 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
|
||||
@@ -275,18 +308,18 @@ export class GeminiHandler implements ApiHandler {
|
||||
const inputTokensCost = inputPrice * (uncachedInputTokens / 1_000_000)
|
||||
|
||||
// 2. Output token costs
|
||||
const outputTokensCost = outputPrice * (outputTokens / 1_000_000)
|
||||
const responseTokensCost = outputPrice * ((outputTokens + thoughtsTokenCount) / 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 + outputTokensCost + cacheReadCost
|
||||
const totalCost = inputTokensCost + responseTokensCost + 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: outputTokensCost },
|
||||
output: { price: outputPrice, tokens: outputTokens, cost: responseTokensCost },
|
||||
}
|
||||
|
||||
// Only include cache read costs in the trace (cache write costs are tracked separately)
|
||||
|
||||
@@ -0,0 +1,540 @@
|
||||
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,5 +17,6 @@ export interface ApiStreamUsageChunk {
|
||||
outputTokens: number
|
||||
cacheWriteTokens?: number
|
||||
cacheReadTokens?: number
|
||||
thoughtsTokenCount?: number // openrouter
|
||||
totalCost?: number // openrouter
|
||||
}
|
||||
|
||||
@@ -8,10 +8,7 @@ export function checkIsOpenRouterContextWindowError(error: any): boolean {
|
||||
|
||||
export function checkIsAnthropicContextWindowError(response: any): boolean {
|
||||
try {
|
||||
return (
|
||||
response?.error?.error?.type === "invalid_request_error" &&
|
||||
response?.error?.error?.message?.includes("prompt is too long")
|
||||
)
|
||||
return response?.error?.error?.type === "invalid_request_error"
|
||||
} catch (e: unknown) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -135,6 +135,8 @@ export class Controller {
|
||||
chatSettings,
|
||||
shellIntegrationTimeout,
|
||||
terminalReuseEnabled,
|
||||
terminalOutputLineLimit,
|
||||
defaultTerminalProfile,
|
||||
enableCheckpointsSetting,
|
||||
isNewUser,
|
||||
taskHistory,
|
||||
@@ -170,6 +172,8 @@ export class Controller {
|
||||
chatSettings,
|
||||
shellIntegrationTimeout,
|
||||
terminalReuseEnabled ?? true,
|
||||
terminalOutputLineLimit ?? 500,
|
||||
defaultTerminalProfile ?? "default",
|
||||
enableCheckpointsSetting ?? true,
|
||||
task,
|
||||
images,
|
||||
@@ -220,6 +224,7 @@ export class Controller {
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
}
|
||||
|
||||
case "clearAllTaskHistory": {
|
||||
const answer = await vscode.window.showWarningMessage(
|
||||
"What would you like to delete?",
|
||||
@@ -280,6 +285,12 @@ 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)
|
||||
|
||||
@@ -345,6 +356,23 @@ 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
|
||||
@@ -400,6 +428,9 @@ 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) {
|
||||
@@ -945,6 +976,7 @@ export class Controller {
|
||||
chatSettings,
|
||||
userInfo,
|
||||
mcpMarketplaceEnabled,
|
||||
mcpRichDisplayEnabled,
|
||||
telemetrySetting,
|
||||
planActSeparateModelsSetting,
|
||||
enableCheckpointsSetting,
|
||||
@@ -952,8 +984,10 @@ export class Controller {
|
||||
globalWorkflowToggles,
|
||||
shellIntegrationTimeout,
|
||||
terminalReuseEnabled,
|
||||
defaultTerminalProfile,
|
||||
isNewUser,
|
||||
mcpResponsesCollapsed,
|
||||
terminalOutputLineLimit,
|
||||
} = await getAllExtensionState(this.context)
|
||||
|
||||
const localClineRulesToggles =
|
||||
@@ -985,6 +1019,7 @@ export class Controller {
|
||||
chatSettings,
|
||||
userInfo,
|
||||
mcpMarketplaceEnabled,
|
||||
mcpRichDisplayEnabled,
|
||||
telemetrySetting,
|
||||
planActSeparateModelsSetting,
|
||||
enableCheckpointsSetting: enableCheckpointsSetting ?? true,
|
||||
@@ -997,8 +1032,10 @@ export class Controller {
|
||||
globalWorkflowToggles: globalWorkflowToggles || {},
|
||||
shellIntegrationTimeout,
|
||||
terminalReuseEnabled,
|
||||
defaultTerminalProfile,
|
||||
isNewUser,
|
||||
mcpResponsesCollapsed,
|
||||
terminalOutputLineLimit,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
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 || "",
|
||||
})),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
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,
|
||||
})
|
||||
}
|
||||
@@ -50,6 +50,11 @@ 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)
|
||||
@@ -69,6 +74,11 @@ 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()
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Controller } from "../index"
|
||||
import * as proto from "@/shared/proto"
|
||||
import { updateGlobalState } from "../../storage/state"
|
||||
|
||||
export async function updateTerminalConnectionTimeout(
|
||||
controller: Controller,
|
||||
request: proto.cline.Int64Request,
|
||||
): Promise<proto.cline.Int64> {
|
||||
const timeoutValue = 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 })
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
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({})
|
||||
}
|
||||
@@ -22,6 +22,8 @@ export type SecretKey =
|
||||
| "nebiusApiKey"
|
||||
| "sambanovaApiKey"
|
||||
| "cerebrasApiKey"
|
||||
| "sapAiCoreClientId"
|
||||
| "sapAiCoreClientSecret"
|
||||
|
||||
export type GlobalStateKey =
|
||||
| "awsRegion"
|
||||
@@ -67,7 +69,16 @@ export type GlobalStateKey =
|
||||
| "shellIntegrationTimeout"
|
||||
| "mcpResponsesCollapsed"
|
||||
| "terminalReuseEnabled"
|
||||
| "defaultTerminalProfile"
|
||||
| "isNewUser"
|
||||
| "terminalOutputLineLimit"
|
||||
| "mcpRichDisplayEnabled"
|
||||
| "sapAiCoreTokenUrl"
|
||||
| "sapAiCoreBaseUrl"
|
||||
| "sapAiResourceGroup"
|
||||
| "sapAiCoreClientId"
|
||||
| "sapAiCoreClientSecret"
|
||||
| "sapAiCoreModelId"
|
||||
|
||||
export type LocalStateKey =
|
||||
| "localClineRulesToggles"
|
||||
@@ -101,3 +112,9 @@ export type LocalStateKey =
|
||||
| "previousModeReasoningEffort"
|
||||
| "previousModeAwsBedrockCustomSelected"
|
||||
| "previousModeAwsBedrockCustomModelBaseId"
|
||||
| "previousModeSapAiCoreClientId"
|
||||
| "previousModeSapAiCoreClientSecret"
|
||||
| "previousModeSapAiCoreBaseUrl"
|
||||
| "previousModeSapAiCoreTokenUrl"
|
||||
| "previousModeSapAiCoreResourceGroup"
|
||||
| "previousModeSapAiCoreModelId"
|
||||
|
||||
@@ -234,9 +234,18 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
shellIntegrationTimeout,
|
||||
enableCheckpointsSettingRaw,
|
||||
mcpMarketplaceEnabledRaw,
|
||||
mcpRichDisplayEnabled,
|
||||
mcpResponsesCollapsedRaw,
|
||||
globalWorkflowToggles,
|
||||
terminalReuseEnabled,
|
||||
terminalOutputLineLimit,
|
||||
defaultTerminalProfile,
|
||||
sapAiCoreClientId,
|
||||
sapAiCoreClientSecret,
|
||||
sapAiCoreBaseUrl,
|
||||
sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup,
|
||||
sapAiCoreModelId,
|
||||
] = await Promise.all([
|
||||
getGlobalState(context, "isNewUser") as Promise<boolean | undefined>,
|
||||
getSecret(context, "apiKey") as Promise<string | undefined>,
|
||||
@@ -297,9 +306,18 @@ 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>,
|
||||
])
|
||||
|
||||
const localClineRulesToggles = (await getWorkspaceState(context, "localClineRulesToggles")) as ClineRulesToggles
|
||||
@@ -333,6 +351,12 @@ 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>,
|
||||
@@ -362,6 +386,12 @@ 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
|
||||
@@ -468,6 +498,12 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
nebiusApiKey,
|
||||
favoritedModelIds,
|
||||
requestTimeoutMs,
|
||||
sapAiCoreClientId,
|
||||
sapAiCoreClientSecret,
|
||||
sapAiCoreBaseUrl,
|
||||
sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup,
|
||||
sapAiCoreModelId,
|
||||
},
|
||||
isNewUser: isNewUser ?? true,
|
||||
lastShownAnnouncementId,
|
||||
@@ -489,13 +525,22 @@ 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 || {},
|
||||
}
|
||||
}
|
||||
@@ -567,6 +612,12 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
fireworksModelId,
|
||||
fireworksModelMaxCompletionTokens,
|
||||
fireworksModelMaxTokens,
|
||||
sapAiCoreClientId,
|
||||
sapAiCoreClientSecret,
|
||||
sapAiCoreBaseUrl,
|
||||
sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup,
|
||||
sapAiCoreModelId,
|
||||
} = apiConfiguration
|
||||
// Workspace state updates
|
||||
await updateWorkspaceState(context, "apiProvider", apiProvider)
|
||||
@@ -615,6 +666,12 @@ 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)
|
||||
|
||||
// Secret updates
|
||||
await storeSecret(context, "apiKey", apiKey)
|
||||
@@ -639,6 +696,8 @@ 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 resetExtensionState(context: vscode.ExtensionContext) {
|
||||
|
||||
+181
-69
@@ -113,7 +113,8 @@ 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 { isClaude4ModelFamily } from "@utils/model-utils"
|
||||
import { saveClineMessagesAndUpdateHistory } from "./message-state"
|
||||
|
||||
export const USE_EXPERIMENTAL_CLAUDE4_FEATURES = false
|
||||
|
||||
@@ -140,7 +141,7 @@ export class Task {
|
||||
readonly taskId: string
|
||||
private taskIsFavorited?: boolean
|
||||
api: ApiHandler
|
||||
private terminalManager: TerminalManager
|
||||
terminalManager: TerminalManager
|
||||
private urlContentFetcher: UrlContentFetcher
|
||||
browserSession: BrowserSession
|
||||
contextManager: ContextManager
|
||||
@@ -203,6 +204,8 @@ export class Task {
|
||||
chatSettings: ChatSettings,
|
||||
shellIntegrationTimeout: number,
|
||||
terminalReuseEnabled: boolean,
|
||||
terminalOutputLineLimit: number,
|
||||
defaultTerminalProfile: string,
|
||||
enableCheckpointsSetting: boolean,
|
||||
task?: string,
|
||||
images?: string[],
|
||||
@@ -222,6 +225,8 @@ 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()
|
||||
@@ -343,52 +348,28 @@ 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 this.saveClineMessagesAndUpdateHistory()
|
||||
await saveClineMessagesAndUpdateHistory(
|
||||
this.getContext(),
|
||||
this.taskId,
|
||||
this.clineMessages,
|
||||
this.taskIsFavorited ?? false,
|
||||
this.conversationHistoryDeletedRange,
|
||||
this.checkpointTracker,
|
||||
this.updateTaskHistory,
|
||||
)
|
||||
}
|
||||
|
||||
private async overwriteClineMessages(newMessages: ClineMessage[]) {
|
||||
this.clineMessages = newMessages
|
||||
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)
|
||||
}
|
||||
await saveClineMessagesAndUpdateHistory(
|
||||
this.getContext(),
|
||||
this.taskId,
|
||||
this.clineMessages,
|
||||
this.taskIsFavorited ?? false,
|
||||
this.conversationHistoryDeletedRange,
|
||||
this.checkpointTracker,
|
||||
this.updateTaskHistory,
|
||||
)
|
||||
}
|
||||
|
||||
async restoreCheckpoint(messageTs: number, restoreType: ClineCheckpointRestore, offset?: number) {
|
||||
@@ -515,9 +496,15 @@ export class Task {
|
||||
})
|
||||
}
|
||||
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
|
||||
sendRelinquishControlEvent()
|
||||
await saveClineMessagesAndUpdateHistory(
|
||||
this.getContext(),
|
||||
this.taskId,
|
||||
this.clineMessages,
|
||||
this.taskIsFavorited ?? false,
|
||||
this.conversationHistoryDeletedRange,
|
||||
this.checkpointTracker,
|
||||
this.updateTaskHistory,
|
||||
)
|
||||
|
||||
this.cancelTask() // the task is already cancelled by the provider beforehand, but we need to re-init to get the updated messages
|
||||
} else {
|
||||
@@ -790,7 +777,15 @@ export class Task {
|
||||
// lastMessage.ts = askTs
|
||||
lastMessage.text = text
|
||||
lastMessage.partial = false
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
await saveClineMessagesAndUpdateHistory(
|
||||
this.getContext(),
|
||||
this.taskId,
|
||||
this.clineMessages,
|
||||
this.taskIsFavorited ?? false,
|
||||
this.conversationHistoryDeletedRange,
|
||||
this.checkpointTracker,
|
||||
this.updateTaskHistory,
|
||||
)
|
||||
// await this.postStateToWebview()
|
||||
const protoMessage = convertClineMessageToProto(lastMessage)
|
||||
await sendPartialMessageEvent(protoMessage)
|
||||
@@ -898,7 +893,15 @@ export class Task {
|
||||
lastMessage.partial = false
|
||||
|
||||
// instead of streaming partialMessage events, we do a save and post like normal to persist to disk
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
await saveClineMessagesAndUpdateHistory(
|
||||
this.getContext(),
|
||||
this.taskId,
|
||||
this.clineMessages,
|
||||
this.taskIsFavorited ?? false,
|
||||
this.conversationHistoryDeletedRange,
|
||||
this.checkpointTracker,
|
||||
this.updateTaskHistory,
|
||||
)
|
||||
// await this.postStateToWebview()
|
||||
const protoMessage = convertClineMessageToProto(lastMessage)
|
||||
await sendPartialMessageEvent(protoMessage) // more performant than an entire postStateToWebview
|
||||
@@ -947,8 +950,15 @@ 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 this.saveClineMessagesAndUpdateHistory()
|
||||
await this.postStateToWebview()
|
||||
await saveClineMessagesAndUpdateHistory(
|
||||
this.getContext(),
|
||||
this.taskId,
|
||||
this.clineMessages,
|
||||
this.taskIsFavorited ?? false,
|
||||
this.conversationHistoryDeletedRange,
|
||||
this.checkpointTracker,
|
||||
this.updateTaskHistory,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1232,7 +1242,15 @@ export class Task {
|
||||
const lastCheckpointMessage = findLast(this.clineMessages, (m) => m.say === "checkpoint_created")
|
||||
if (lastCheckpointMessage) {
|
||||
lastCheckpointMessage.lastCheckpointHash = commitHash
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
await saveClineMessagesAndUpdateHistory(
|
||||
this.getContext(),
|
||||
this.taskId,
|
||||
this.clineMessages,
|
||||
this.taskIsFavorited ?? false,
|
||||
this.conversationHistoryDeletedRange,
|
||||
this.checkpointTracker,
|
||||
this.updateTaskHistory,
|
||||
)
|
||||
}
|
||||
}) // silently fails for now
|
||||
|
||||
@@ -1264,7 +1282,15 @@ export class Task {
|
||||
)
|
||||
if (lastCompletionResultMessage) {
|
||||
lastCompletionResultMessage.lastCheckpointHash = commitHash
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
await saveClineMessagesAndUpdateHistory(
|
||||
this.getContext(),
|
||||
this.taskId,
|
||||
this.clineMessages,
|
||||
this.taskIsFavorited ?? false,
|
||||
this.conversationHistoryDeletedRange,
|
||||
this.checkpointTracker,
|
||||
this.updateTaskHistory,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
console.error("Checkpoint tracker does not exist and could not be initialized for attempt completion")
|
||||
@@ -1453,9 +1479,9 @@ export class Task {
|
||||
chunkTimer = setTimeout(async () => await flushBuffer(), CHUNK_DEBOUNCE_MS)
|
||||
}
|
||||
|
||||
let result = ""
|
||||
const outputLines: string[] = []
|
||||
process.on("line", async (line) => {
|
||||
result += line + "\n"
|
||||
outputLines.push(line)
|
||||
|
||||
if (!didContinue) {
|
||||
outputBuffer.push(line)
|
||||
@@ -1497,7 +1523,7 @@ export class Task {
|
||||
// grouping command_output messages despite any gaps anyways)
|
||||
await setTimeoutPromise(50)
|
||||
|
||||
result = result.trim()
|
||||
let result = this.terminalManager.processOutput(outputLines)
|
||||
|
||||
if (userFeedback) {
|
||||
await this.say("user_feedback", userFeedback.text, userFeedback.images, userFeedback.files)
|
||||
@@ -1702,7 +1728,15 @@ export class Task {
|
||||
|
||||
if (contextManagementMetadata.updatedConversationHistoryDeletedRange) {
|
||||
this.conversationHistoryDeletedRange = contextManagementMetadata.conversationHistoryDeletedRange
|
||||
await this.saveClineMessagesAndUpdateHistory() // saves task history item which we use to keep track of conversation history deleted range
|
||||
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
|
||||
}
|
||||
|
||||
let stream = this.api.createMessage(systemPrompt, contextManagementMetadata.truncatedConversationHistory)
|
||||
@@ -1727,7 +1761,15 @@ export class Task {
|
||||
this.conversationHistoryDeletedRange,
|
||||
"quarter", // Force aggressive truncation
|
||||
)
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
await saveClineMessagesAndUpdateHistory(
|
||||
this.getContext(),
|
||||
this.taskId,
|
||||
this.clineMessages,
|
||||
this.taskIsFavorited ?? false,
|
||||
this.conversationHistoryDeletedRange,
|
||||
this.checkpointTracker,
|
||||
this.updateTaskHistory,
|
||||
)
|
||||
await this.contextManager.triggerApplyStandardContextTruncationNoticeChange(
|
||||
Date.now(),
|
||||
await ensureTaskDirectoryExists(this.getContext(), this.taskId),
|
||||
@@ -1741,7 +1783,15 @@ export class Task {
|
||||
this.conversationHistoryDeletedRange,
|
||||
"quarter", // Force aggressive truncation
|
||||
)
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
await saveClineMessagesAndUpdateHistory(
|
||||
this.getContext(),
|
||||
this.taskId,
|
||||
this.clineMessages,
|
||||
this.taskIsFavorited ?? false,
|
||||
this.conversationHistoryDeletedRange,
|
||||
this.checkpointTracker,
|
||||
this.updateTaskHistory,
|
||||
)
|
||||
await this.contextManager.triggerApplyStandardContextTruncationNoticeChange(
|
||||
Date.now(),
|
||||
await ensureTaskDirectoryExists(this.getContext(), this.taskId),
|
||||
@@ -3505,8 +3555,15 @@ export class Task {
|
||||
...sharedMessage,
|
||||
selected: text,
|
||||
} satisfies ClineAskQuestion)
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
telemetryService.captureOptionSelected(this.taskId, options.length, "act")
|
||||
await saveClineMessagesAndUpdateHistory(
|
||||
this.getContext(),
|
||||
this.taskId,
|
||||
this.clineMessages,
|
||||
this.taskIsFavorited ?? false,
|
||||
this.conversationHistoryDeletedRange,
|
||||
this.checkpointTracker,
|
||||
this.updateTaskHistory,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Option not selected, send user feedback
|
||||
@@ -3638,7 +3695,15 @@ export class Task {
|
||||
this.conversationHistoryDeletedRange,
|
||||
keepStrategy,
|
||||
)
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
await saveClineMessagesAndUpdateHistory(
|
||||
this.getContext(),
|
||||
this.taskId,
|
||||
this.clineMessages,
|
||||
this.taskIsFavorited ?? false,
|
||||
this.conversationHistoryDeletedRange,
|
||||
this.checkpointTracker,
|
||||
this.updateTaskHistory,
|
||||
)
|
||||
await this.contextManager.triggerApplyStandardContextTruncationNoticeChange(
|
||||
Date.now(),
|
||||
await ensureTaskDirectoryExists(this.getContext(), this.taskId),
|
||||
@@ -3939,8 +4004,15 @@ export class Task {
|
||||
...sharedMessage,
|
||||
selected: text,
|
||||
} satisfies ClinePlanModeResponse)
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
telemetryService.captureOptionSelected(this.taskId, options.length, "plan")
|
||||
await saveClineMessagesAndUpdateHistory(
|
||||
this.getContext(),
|
||||
this.taskId,
|
||||
this.clineMessages,
|
||||
this.taskIsFavorited ?? false,
|
||||
this.conversationHistoryDeletedRange,
|
||||
this.checkpointTracker,
|
||||
this.updateTaskHistory,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Option not selected, send user feedback
|
||||
@@ -4042,7 +4114,15 @@ export class Task {
|
||||
) {
|
||||
lastCompletionResultMessage.text += COMPLETION_RESULT_CHANGES_FLAG
|
||||
}
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
await saveClineMessagesAndUpdateHistory(
|
||||
this.getContext(),
|
||||
this.taskId,
|
||||
this.clineMessages,
|
||||
this.taskIsFavorited ?? false,
|
||||
this.conversationHistoryDeletedRange,
|
||||
this.checkpointTracker,
|
||||
this.updateTaskHistory,
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -4377,7 +4457,15 @@ export class Task {
|
||||
this.clineMessages[lastApiReqIndex].text = JSON.stringify({
|
||||
request: userContent.map((block) => formatContentBlockToMarkdown(block)).join("\n\n"),
|
||||
} satisfies ClineApiReqInfo)
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
await saveClineMessagesAndUpdateHistory(
|
||||
this.getContext(),
|
||||
this.taskId,
|
||||
this.clineMessages,
|
||||
this.taskIsFavorited ?? false,
|
||||
this.conversationHistoryDeletedRange,
|
||||
this.checkpointTracker,
|
||||
this.updateTaskHistory,
|
||||
)
|
||||
await this.postStateToWebview()
|
||||
|
||||
try {
|
||||
@@ -4448,7 +4536,15 @@ 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 this.saveClineMessagesAndUpdateHistory()
|
||||
await saveClineMessagesAndUpdateHistory(
|
||||
this.getContext(),
|
||||
this.taskId,
|
||||
this.clineMessages,
|
||||
this.taskIsFavorited ?? false,
|
||||
this.conversationHistoryDeletedRange,
|
||||
this.checkpointTracker,
|
||||
this.updateTaskHistory,
|
||||
)
|
||||
|
||||
telemetryService.captureConversationTurnEvent(
|
||||
this.taskId,
|
||||
@@ -4574,7 +4670,15 @@ export class Task {
|
||||
totalCost = apiStreamUsage.totalCost
|
||||
}
|
||||
updateApiReqMsg()
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
await saveClineMessagesAndUpdateHistory(
|
||||
this.getContext(),
|
||||
this.taskId,
|
||||
this.clineMessages,
|
||||
this.taskIsFavorited ?? false,
|
||||
this.conversationHistoryDeletedRange,
|
||||
this.checkpointTracker,
|
||||
this.updateTaskHistory,
|
||||
)
|
||||
await this.postStateToWebview()
|
||||
})
|
||||
}
|
||||
@@ -4598,7 +4702,15 @@ export class Task {
|
||||
}
|
||||
|
||||
updateApiReqMsg()
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
await saveClineMessagesAndUpdateHistory(
|
||||
this.getContext(),
|
||||
this.taskId,
|
||||
this.clineMessages,
|
||||
this.taskIsFavorited ?? false,
|
||||
this.conversationHistoryDeletedRange,
|
||||
this.checkpointTracker,
|
||||
this.updateTaskHistory,
|
||||
)
|
||||
await this.postStateToWebview()
|
||||
|
||||
// now add to apiconversationhistory
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
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"
|
||||
|
||||
@@ -95,6 +96,8 @@ 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
|
||||
@@ -218,12 +221,18 @@ 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)
|
||||
const matchingTerminal = terminals.find((t) => {
|
||||
if (t.busy) {
|
||||
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) {
|
||||
return false
|
||||
@@ -237,7 +246,7 @@ export class TerminalManager {
|
||||
|
||||
// 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)
|
||||
const availableTerminal = terminals.find((t) => !t.busy && t.shellPath === expectedShellPath)
|
||||
if (availableTerminal) {
|
||||
// Set up promise and tracking for CWD change
|
||||
const cwdPromise = new Promise<void>((resolve, reject) => {
|
||||
@@ -275,8 +284,8 @@ export class TerminalManager {
|
||||
}
|
||||
}
|
||||
|
||||
// If all terminals are busy, create a new one
|
||||
const newTerminalInfo = TerminalRegistry.createTerminal(cwd)
|
||||
// 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)
|
||||
this.terminalIds.add(newTerminalInfo.id)
|
||||
return newTerminalInfo
|
||||
}
|
||||
@@ -318,4 +327,117 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ export interface TerminalInfo {
|
||||
busy: boolean
|
||||
lastCommand: string
|
||||
id: number
|
||||
shellPath?: string
|
||||
lastActive: number
|
||||
pendingCwdChange?: string
|
||||
cwdResolved?: {
|
||||
resolve: () => void
|
||||
@@ -18,17 +20,26 @@ export class TerminalRegistry {
|
||||
private static terminals: TerminalInfo[] = []
|
||||
private static nextTerminalId = 1
|
||||
|
||||
static createTerminal(cwd?: string | vscode.Uri | undefined): TerminalInfo {
|
||||
const terminal = vscode.window.createTerminal({
|
||||
static createTerminal(cwd?: string | vscode.Uri | undefined, shellPath?: string): TerminalInfo {
|
||||
const terminalOptions: vscode.TerminalOptions = {
|
||||
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,55 +1,81 @@
|
||||
import { globby, Options } from "globby"
|
||||
import os from "os"
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
import { arePathsEqual } from "@utils/path"
|
||||
|
||||
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.
|
||||
// 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 {
|
||||
const root = process.platform === "win32" ? path.parse(absolutePath).root : "/"
|
||||
const isRoot = arePathsEqual(absolutePath, root)
|
||||
if (isRoot) {
|
||||
return [[root], false]
|
||||
return true
|
||||
}
|
||||
|
||||
const homeDir = os.homedir()
|
||||
const isHomeDir = arePathsEqual(absolutePath, homeDir)
|
||||
if (isHomeDir) {
|
||||
return [[homeDir], false]
|
||||
return true
|
||||
}
|
||||
|
||||
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}/**`)
|
||||
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 options: Options = {
|
||||
cwd: dirPath,
|
||||
dot: true, // do not ignore hidden files/directories
|
||||
absolute: true,
|
||||
markDirectories: true, // Append a / on any directories matched (/ is used on windows as well, so dont use path.sep)
|
||||
markDirectories: true, // Append a / on any directories matched
|
||||
gitignore: recursive, // globby ignores any files that are gitignored
|
||||
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
|
||||
ignore: recursive ? buildIgnorePatterns(absolutePath) : undefined,
|
||||
onlyFiles: false, // include directories in results
|
||||
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]
|
||||
|
||||
+48
-26
@@ -16,49 +16,71 @@ 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(),
|
||||
// Explicitly disallow other types' fields
|
||||
url: z.undefined().optional(),
|
||||
headers: z.undefined().optional(),
|
||||
// Allow other fields for backward compatibility
|
||||
url: z.string().optional(),
|
||||
headers: z.record(z.string()).optional(),
|
||||
})
|
||||
.transform((data) => ({
|
||||
...data,
|
||||
type: "stdio" as const,
|
||||
}))
|
||||
.refine((data) => data.type === undefined || data.type === "stdio", { message: TYPE_ERROR_MESSAGE }),
|
||||
.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 }),
|
||||
// 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(),
|
||||
// Explicitly disallow other types' fields
|
||||
command: z.undefined().optional(),
|
||||
args: z.undefined().optional(),
|
||||
env: z.undefined().optional(),
|
||||
// Allow other fields for backward compatibility
|
||||
command: z.string().optional(),
|
||||
args: z.array(z.string()).optional(),
|
||||
env: z.record(z.string()).optional(),
|
||||
})
|
||||
.transform((data) => ({
|
||||
...data,
|
||||
type: "sse" as const,
|
||||
}))
|
||||
.refine((data) => data.type === undefined || data.type === "sse", { message: TYPE_ERROR_MESSAGE }),
|
||||
.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 }),
|
||||
// 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(),
|
||||
// Explicitly disallow other types' fields
|
||||
command: z.undefined().optional(),
|
||||
args: z.undefined().optional(),
|
||||
env: z.undefined().optional(),
|
||||
// Allow other fields for backward compatibility
|
||||
command: z.string().optional(),
|
||||
args: z.array(z.string()).optional(),
|
||||
env: z.record(z.string()).optional(),
|
||||
})
|
||||
.transform((data) => ({
|
||||
...data,
|
||||
type: "streamableHttp" as const,
|
||||
}))
|
||||
.refine((data) => data.type === undefined || data.type === "streamableHttp", {
|
||||
.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", {
|
||||
message: TYPE_ERROR_MESSAGE,
|
||||
}),
|
||||
])
|
||||
|
||||
@@ -82,6 +82,7 @@ export interface ExtensionState {
|
||||
clineMessages: ClineMessage[]
|
||||
currentTaskItem?: HistoryItem
|
||||
mcpMarketplaceEnabled?: boolean
|
||||
mcpRichDisplayEnabled: boolean
|
||||
planActSeparateModelsSetting: boolean
|
||||
enableCheckpointsSetting?: boolean
|
||||
platform: Platform
|
||||
@@ -90,6 +91,8 @@ export interface ExtensionState {
|
||||
telemetrySetting: TelemetrySetting
|
||||
shellIntegrationTimeout: number
|
||||
terminalReuseEnabled?: boolean
|
||||
terminalOutputLineLimit: number
|
||||
defaultTerminalProfile?: string
|
||||
uriScheme?: string
|
||||
userInfo?: {
|
||||
displayName: string | null
|
||||
|
||||
@@ -45,6 +45,7 @@ export interface WebviewMessage {
|
||||
mcpMarketplaceEnabled?: boolean
|
||||
mcpResponsesCollapsed?: boolean
|
||||
telemetrySetting?: TelemetrySetting
|
||||
mcpRichDisplayEnabled?: boolean
|
||||
mentionsRequestId?: string
|
||||
query?: string
|
||||
// For toggleFavoriteModel
|
||||
@@ -69,6 +70,7 @@ export interface WebviewMessage {
|
||||
offset?: number
|
||||
shellIntegrationTimeout?: number
|
||||
terminalReuseEnabled?: boolean
|
||||
defaultTerminalProfile?: string
|
||||
}
|
||||
|
||||
export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse"
|
||||
|
||||
@@ -25,6 +25,7 @@ export type ApiProvider =
|
||||
| "xai"
|
||||
| "sambanova"
|
||||
| "cerebras"
|
||||
| "sapaicore"
|
||||
|
||||
export interface ApiHandlerOptions {
|
||||
apiModelId?: string
|
||||
@@ -92,6 +93,12 @@ 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
|
||||
}
|
||||
|
||||
@@ -598,6 +605,9 @@ export const vertexModels = {
|
||||
cacheReadsPrice: 0.625,
|
||||
},
|
||||
],
|
||||
thinkingConfig: {
|
||||
maxBudget: 32768,
|
||||
},
|
||||
},
|
||||
"gemini-2.5-flash-preview-04-17": {
|
||||
maxTokens: 65536,
|
||||
@@ -766,6 +776,9 @@ export const geminiModels = {
|
||||
cacheReadsPrice: 0.625,
|
||||
},
|
||||
],
|
||||
thinkingConfig: {
|
||||
maxBudget: 32768,
|
||||
},
|
||||
},
|
||||
"gemini-2.5-flash-preview-05-20": {
|
||||
maxTokens: 65536,
|
||||
@@ -2401,3 +2414,125 @@ 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>
|
||||
|
||||
@@ -102,6 +102,13 @@ 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,
|
||||
@@ -208,6 +215,13 @@ 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,
|
||||
|
||||
// Arrays
|
||||
favoritedModelIds: protoConfig.favoritedModelIds || [],
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as health from "grpc-health-check"
|
||||
import { activate } from "../extension"
|
||||
import { Controller } from "../core/controller"
|
||||
import { extensionContext, outputChannel, postMessage } from "./vscode-context"
|
||||
import { packageDefinition, proto, log, camelToSnakeCase, snakeToCamelCase } from "./utils"
|
||||
import { getPackageDefinition, log } from "./utils"
|
||||
import { GrpcHandler, GrpcStreamingResponseHandler } from "./grpc-types"
|
||||
import { addServices } from "./server-setup"
|
||||
import { StreamingResponseHandler } from "@/core/controller/grpc-handler"
|
||||
@@ -22,10 +22,10 @@ function main() {
|
||||
healthImpl.addToServer(server)
|
||||
|
||||
// Add all the handlers for the ProtoBus services to the server.
|
||||
addServices(server, proto, controller, wrapHandler, wrapStreamingResponseHandler)
|
||||
addServices(server, controller, wrapHandler, wrapStreamingResponseHandler)
|
||||
|
||||
// Set up reflection.
|
||||
const reflection = new ReflectionService(packageDefinition)
|
||||
const reflection = new ReflectionService(getPackageDefinition())
|
||||
reflection.addToServer(server)
|
||||
|
||||
// Start the server.
|
||||
@@ -58,10 +58,8 @@ 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, 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))
|
||||
const result = await handler(controller, call.request)
|
||||
callback(null, result)
|
||||
} catch (err: any) {
|
||||
log(`gRPC handler error: ${call.getPath()}\n${err.stack}`)
|
||||
callback({
|
||||
@@ -83,9 +81,7 @@ function wrapStreamingResponseHandler<TRequest, TResponse>(
|
||||
|
||||
const responseHandler: StreamingResponseHandler = (response, isLast, sequenceNumber) => {
|
||||
try {
|
||||
// The grpc-js serializer expects the proto message to be in the same
|
||||
// case as the proto file. This is a work around until we find a solution.
|
||||
call.write(camelToSnakeCase(response)) // Use a bound version of call.write to maintain proper 'this' context
|
||||
call.write(response) // Use a bound version of call.write to maintain proper 'this' context
|
||||
|
||||
if (isLast === true) {
|
||||
log(`Closing stream for ${requestId}`)
|
||||
@@ -96,7 +92,7 @@ function wrapStreamingResponseHandler<TRequest, TResponse>(
|
||||
return Promise.reject(error)
|
||||
}
|
||||
}
|
||||
await handler(controller, snakeToCamelCase(call.request), responseHandler, requestId)
|
||||
await handler(controller, call.request, responseHandler, requestId)
|
||||
} catch (err: any) {
|
||||
log(`gRPC handler error: ${call.getPath()}\n${err.stack}`)
|
||||
call.destroy({
|
||||
|
||||
+8
-54
@@ -8,58 +8,12 @@ const log = (...args: unknown[]) => {
|
||||
console.log(`[${timestamp}]`, "#bot.cline.server.ts", ...args)
|
||||
}
|
||||
|
||||
// Load service definitions.
|
||||
const descriptorSet = fs.readFileSync("proto/descriptor_set.pb")
|
||||
const clineDef = protoLoader.loadFileDescriptorSetFromBuffer(descriptorSet)
|
||||
const healthDef = protoLoader.loadSync(health.protoPath)
|
||||
const packageDefinition = { ...clineDef, ...healthDef }
|
||||
const proto = grpc.loadPackageDefinition(packageDefinition) as unknown
|
||||
|
||||
// Helper function to convert camelCase to snake_case
|
||||
function camelToSnakeCase(obj: any): any {
|
||||
if (obj === null || typeof obj !== "object") {
|
||||
return obj
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(camelToSnakeCase)
|
||||
}
|
||||
|
||||
return Object.keys(obj).reduce((acc: any, key: string) => {
|
||||
// Convert key from camelCase to snake_case
|
||||
const snakeKey = key
|
||||
.replace(/([A-Z])/g, "_$1")
|
||||
.replace(/^_+/, "")
|
||||
.toLowerCase()
|
||||
|
||||
// Convert value recursively if it's an object
|
||||
const value = obj[key]
|
||||
acc[snakeKey] = camelToSnakeCase(value)
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
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
|
||||
}
|
||||
|
||||
// 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 }
|
||||
export { getPackageDefinition, log }
|
||||
|
||||
+107
-1
@@ -179,7 +179,113 @@ function getShellFromEnv(): string | null {
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
// 4) Publicly Exposed Shell Getter
|
||||
// 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
|
||||
// -----------------------------------------------------
|
||||
|
||||
export function getShell(): string {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
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"
|
||||
@@ -9,6 +11,8 @@ 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"
|
||||
@@ -28,76 +32,19 @@ import {
|
||||
} from "@shared/ExtensionMessage"
|
||||
import { COMMAND_OUTPUT_STRING, COMMAND_REQ_APP_STRING } from "@shared/combineCommandSequences"
|
||||
import { Int64Request, StringRequest } from "@shared/proto/common"
|
||||
import { useEvent, useSize } from "react-use"
|
||||
import styled from "styled-components"
|
||||
import { CheckpointControls } from "../common/CheckpointControls"
|
||||
|
||||
import CodeAccordian, { cleanPathPrefix } from "../common/CodeAccordian"
|
||||
import { CheckpointControls } from "../common/CheckpointControls"
|
||||
import NewTaskPreview from "./NewTaskPreview"
|
||||
import QuoteButton from "./QuoteButton"
|
||||
import ReportBugPreview from "./ReportBugPreview"
|
||||
import UserMessage from "./UserMessage"
|
||||
|
||||
interface CopyButtonProps {
|
||||
textToCopy: string | undefined
|
||||
}
|
||||
import QuoteButton from "./QuoteButton"
|
||||
|
||||
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;
|
||||
@@ -1086,7 +1033,11 @@ export const ChatRowContent = ({
|
||||
)
|
||||
case "text":
|
||||
return (
|
||||
<WithCopyButton ref={contentRef} onMouseUp={handleMouseUp} textToCopy={message.text}>
|
||||
<WithCopyButton
|
||||
ref={contentRef}
|
||||
onMouseUp={handleMouseUp}
|
||||
textToCopy={message.text}
|
||||
position="bottom-right">
|
||||
<Markdown markdown={message.text} />
|
||||
{quoteButtonState.visible && (
|
||||
<QuoteButton
|
||||
@@ -1323,6 +1274,7 @@ export const ChatRowContent = ({
|
||||
ref={contentRef}
|
||||
onMouseUp={handleMouseUp}
|
||||
textToCopy={text}
|
||||
position="bottom-right"
|
||||
style={{
|
||||
color: "var(--vscode-charts-green)",
|
||||
paddingTop: 10,
|
||||
@@ -1487,6 +1439,7 @@ export const ChatRowContent = ({
|
||||
ref={contentRef}
|
||||
onMouseUp={handleMouseUp}
|
||||
textToCopy={text}
|
||||
position="bottom-right"
|
||||
style={{
|
||||
color: "var(--vscode-charts-green)",
|
||||
paddingTop: 10,
|
||||
@@ -1557,6 +1510,7 @@ export const ChatRowContent = ({
|
||||
ref={contentRef}
|
||||
onMouseUp={handleMouseUp}
|
||||
textToCopy={question}
|
||||
position="bottom-right"
|
||||
style={{ paddingTop: 10 }}>
|
||||
<Markdown markdown={question} />
|
||||
<OptionsButtons
|
||||
@@ -1640,7 +1594,7 @@ export const ChatRowContent = ({
|
||||
response = message.text
|
||||
}
|
||||
return (
|
||||
<WithCopyButton ref={contentRef} onMouseUp={handleMouseUp} textToCopy={response}>
|
||||
<WithCopyButton ref={contentRef} onMouseUp={handleMouseUp} textToCopy={response} position="bottom-right">
|
||||
<Markdown markdown={response} />
|
||||
<OptionsButtons
|
||||
options={options}
|
||||
|
||||
@@ -1576,7 +1576,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
|
||||
Type @ for context, / for slash commands & workflows, hold shift to drag in files/images
|
||||
</div>
|
||||
)}
|
||||
{(selectedImages.length > 0 || selectedFiles.length > 0) && (
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
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,17 +1,18 @@
|
||||
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 type { ComponentProps } 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 type { Node } from "unist"
|
||||
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"
|
||||
|
||||
// Styled component for Act Mode text with more specific styling
|
||||
const ActModeHighlight: React.FC = () => (
|
||||
@@ -178,24 +179,6 @@ 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};
|
||||
@@ -337,7 +320,6 @@ 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) {
|
||||
@@ -345,22 +327,19 @@ const PreWithCopyButton = ({
|
||||
const textToCopy = codeElement ? codeElement.textContent : preRef.current.textContent
|
||||
|
||||
if (!textToCopy) return
|
||||
navigator.clipboard.writeText(textToCopy).then(() => {
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 1500)
|
||||
})
|
||||
return textToCopy
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const styledPreProps = theme ? { ...preProps, theme } : preProps
|
||||
|
||||
return (
|
||||
<CodeBlockContainer>
|
||||
<CopyButton appearance="icon" onClick={handleCopy} aria-label={copied ? "Copied" : "Copy"}>
|
||||
<span className={`codicon codicon-${copied ? "check" : "copy"}`}></span>
|
||||
</CopyButton>
|
||||
<StyledPre {...preProps} theme={theme} ref={preRef}>
|
||||
<WithCopyButton onCopy={handleCopy} position="top-right" ariaLabel="Copy code">
|
||||
<StyledPre {...styledPreProps} ref={preRef}>
|
||||
{children}
|
||||
</StyledPre>
|
||||
</CodeBlockContainer>
|
||||
</WithCopyButton>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -117,36 +117,19 @@ interface UrlMatch {
|
||||
}
|
||||
|
||||
const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText }) => {
|
||||
const { mcpResponsesCollapsed } = useExtensionState() // Get setting from context
|
||||
const { mcpResponsesCollapsed, mcpRichDisplayEnabled } = 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">(() => {
|
||||
// Get saved preference from localStorage, default to 'rich'
|
||||
const savedMode = localStorage.getItem("mcpDisplayMode")
|
||||
return savedMode === "plain" ? "plain" : "rich"
|
||||
// Initialize directly from the global setting.
|
||||
return mcpRichDisplayEnabled ? "rich" : "plain"
|
||||
})
|
||||
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(() => {
|
||||
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])
|
||||
setDisplayMode((prevMode) => (prevMode === "rich" ? "plain" : "rich"))
|
||||
}, [])
|
||||
|
||||
const toggleExpand = useCallback(() => {
|
||||
setIsExpanded((prev) => !prev)
|
||||
@@ -155,14 +138,16 @@ 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)
|
||||
setUrlMatches([]) // Clear any existing matches when in plain mode
|
||||
if (urlMatches.length > 0) {
|
||||
setUrlMatches([]) // Clear any existing matches when in plain mode
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -280,7 +265,7 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
|
||||
processingCanceled = true
|
||||
console.log("Cleaning up URL processing")
|
||||
}
|
||||
}, [responseText, displayMode, forceUpdateCounter, isExpanded])
|
||||
}, [responseText, displayMode, isExpanded])
|
||||
|
||||
// Function to render content based on display mode
|
||||
const renderContent = () => {
|
||||
|
||||
@@ -46,6 +46,8 @@ import {
|
||||
vertexModels,
|
||||
xaiDefaultModelId,
|
||||
xaiModels,
|
||||
sapAiCoreDefaultModelId,
|
||||
sapAiCoreModels,
|
||||
} from "@shared/api"
|
||||
import { EmptyRequest, StringRequest } from "@shared/proto/common"
|
||||
import { OpenAiModelsRequest, UpdateApiConfigurationRequest } from "@shared/proto/models"
|
||||
@@ -70,6 +72,7 @@ import OllamaModelPicker from "./OllamaModelPicker"
|
||||
import OpenRouterModelPicker, { ModelDescriptionMarkdown, OPENROUTER_MODEL_PICKER_Z_INDEX } from "./OpenRouterModelPicker"
|
||||
import RequestyModelPicker from "./RequestyModelPicker"
|
||||
import ThinkingBudgetSlider from "./ThinkingBudgetSlider"
|
||||
import { ExtensionMessage } from "@shared/ExtensionMessage"
|
||||
|
||||
interface ApiOptionsProps {
|
||||
showModelOptions: boolean
|
||||
@@ -119,7 +122,14 @@ const OpenRouterBalanceDisplay = ({ apiKey }: { apiKey: string }) => {
|
||||
|
||||
const SUPPORTED_THINKING_MODELS: Record<string, string[]> = {
|
||||
anthropic: ["claude-3-7-sonnet-20250219", "claude-sonnet-4-20250514", "claude-opus-4-20250514"],
|
||||
vertex: ["claude-3-7-sonnet@20250219", "claude-sonnet-4@20250514", "claude-opus-4@20250514"],
|
||||
vertex: [
|
||||
"claude-3-7-sonnet@20250219",
|
||||
"claude-sonnet-4@20250514",
|
||||
"claude-opus-4@20250514",
|
||||
"gemini-2.5-flash-preview-05-20",
|
||||
"gemini-2.5-flash-preview-04-17",
|
||||
"gemini-2.5-pro-preview-06-05",
|
||||
],
|
||||
qwen: [
|
||||
"qwen3-235b-a22b",
|
||||
"qwen3-32b",
|
||||
@@ -132,6 +142,7 @@ const SUPPORTED_THINKING_MODELS: Record<string, string[]> = {
|
||||
"qwen-plus-latest",
|
||||
"qwen-turbo-latest",
|
||||
],
|
||||
gemini: ["gemini-2.5-flash-preview-05-20", "gemini-2.5-flash-preview-04-17", "gemini-2.5-pro-preview-06-05"],
|
||||
}
|
||||
|
||||
// This is necessary to ensure dropdown opens downward, important for when this is used in popup
|
||||
@@ -366,6 +377,7 @@ const ApiOptions = ({
|
||||
<VSCodeOption value="xai">xAI</VSCodeOption>
|
||||
<VSCodeOption value="sambanova">SambaNova</VSCodeOption>
|
||||
<VSCodeOption value="cerebras">Cerebras</VSCodeOption>
|
||||
<VSCodeOption value="sapaicore">SAP AI Core</VSCodeOption>
|
||||
</VSCodeDropdown>
|
||||
</DropdownContainer>
|
||||
|
||||
@@ -1057,15 +1069,6 @@ const ApiOptions = ({
|
||||
</VSCodeLink>
|
||||
)}
|
||||
</p>
|
||||
|
||||
{/* Add Thinking Budget Slider specifically for gemini-2.5-flash-preview-04-17 */}
|
||||
{selectedProvider === "gemini" && selectedModelId === "gemini-2.5-flash-preview-04-17" && (
|
||||
<ThinkingBudgetSlider
|
||||
apiConfiguration={apiConfiguration}
|
||||
setApiConfiguration={setApiConfiguration}
|
||||
maxBudget={selectedModelInfo.thinkingConfig?.maxBudget}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -2082,6 +2085,71 @@ const ApiOptions = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedProvider === "sapaicore" && (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.sapAiCoreClientId || ""}
|
||||
style={{ width: "100%" }}
|
||||
type="password"
|
||||
onInput={handleInputChange("sapAiCoreClientId")}
|
||||
placeholder="Enter AI Core Client Id...">
|
||||
<span style={{ fontWeight: 500 }}>AI Core Client Id</span>
|
||||
</VSCodeTextField>
|
||||
{apiConfiguration?.sapAiCoreClientId && (
|
||||
<p style={{ fontSize: "12px", color: "var(--vscode-descriptionForeground)" }}>
|
||||
Client Id is set. To change it, please re-enter the value.
|
||||
</p>
|
||||
)}
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.sapAiCoreClientSecret ? "********" : ""}
|
||||
style={{ width: "100%" }}
|
||||
type="password"
|
||||
onInput={handleInputChange("sapAiCoreClientSecret")}
|
||||
placeholder="Enter AI Core Client Secret...">
|
||||
<span style={{ fontWeight: 500 }}>AI Core Client Secret</span>
|
||||
</VSCodeTextField>
|
||||
{apiConfiguration?.sapAiCoreClientSecret && (
|
||||
<p style={{ fontSize: "12px", color: "var(--vscode-descriptionForeground)" }}>
|
||||
Client Secret is set. To change it, please re-enter the value.
|
||||
</p>
|
||||
)}
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.sapAiCoreBaseUrl || ""}
|
||||
style={{ width: "100%" }}
|
||||
onInput={handleInputChange("sapAiCoreBaseUrl")}
|
||||
placeholder="Enter AI Core Base URL...">
|
||||
<span style={{ fontWeight: 500 }}>AI Core Base URL</span>
|
||||
</VSCodeTextField>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.sapAiCoreTokenUrl || ""}
|
||||
style={{ width: "100%" }}
|
||||
onInput={handleInputChange("sapAiCoreTokenUrl")}
|
||||
placeholder="Enter AI Core Auth URL...">
|
||||
<span style={{ fontWeight: 500 }}>AI Core Auth URL</span>
|
||||
</VSCodeTextField>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.sapAiResourceGroup || ""}
|
||||
style={{ width: "100%" }}
|
||||
onInput={handleInputChange("sapAiResourceGroup")}
|
||||
placeholder="Enter AI Core Resource Group...">
|
||||
<span style={{ fontWeight: 500 }}>AI Core Resource Group</span>
|
||||
</VSCodeTextField>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: "5px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
These credentials are stored locally and only used to make API requests from this extension.
|
||||
<VSCodeLink
|
||||
href="https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/access-sap-ai-core-via-api"
|
||||
style={{ display: "inline" }}>
|
||||
You can find more information about SAP AI Core API access here.
|
||||
</VSCodeLink>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{apiErrorMessage && (
|
||||
<p
|
||||
style={{
|
||||
@@ -2201,6 +2269,7 @@ const ApiOptions = ({
|
||||
{selectedProvider === "sambanova" && createDropdown(sambanovaModels)}
|
||||
{selectedProvider === "cerebras" && createDropdown(cerebrasModels)}
|
||||
{selectedProvider === "nebius" && createDropdown(nebiusModels)}
|
||||
{selectedProvider === "sapaicore" && createDropdown(sapAiCoreModels)}
|
||||
</DropdownContainer>
|
||||
|
||||
{SUPPORTED_THINKING_MODELS[selectedProvider]?.includes(selectedModelId) && (
|
||||
@@ -2632,6 +2701,8 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration):
|
||||
return getProviderData(sambanovaModels, sambanovaDefaultModelId)
|
||||
case "cerebras":
|
||||
return getProviderData(cerebrasModels, cerebrasDefaultModelId)
|
||||
case "sapaicore":
|
||||
return getProviderData(sapAiCoreModels, sapAiCoreDefaultModelId)
|
||||
default:
|
||||
return getProviderData(anthropicModels, anthropicDefaultModelId)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ const FeatureSettingsSection = () => {
|
||||
setEnableCheckpointsSetting,
|
||||
mcpMarketplaceEnabled,
|
||||
setMcpMarketplaceEnabled,
|
||||
mcpRichDisplayEnabled,
|
||||
setMcpRichDisplayEnabled,
|
||||
mcpResponsesCollapsed,
|
||||
setMcpResponsesCollapsed,
|
||||
chatSettings,
|
||||
@@ -44,6 +46,19 @@ const FeatureSettingsSection = () => {
|
||||
Enables the MCP Marketplace tab for discovering and installing MCP servers.
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<VSCodeCheckbox
|
||||
checked={mcpRichDisplayEnabled}
|
||||
onChange={(e: any) => {
|
||||
const checked = e.target.checked === true
|
||||
setMcpRichDisplayEnabled(checked)
|
||||
}}>
|
||||
Enable Rich MCP Display
|
||||
</VSCodeCheckbox>
|
||||
<p className="text-xs text-[var(--vscode-descriptionForeground)]">
|
||||
Enables rich formatting for MCP responses. When disabled, responses will be shown in plain text.
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<VSCodeCheckbox
|
||||
checked={mcpResponsesCollapsed}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { cn } from "@/utils/cn"
|
||||
import { validateApiConfiguration, validateModelId } from "@/utils/validate"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { ExtensionMessage } from "@shared/ExtensionMessage"
|
||||
import { EmptyRequest } from "@shared/proto/common"
|
||||
import { EmptyRequest, StringRequest } from "@shared/proto/common"
|
||||
import { PlanActMode, TogglePlanActModeRequest, UpdateSettingsRequest } from "@shared/proto/state"
|
||||
import { VSCodeButton, VSCodeCheckbox, VSCodeLink, VSCodeTextArea } from "@vscode/webview-ui-toolkit/react"
|
||||
import { CheckCheck, FlaskConical, Info, LucideIcon, Settings, SquareMousePointer, SquareTerminal, Webhook } from "lucide-react"
|
||||
@@ -126,10 +126,16 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
setEnableCheckpointsSetting,
|
||||
mcpMarketplaceEnabled,
|
||||
setMcpMarketplaceEnabled,
|
||||
mcpRichDisplayEnabled,
|
||||
setMcpRichDisplayEnabled,
|
||||
shellIntegrationTimeout,
|
||||
setShellIntegrationTimeout,
|
||||
terminalOutputLineLimit,
|
||||
setTerminalOutputLineLimit,
|
||||
terminalReuseEnabled,
|
||||
setTerminalReuseEnabled,
|
||||
defaultTerminalProfile,
|
||||
setDefaultTerminalProfile,
|
||||
mcpResponsesCollapsed,
|
||||
setMcpResponsesCollapsed,
|
||||
setApiConfiguration,
|
||||
@@ -142,10 +148,13 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
planActSeparateModelsSetting,
|
||||
enableCheckpointsSetting,
|
||||
mcpMarketplaceEnabled,
|
||||
mcpRichDisplayEnabled,
|
||||
mcpResponsesCollapsed,
|
||||
chatSettings,
|
||||
shellIntegrationTimeout,
|
||||
terminalReuseEnabled,
|
||||
terminalOutputLineLimit,
|
||||
defaultTerminalProfile,
|
||||
})
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
|
||||
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
|
||||
@@ -180,6 +189,7 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
telemetrySetting,
|
||||
enableCheckpointsSetting,
|
||||
mcpMarketplaceEnabled,
|
||||
mcpRichDisplayEnabled,
|
||||
shellIntegrationTimeout,
|
||||
terminalReuseEnabled,
|
||||
mcpResponsesCollapsed,
|
||||
@@ -187,8 +197,32 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
? convertApiConfigurationToProtoApiConfiguration(apiConfigurationToSubmit)
|
||||
: undefined,
|
||||
chatSettings: chatSettings ? convertChatSettingsToProtoChatSettings(chatSettings) : undefined,
|
||||
terminalOutputLineLimit,
|
||||
}),
|
||||
)
|
||||
|
||||
// Update default terminal profile if it has changed
|
||||
if (defaultTerminalProfile !== originalState.current.defaultTerminalProfile) {
|
||||
await StateServiceClient.updateDefaultTerminalProfile({
|
||||
value: defaultTerminalProfile || "default",
|
||||
} as StringRequest)
|
||||
}
|
||||
|
||||
// Update the original state to reflect the saved changes
|
||||
originalState.current = {
|
||||
apiConfiguration,
|
||||
telemetrySetting,
|
||||
planActSeparateModelsSetting,
|
||||
enableCheckpointsSetting,
|
||||
mcpMarketplaceEnabled,
|
||||
mcpRichDisplayEnabled,
|
||||
mcpResponsesCollapsed,
|
||||
chatSettings,
|
||||
shellIntegrationTimeout,
|
||||
terminalReuseEnabled,
|
||||
terminalOutputLineLimit,
|
||||
defaultTerminalProfile,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to update settings:", error)
|
||||
}
|
||||
@@ -211,10 +245,14 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
planActSeparateModelsSetting !== originalState.current.planActSeparateModelsSetting ||
|
||||
enableCheckpointsSetting !== originalState.current.enableCheckpointsSetting ||
|
||||
mcpMarketplaceEnabled !== originalState.current.mcpMarketplaceEnabled ||
|
||||
mcpRichDisplayEnabled !== originalState.current.mcpRichDisplayEnabled ||
|
||||
JSON.stringify(chatSettings) !== JSON.stringify(originalState.current.chatSettings) ||
|
||||
mcpResponsesCollapsed !== originalState.current.mcpResponsesCollapsed ||
|
||||
JSON.stringify(chatSettings) !== JSON.stringify(originalState.current.chatSettings) ||
|
||||
shellIntegrationTimeout !== originalState.current.shellIntegrationTimeout ||
|
||||
terminalReuseEnabled !== originalState.current.terminalReuseEnabled
|
||||
terminalOutputLineLimit !== originalState.current.terminalOutputLineLimit ||
|
||||
terminalReuseEnabled !== originalState.current.terminalReuseEnabled ||
|
||||
defaultTerminalProfile !== originalState.current.defaultTerminalProfile
|
||||
|
||||
setHasUnsavedChanges(hasChanges)
|
||||
}, [
|
||||
@@ -223,10 +261,13 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
planActSeparateModelsSetting,
|
||||
enableCheckpointsSetting,
|
||||
mcpMarketplaceEnabled,
|
||||
mcpRichDisplayEnabled,
|
||||
mcpResponsesCollapsed,
|
||||
chatSettings,
|
||||
shellIntegrationTimeout,
|
||||
terminalReuseEnabled,
|
||||
terminalOutputLineLimit,
|
||||
defaultTerminalProfile,
|
||||
])
|
||||
|
||||
// Handle cancel button click
|
||||
@@ -256,13 +297,26 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
: false,
|
||||
)
|
||||
}
|
||||
if (typeof setMcpRichDisplayEnabled === "function") {
|
||||
setMcpRichDisplayEnabled(
|
||||
typeof originalState.current.mcpRichDisplayEnabled === "boolean"
|
||||
? originalState.current.mcpRichDisplayEnabled
|
||||
: true,
|
||||
)
|
||||
}
|
||||
// Reset terminal settings
|
||||
if (typeof setShellIntegrationTimeout === "function") {
|
||||
setShellIntegrationTimeout(originalState.current.shellIntegrationTimeout)
|
||||
}
|
||||
if (typeof setTerminalOutputLineLimit === "function") {
|
||||
setTerminalOutputLineLimit(originalState.current.terminalOutputLineLimit)
|
||||
}
|
||||
if (typeof setTerminalReuseEnabled === "function") {
|
||||
setTerminalReuseEnabled(originalState.current.terminalReuseEnabled ?? true)
|
||||
}
|
||||
if (typeof setDefaultTerminalProfile === "function") {
|
||||
setDefaultTerminalProfile(originalState.current.defaultTerminalProfile ?? "default")
|
||||
}
|
||||
if (typeof setMcpResponsesCollapsed === "function") {
|
||||
setMcpResponsesCollapsed(originalState.current.mcpResponsesCollapsed ?? false)
|
||||
}
|
||||
@@ -282,6 +336,7 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
setApiConfiguration,
|
||||
setEnableCheckpointsSetting,
|
||||
setMcpMarketplaceEnabled,
|
||||
setMcpRichDisplayEnabled,
|
||||
setMcpResponsesCollapsed,
|
||||
])
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from "react"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
|
||||
const TerminalOutputLineLimitSlider: React.FC = () => {
|
||||
const { terminalOutputLineLimit, setTerminalOutputLineLimit } = useExtensionState()
|
||||
|
||||
const handleSliderChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = parseInt(event.target.value, 10)
|
||||
setTerminalOutputLineLimit(value)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ marginBottom: 15 }}>
|
||||
<label htmlFor="terminal-output-limit" style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>
|
||||
Terminal output limit
|
||||
</label>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<input
|
||||
type="range"
|
||||
id="terminal-output-limit"
|
||||
min="100"
|
||||
max="5000"
|
||||
step="100"
|
||||
value={terminalOutputLineLimit ?? 500}
|
||||
onChange={handleSliderChange}
|
||||
style={{ flexGrow: 1, marginRight: "1rem" }}
|
||||
/>
|
||||
<span>{terminalOutputLineLimit ?? 500}</span>
|
||||
</div>
|
||||
<p style={{ fontSize: "12px", color: "var(--vscode-descriptionForeground)", margin: "5px 0 0 0" }}>
|
||||
Maximum number of lines to include in terminal output when executing commands. When exceeded, lines will be
|
||||
removed from the middle, saving tokens.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TerminalOutputLineLimitSlider
|
||||
@@ -1,10 +1,22 @@
|
||||
import React, { useState } from "react"
|
||||
import { VSCodeTextField, VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
|
||||
import React, { useState, useEffect } from "react"
|
||||
import { VSCodeTextField, VSCodeCheckbox, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import TerminalOutputLineLimitSlider from "./TerminalOutputLineLimitSlider"
|
||||
import { StateServiceClient } from "../../services/grpc-client"
|
||||
import { Int64, Int64Request } from "@shared/proto/common"
|
||||
|
||||
export const TerminalSettingsSection: React.FC = () => {
|
||||
const { shellIntegrationTimeout, setShellIntegrationTimeout, terminalReuseEnabled, setTerminalReuseEnabled } =
|
||||
useExtensionState()
|
||||
const {
|
||||
shellIntegrationTimeout,
|
||||
setShellIntegrationTimeout,
|
||||
terminalReuseEnabled,
|
||||
setTerminalReuseEnabled,
|
||||
defaultTerminalProfile,
|
||||
setDefaultTerminalProfile,
|
||||
availableTerminalProfiles,
|
||||
platform,
|
||||
} = useExtensionState()
|
||||
|
||||
const [inputValue, setInputValue] = useState((shellIntegrationTimeout / 1000).toString())
|
||||
const [inputError, setInputError] = useState<string | null>(null)
|
||||
|
||||
@@ -21,14 +33,23 @@ export const TerminalSettingsSection: React.FC = () => {
|
||||
}
|
||||
|
||||
setInputError(null)
|
||||
const timeout = Math.round(seconds * 1000) // Convert to milliseconds
|
||||
const timeout = Math.round(seconds * 1000)
|
||||
|
||||
// Update local state
|
||||
setShellIntegrationTimeout(timeout)
|
||||
|
||||
StateServiceClient.updateTerminalConnectionTimeout({
|
||||
value: timeout,
|
||||
} as Int64Request)
|
||||
.then((response: Int64) => {
|
||||
setShellIntegrationTimeout(response.value)
|
||||
setInputValue((response.value / 1000).toString())
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Failed to update terminal connection timeout:", error)
|
||||
})
|
||||
}
|
||||
|
||||
const handleInputBlur = () => {
|
||||
// If there was an error, reset the input to the current valid value
|
||||
if (inputError) {
|
||||
setInputValue((shellIntegrationTimeout / 1000).toString())
|
||||
setInputError(null)
|
||||
@@ -38,16 +59,44 @@ export const TerminalSettingsSection: React.FC = () => {
|
||||
const handleTerminalReuseChange = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
const checked = target.checked
|
||||
|
||||
// Update local state
|
||||
setTerminalReuseEnabled(checked)
|
||||
|
||||
// TODO: Send to extension using gRPC when the backend is ready
|
||||
// For now, we'll just update the local state
|
||||
StateServiceClient.updateTerminalReuseEnabled({ value: checked } as any).catch((error) => {
|
||||
console.error("Failed to update terminal reuse enabled:", error)
|
||||
})
|
||||
}
|
||||
|
||||
// Use any to avoid type conflicts between Event and FormEvent
|
||||
const handleDefaultTerminalProfileChange = (event: any) => {
|
||||
const target = event.target as HTMLSelectElement
|
||||
const profileId = target.value
|
||||
// Only update the local state, let the Save button handle the backend update
|
||||
setDefaultTerminalProfile(profileId)
|
||||
}
|
||||
|
||||
const profilesToShow = availableTerminalProfiles
|
||||
|
||||
return (
|
||||
<div id="terminal-settings-section" style={{ marginBottom: 20 }}>
|
||||
<div style={{ marginBottom: 15 }}>
|
||||
<label htmlFor="default-terminal-profile" style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>
|
||||
Default Terminal Profile
|
||||
</label>
|
||||
<VSCodeDropdown
|
||||
id="default-terminal-profile"
|
||||
value={defaultTerminalProfile || "default"}
|
||||
onChange={handleDefaultTerminalProfileChange}
|
||||
style={{ width: "100%" }}>
|
||||
{profilesToShow.map((profile) => (
|
||||
<VSCodeOption key={profile.id} value={profile.id} title={profile.description}>
|
||||
{profile.name}
|
||||
</VSCodeOption>
|
||||
))}
|
||||
</VSCodeDropdown>
|
||||
<p style={{ fontSize: "12px", color: "var(--vscode-descriptionForeground)", margin: "5px 0 0 0" }}>
|
||||
Select the default terminal Cline will use. 'Default' uses your VSCode global setting.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 15 }}>
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<label style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>
|
||||
@@ -85,6 +134,7 @@ export const TerminalSettingsSection: React.FC = () => {
|
||||
this if you experience issues with task lockout after a terminal command.
|
||||
</p>
|
||||
</div>
|
||||
<TerminalOutputLineLimitSlider />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { EmptyRequest, StringRequest } from "@shared/proto/common"
|
||||
import { UpdateSettingsRequest } from "@shared/proto/state"
|
||||
import { WebviewProviderType as WebviewProviderTypeEnum, WebviewProviderTypeRequest } from "@shared/proto/ui"
|
||||
import { TerminalProfile } from "@shared/proto/state"
|
||||
import { convertProtoToClineMessage } from "@shared/proto-conversions/cline-message"
|
||||
import { convertProtoMcpServersToMcpServers } from "@shared/proto-conversions/mcp/mcp-server-conversion"
|
||||
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "@shared/AutoApprovalSettings"
|
||||
@@ -41,6 +42,7 @@ interface ExtensionStateContextType extends ExtensionState {
|
||||
mcpMarketplaceCatalog: McpMarketplaceCatalog
|
||||
filePaths: string[]
|
||||
totalTasksSize: number | null
|
||||
availableTerminalProfiles: TerminalProfile[]
|
||||
|
||||
// View state
|
||||
showMcp: boolean
|
||||
@@ -58,9 +60,12 @@ interface ExtensionStateContextType extends ExtensionState {
|
||||
setPlanActSeparateModelsSetting: (value: boolean) => void
|
||||
setEnableCheckpointsSetting: (value: boolean) => void
|
||||
setMcpMarketplaceEnabled: (value: boolean) => void
|
||||
setMcpRichDisplayEnabled: (value: boolean) => void
|
||||
setMcpResponsesCollapsed: (value: boolean) => void
|
||||
setShellIntegrationTimeout: (value: number) => void
|
||||
setTerminalReuseEnabled: (value: boolean) => void
|
||||
setTerminalOutputLineLimit: (value: number) => void
|
||||
setDefaultTerminalProfile: (value: string) => void
|
||||
setChatSettings: (value: ChatSettings) => void
|
||||
setMcpServers: (value: McpServer[]) => void
|
||||
setGlobalClineRulesToggles: (toggles: Record<string, boolean>) => void
|
||||
@@ -71,6 +76,7 @@ interface ExtensionStateContextType extends ExtensionState {
|
||||
setGlobalWorkflowToggles: (toggles: Record<string, boolean>) => void
|
||||
setMcpMarketplaceCatalog: (value: McpMarketplaceCatalog) => void
|
||||
setTotalTasksSize: (value: number | null) => void
|
||||
setAvailableTerminalProfiles: (profiles: TerminalProfile[]) => void // Setter for profiles
|
||||
|
||||
// Refresh functions
|
||||
refreshOpenRouterModels: () => void
|
||||
@@ -180,14 +186,17 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
distinctId: "",
|
||||
planActSeparateModelsSetting: true,
|
||||
enableCheckpointsSetting: true,
|
||||
mcpRichDisplayEnabled: true,
|
||||
globalClineRulesToggles: {},
|
||||
localClineRulesToggles: {},
|
||||
localCursorRulesToggles: {},
|
||||
localWindsurfRulesToggles: {},
|
||||
localWorkflowToggles: {},
|
||||
globalWorkflowToggles: {},
|
||||
shellIntegrationTimeout: 4000, // default timeout for shell integration
|
||||
terminalReuseEnabled: true, // default to enabled for backward compatibility
|
||||
shellIntegrationTimeout: 4000,
|
||||
terminalReuseEnabled: true,
|
||||
terminalOutputLineLimit: 500,
|
||||
defaultTerminalProfile: "default",
|
||||
isNewUser: false,
|
||||
mcpResponsesCollapsed: false, // Default value (expanded), will be overwritten by extension state
|
||||
})
|
||||
@@ -199,6 +208,7 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
[openRouterDefaultModelId]: openRouterDefaultModelInfo,
|
||||
})
|
||||
const [totalTasksSize, setTotalTasksSize] = useState<number | null>(null)
|
||||
const [availableTerminalProfiles, setAvailableTerminalProfiles] = useState<TerminalProfile[]>([])
|
||||
|
||||
const [openAiModels, setOpenAiModels] = useState<string[]>([])
|
||||
const [requestyModels, setRequestyModels] = useState<Record<string, ModelInfo>>({
|
||||
@@ -307,6 +317,7 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
config.asksageApiKey,
|
||||
config.xaiApiKey,
|
||||
config.sambanovaApiKey,
|
||||
config.sapAiCoreClientId,
|
||||
].some((key) => key !== undefined)
|
||||
: false
|
||||
|
||||
@@ -541,6 +552,15 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
},
|
||||
})
|
||||
|
||||
// Fetch available terminal profiles on launch
|
||||
StateServiceClient.getAvailableTerminalProfiles(EmptyRequest.create({}))
|
||||
.then((response) => {
|
||||
setAvailableTerminalProfiles(response.profiles)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Failed to fetch available terminal profiles:", error)
|
||||
})
|
||||
|
||||
// Subscribe to relinquish control events
|
||||
relinquishControlUnsubscribeRef.current = UiServiceClient.subscribeToRelinquishControl(EmptyRequest.create({}), {
|
||||
onResponse: () => {
|
||||
@@ -656,6 +676,7 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
mcpMarketplaceCatalog,
|
||||
filePaths,
|
||||
totalTasksSize,
|
||||
availableTerminalProfiles,
|
||||
showMcp,
|
||||
mcpTab,
|
||||
showSettings,
|
||||
@@ -707,6 +728,11 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
...prevState,
|
||||
mcpMarketplaceEnabled: value,
|
||||
})),
|
||||
setMcpRichDisplayEnabled: (value) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
mcpRichDisplayEnabled: value,
|
||||
})),
|
||||
setMcpResponsesCollapsed: (value) => {
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
@@ -729,8 +755,19 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
...prevState,
|
||||
terminalReuseEnabled: value,
|
||||
})),
|
||||
setTerminalOutputLineLimit: (value) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
terminalOutputLineLimit: value,
|
||||
})),
|
||||
setDefaultTerminalProfile: (value) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
defaultTerminalProfile: value,
|
||||
})),
|
||||
setMcpServers: (mcpServers: McpServer[]) => setMcpServers(mcpServers),
|
||||
setMcpMarketplaceCatalog: (catalog: McpMarketplaceCatalog) => setMcpMarketplaceCatalog(catalog),
|
||||
setAvailableTerminalProfiles,
|
||||
setShowMcp,
|
||||
closeMcpView,
|
||||
setChatSettings: async (value) => {
|
||||
@@ -738,7 +775,6 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
...prevState,
|
||||
chatSettings: value,
|
||||
}))
|
||||
|
||||
try {
|
||||
// Import the conversion functions
|
||||
const { convertApiConfigurationToProtoApiConfiguration } = await import(
|
||||
@@ -758,6 +794,7 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
planActSeparateModelsSetting: state.planActSeparateModelsSetting,
|
||||
enableCheckpointsSetting: state.enableCheckpointsSetting,
|
||||
mcpMarketplaceEnabled: state.mcpMarketplaceEnabled,
|
||||
mcpRichDisplayEnabled: state.mcpRichDisplayEnabled,
|
||||
mcpResponsesCollapsed: state.mcpResponsesCollapsed,
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -113,6 +113,20 @@ export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): s
|
||||
return "You must provide a valid API key or choose a different provider."
|
||||
}
|
||||
break
|
||||
case "sapaicore":
|
||||
if (!apiConfiguration.sapAiCoreBaseUrl) {
|
||||
return "You must provide a valid Base URL key or choose a different provider."
|
||||
}
|
||||
if (!apiConfiguration.sapAiCoreClientId) {
|
||||
return "You must provide a valid Client Id or choose a different provider."
|
||||
}
|
||||
if (!apiConfiguration.sapAiCoreClientSecret) {
|
||||
return "You must provide a valid Client Secret or choose a different provider."
|
||||
}
|
||||
if (!apiConfiguration.sapAiCoreTokenUrl) {
|
||||
return "You must provide a valid Auth URL or choose a different provider."
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
|
||||
Reference in New Issue
Block a user