mirror of
https://github.com/cline/cline.git
synced 2026-09-13 09:50:12 +08:00
Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d58dc8f36a | ||
|
|
2a054ef6a5 | ||
|
|
57c8b8120d | ||
|
|
5243f0b9b1 | ||
|
|
c014060275 | ||
|
|
d790ce86a0 | ||
|
|
5e2b199377 | ||
|
|
9234d0cdc4 | ||
|
|
db1db8c95d | ||
|
|
f53af72643 | ||
|
|
260e0d5f8e | ||
|
|
5b68ee5523 | ||
|
|
3e5abd5e72 | ||
|
|
1ba5873454 | ||
|
|
1bdaf8ef6f |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add groq to kimi providers
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Add Kimi-K2 as the trending model in the Cline Provider
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Added API Key support for Bedrock integration
|
||||
@@ -147,6 +147,7 @@
|
||||
"pages": [
|
||||
"provider-config/anthropic",
|
||||
"provider-config/claude-code",
|
||||
"provider-config/aws-bedrock-with-apikey-authentication",
|
||||
"provider-config/aws-bedrock-with-credentials-authentication",
|
||||
"provider-config/aws-bedrock-with-profile-authentication",
|
||||
"provider-config/gcp-vertex-ai",
|
||||
|
||||
@@ -14,6 +14,8 @@ Certain scenarios may warrant using local models, including handling highly sens
|
||||
|
||||
#### [IAM Security Best Practices](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html) (For administrators)
|
||||
|
||||
#### [AWS Bedrock setup for API Keys](/provider-config/aws-bedrock-with-apikey-authentication)
|
||||
|
||||
#### [AWS Bedrock setup for Legacy IAM (AWS Credentials)](/provider-config/aws-bedrock-with-credentials-authentication)
|
||||
|
||||
#### [AWS Bedrock setup for SSO token (AWS Profile)](/provider-config/aws-bedrock-with-profile-authentication)
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
---
|
||||
title: "AWS Bedrock"
|
||||
description: "Learn how to set up AWS Bedrock with Cline using credentials authentication. This guide covers AWS environment setup, regional access verification, and secure integration with the Cline VS Code extension."
|
||||
---
|
||||
|
||||
### Overview
|
||||
|
||||
- **AWS Bedrock:** A fully managed service that offers access to leading generative AI models (e.g., Anthropic Claude, Amazon Nova) through AWS.\
|
||||
[Learn more about AWS Bedrock](https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html).
|
||||
- **Cline:** A VS Code extension that acts as a coding assistant by integrating with AI models—empowering developers to generate code, debug, and analyze data.
|
||||
- **Developer Focus:** This guide is tailored for individual developers that want to enable access to frontier models via AWS Bedrock with a simplified setup using API Keys.
|
||||
|
||||
---
|
||||
|
||||
### Step 1: Prepare Your AWS Environment
|
||||
|
||||
#### 1.1 Individual user setup - Create a Bedrock API Key
|
||||
|
||||
For more detailed instructions check the [documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/api-keys.html).
|
||||
|
||||
1. **Sign in to the AWS Management Console:**\
|
||||
[AWS Console](https://aws.amazon.com/console/)
|
||||
2. **Access Bedrock Console:**
|
||||
- [Bedrock Console](https://console.aws.amazon.com/bedrock)
|
||||
- Create a new Long Lived API Key. This API Key will have by default the `AmazonBedrockLimitedAccess` IAM policy
|
||||
[View AmazonBedrockLimitedAccess Policy Details](https://docs.aws.amazon.com/bedrock/latest/userguide/security-iam.html)
|
||||
|
||||
#### 1.2 Create or Modify the Policy
|
||||
|
||||
To ensure Cline can interact with AWS Bedrock, your IAM user or role needs specific permissions. While the `AmazonBedrockLimitedAccess` managed policy provides comprehensive access, for a more restricted and secure setup adhering to the principle of least privilege, the following minimal permissions are sufficient for Cline's core model invocation functionality:
|
||||
|
||||
- `bedrock:InvokeModel`
|
||||
- `bedrock:InvokeModelWithResponseStream`
|
||||
- `bedrock:CallWithBearerToken`
|
||||
|
||||
You can create a custom IAM policy with these permissions and attach it to your IAM user or role.
|
||||
|
||||
1. In the AWS IAM console, create a new policy.
|
||||
2. Use the JSON editor to add the following policy document:
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream", "bedrock:CallWithBearerToken"],
|
||||
"Resource": "*" // For enhanced security, scope this to specific model ARNs if possible.
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
3. Name the policy (e.g., `ClineBedrockInvokeAccess`) and attach it to the IAM user associated with the key you created. The IAM user and the API key have the same prefix.
|
||||
|
||||
**Important Considerations:**
|
||||
|
||||
- **Model Listing in Cline:** The minimal permissions (`bedrock:InvokeModel`, `bedrock:InvokeModelWithResponseStream`) are sufficient for Cline to _use_ a model if you specify the model ID directly in Cline's settings. If you rely on Cline to dynamically list available Bedrock models, you might need additional permissions like `bedrock:ListFoundationModels`.
|
||||
- **AWS Marketplace Subscriptions:** For third-party models (e.g., Anthropic Claude), the **`AmazonBedrockLimitedAccess`** policy grants you the necessary permissions to subscribe via the AWS Marketplace. There is no explicit access to be enabled. For Anthropic models you are still required to submit a First Time Use (FTU) form via the Console. If you get the following message in the Cline chat `[ERROR] Failed to process response: Model use case details have not been submitted for this account. Fill out the Anthropic use case details form before using the model.` then open the [Playground in the AWS Bedrock Console](https://console.aws.amazon.com/bedrock/home?#/text-generation-playground), select any Anthropic model and fill in the form (you might need to send a prompt first)
|
||||
|
||||
---
|
||||
|
||||
### Step 2: Verify Regional and Model Access
|
||||
|
||||
#### 2.1 Choose and Confirm a Region
|
||||
|
||||
1. **Select a Region:**\
|
||||
AWS Bedrock is available in multiple regions (e.g., US East, Europe, Asia Pacific). Choose the region that meets your latency and compliance needs.\
|
||||
[AWS Global Infrastructure](https://aws.amazon.com/about-aws/global-infrastructure/regions_az/)
|
||||
2. **Verify Model Access:**
|
||||
- **Note:** Some models are only accessible via an [Inference Profile](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html). In such case check the box "Cross Region Inference".
|
||||
|
||||
---
|
||||
|
||||
### Step 3: Configure the Cline VS Code Extension
|
||||
|
||||
#### 3.1 Install and Open Cline
|
||||
|
||||
1. **Install VS Code:**\
|
||||
Download from the [VS Code website](https://code.visualstudio.com/).
|
||||
2. **Install the Cline Extension:**
|
||||
- Open VS Code.
|
||||
- Go to the Extensions Marketplace (`Ctrl+Shift+X` or `Cmd+Shift+X`).
|
||||
- Search for **Cline** and install it.
|
||||
|
||||
#### 3.2 Configure Cline Settings
|
||||
|
||||
1. **Open Cline Settings:**
|
||||
- Click on the settings ⚙️ to select your API Provider.
|
||||
2. **Select AWS Bedrock as the API Provider:**
|
||||
- From the API Provider dropdown, choose **AWS Bedrock**.
|
||||
3. **Enter Your AWS API Key:**
|
||||
- Input your **API Key**
|
||||
- Specify the correct **AWS Region** (e.g., `us-east-1` or your enterprise-approved region).
|
||||
4. **Select a Model:**
|
||||
- Choose an on-demand model (e.g., **anthropic.claude-3-5-sonnet-20241022-v2:0**).
|
||||
5. **Save and Test:**
|
||||
- Click **Done/Save** to apply your settings.
|
||||
- Test the integration by sending a simple prompt (e.g., "Generate a Python function to check if a number is prime.").
|
||||
|
||||
---
|
||||
|
||||
### Step 4: Security, Monitoring, and Best Practices
|
||||
|
||||
1. **Secure Access:**
|
||||
- Prefer AWS SSO/federated roles over long-lived API Key when possible.
|
||||
- [AWS IAM Best Practices](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html)
|
||||
2. **Enhance Network Security:**
|
||||
- Consider setting up [AWS PrivateLink](https://docs.aws.amazon.com/vpc/latest/userguide/endpoint-services-overview.html) to securely connect to Bedrock.
|
||||
3. **Monitor and Log Activity:**
|
||||
- Enable AWS CloudTrail to log Bedrock API calls.
|
||||
- Use CloudWatch to monitor metrics like invocation count, latency, and token usage.
|
||||
- Set up alerts for abnormal activity.
|
||||
4. **Handle Errors and Manage Costs:**
|
||||
- Implement exponential backoff for throttling errors.
|
||||
- Use AWS Cost Explorer and set billing alerts to track usage.\
|
||||
[AWS Cost Management](https://docs.aws.amazon.com/cost-management/latest/userguide/what-is-aws-cost-management.html)
|
||||
5. **Regular Audits and Compliance:**
|
||||
- Periodically review IAM roles and CloudTrail logs.
|
||||
- Follow internal data privacy and governance policies.
|
||||
|
||||
---
|
||||
|
||||
### Conclusion
|
||||
|
||||
By following these steps, your enterprise team can securely integrate AWS Bedrock with the Cline VS Code extension to accelerate development:
|
||||
|
||||
1. **Prepare Your AWS Environment:** Create or use a secure IAM role/user, attach the `AmazonBedrockLimitedAccess` policy, and ensure necessary permissions.
|
||||
2. **Verify Region and Model Access:** Confirm that your selected region supports your required models.
|
||||
3. **Configure Cline in VS Code:** Install and set up Cline with your AWS credentials and choose an appropriate model.
|
||||
4. **Implement Security and Monitoring:** Use best practices for IAM, network security, monitoring, and cost management.
|
||||
|
||||
For further details, consult the [AWS Bedrock Documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html) and coordinate with your internal cloud team. Happy coding!
|
||||
|
||||
---
|
||||
|
||||
_This guide will be updated as AWS Bedrock and Cline evolve. Always refer to the latest documentation and internal policies for up-to-date practices._
|
||||
@@ -5,7 +5,7 @@ description: "Learn how to set up AWS Bedrock with Cline using credentials authe
|
||||
|
||||
### Overview
|
||||
|
||||
- **AWS Bedrock:** A fully managed service that offers access to leading generative AI models (e.g., Anthropic Claude, Amazon Titan) through AWS.\
|
||||
- **AWS Bedrock:** A fully managed service that offers access to leading generative AI models (e.g., Anthropic Claude, Amazon Nova) through AWS.\
|
||||
[Learn more about AWS Bedrock](https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html).
|
||||
- **Cline:** A VS Code extension that acts as a coding assistant by integrating with AI models—empowering developers to generate code, debug, and analyze data.
|
||||
- **Enterprise Focus:** This guide is tailored for organizations with established AWS environments (using IAM roles, AWS SSO, AWS Organizations, etc.) to ensure secure and compliant usage.
|
||||
@@ -25,7 +25,7 @@ description: "Learn how to set up AWS Bedrock with Cline using credentials authe
|
||||
|
||||
#### 1.2 Attach the Required Policies
|
||||
|
||||
To ensure Cline can interact with AWS Bedrock, your IAM user or role needs specific permissions. While the `AmazonBedrockFullAccess` managed policy provides comprehensive access, for a more restricted and secure setup adhering to the principle of least privilege, the following minimal permissions are sufficient for Cline's core model invocation functionality:
|
||||
To ensure Cline can interact with AWS Bedrock, your IAM user or role needs specific permissions. While the `AmazonBedrockLimitedAccess` managed policy provides comprehensive access, for a more restricted and secure setup adhering to the principle of least privilege, the following minimal permissions are sufficient for Cline's core model invocation functionality:
|
||||
|
||||
- `bedrock:InvokeModel`
|
||||
- `bedrock:InvokeModelWithResponseStream`
|
||||
@@ -52,8 +52,8 @@ You can create a custom IAM policy with these permissions and attach it to your
|
||||
|
||||
**Option 2: Using a Managed Policy (Simpler Initial Setup)**
|
||||
|
||||
- Alternatively, you can attach the AWS managed policy **`AmazonBedrockFullAccess`**. This grants broader permissions, including the ability to list models, manage provisioning, and other Bedrock features. This might be simpler for initial setup or if you require these wider capabilities.
|
||||
[View AmazonBedrockFullAccess Policy Details](https://docs.aws.amazon.com/bedrock/latest/userguide/security-iam.html)
|
||||
- Alternatively, you can attach the AWS managed policy **`AmazonBedrockLimitedAccess`**. This grants broader permissions, including the ability to list models, manage provisioning, and other Bedrock features. This might be simpler for initial setup or if you require these wider capabilities.
|
||||
[View AmazonBedrockLimitedAccess Policy Details](https://docs.aws.amazon.com/bedrock/latest/userguide/security-iam.html)
|
||||
|
||||
**Important Considerations:**
|
||||
|
||||
@@ -71,8 +71,8 @@ You can create a custom IAM policy with these permissions and attach it to your
|
||||
AWS Bedrock is available in multiple regions (e.g., US East, Europe, Asia Pacific). Choose the region that meets your latency and compliance needs.\
|
||||
[AWS Global Infrastructure](https://aws.amazon.com/about-aws/global-infrastructure/regions_az/)
|
||||
2. **Verify Model Access:**
|
||||
- In the AWS Bedrock console, confirm that the models your team requires (e.g., Anthropic Claude, Amazon Titan) are marked as "Access granted."
|
||||
- **Note:** Some advanced models might require an [Inference Profile](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-prereq.html) if not available on-demand.
|
||||
- In the AWS Bedrock console, confirm that the models your team requires (e.g., Anthropic Claude, Amazon Nova) are marked as "Access granted."
|
||||
- **Note:** Some advanced models might require an [Inference Profile](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html) if not available on-demand.
|
||||
|
||||
#### 2.2 Set Up AWS Marketplace Subscriptions (if needed)
|
||||
|
||||
@@ -138,7 +138,7 @@ You can create a custom IAM policy with these permissions and attach it to your
|
||||
|
||||
By following these steps, your enterprise team can securely integrate AWS Bedrock with the Cline VS Code extension to accelerate development:
|
||||
|
||||
1. **Prepare Your AWS Environment:** Create or use a secure IAM role/user, attach the `AmazonBedrockFullAccess` policy, and ensure necessary permissions.
|
||||
1. **Prepare Your AWS Environment:** Create or use a secure IAM role/user, attach the `AmazonBedrockLimitedAccess` policy, and ensure necessary permissions.
|
||||
2. **Verify Region and Model Access:** Confirm that your selected region supports your required models and subscribe via AWS Marketplace if needed.
|
||||
3. **Configure Cline in VS Code:** Install and set up Cline with your AWS credentials and choose an appropriate model.
|
||||
4. **Implement Security and Monitoring:** Use best practices for IAM, network security, monitoring, and cost management.
|
||||
|
||||
+2
-2
@@ -153,8 +153,8 @@ const extensionConfig = {
|
||||
// Standalone-specific configuration
|
||||
const standaloneConfig = {
|
||||
...baseConfig,
|
||||
entryPoints: ["src/standalone/standalone.ts"],
|
||||
outfile: `${destDir}/standalone.js`,
|
||||
entryPoints: ["src/standalone/cline-core.ts"],
|
||||
outfile: `${destDir}/cline-core.js`,
|
||||
// These gRPC protos need to load files from the module directory at runtime,
|
||||
// so they cannot be bundled.
|
||||
external: ["vscode", "@grpc/reflection", "grpc-health-check"],
|
||||
|
||||
Generated
+567
-834
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -409,8 +409,8 @@
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.37.0",
|
||||
"@anthropic-ai/vertex-sdk": "^0.6.4",
|
||||
"@aws-sdk/client-bedrock-runtime": "^3.826.0",
|
||||
"@aws-sdk/credential-providers": "^3.826.0",
|
||||
"@aws-sdk/client-bedrock-runtime": "^3.840.0",
|
||||
"@aws-sdk/credential-providers": "^3.840.0",
|
||||
"@bufbuild/protobuf": "^2.2.5",
|
||||
"@cerebras/cerebras_cloud_sdk": "^1.35.0",
|
||||
"@google-cloud/vertexai": "^1.9.3",
|
||||
|
||||
+3
-1
@@ -235,4 +235,6 @@ message ModelsApiConfiguration {
|
||||
optional string sap_ai_core_token_url = 71;
|
||||
optional string sap_ai_core_base_url = 72;
|
||||
optional string claude_code_path = 73;
|
||||
}
|
||||
optional string aws_authentication = 74;
|
||||
optional string aws_bedrock_api_key = 75;
|
||||
}
|
||||
|
||||
+5
-1
@@ -112,7 +112,7 @@ 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 string mcp_display_mode = 11;
|
||||
optional int64 terminal_output_line_limit = 12;
|
||||
}
|
||||
|
||||
@@ -232,4 +232,8 @@ message ApiConfiguration {
|
||||
|
||||
// Claude Code specific
|
||||
optional string claude_code_path = 77;
|
||||
|
||||
// Extension fields for Bedrock Api Keys
|
||||
optional string aws_authentication = 78;
|
||||
optional string aws_bedrock_api_key = 79;
|
||||
}
|
||||
|
||||
@@ -5,17 +5,31 @@ DIR=${1:-src/}
|
||||
DEST_DIR=dist-standalone
|
||||
SDK_DEST=$DEST_DIR/vscode-sdk-uses.txt
|
||||
CSS_DEST=$DEST_DIR/vscode-css-uses.txt
|
||||
TMP=/tmp/vscode-sdk-uses.txt.tmp
|
||||
mkdir -p $DEST_DIR
|
||||
|
||||
{
|
||||
git grep -h 'vscode\.' $DIR |
|
||||
grep -Ev '//.*vscode' | # remove commented out code
|
||||
grep -v vscode.commands.executeCommand | # executeCommand is handled separately
|
||||
grep -Ev '"vscode' | # remove command strings that get included because they start with vscode
|
||||
sed 's|.*vscode\.|vscode.|'| # remove everything before vscode.
|
||||
sed 's/[^a-zA-Z0-9_.].*$//' | # remove everything after last identifier
|
||||
grep -E '\.[a-z][^.]+$' | # remove types (last part of identifier should be lowercase)
|
||||
sort | uniq -c | sort -n | # Count occurrences
|
||||
cat > $SDK_DEST
|
||||
cat > $TMP
|
||||
}
|
||||
{
|
||||
grep -rh vscode.commands.executeCommand $DIR |
|
||||
perl -ne 'print if /["\x27"]/' | # Remove occurrences where the command is not on the same line (line doesnt contain quote chars) :(
|
||||
sed -n 's|.*\(vscode.commands.executeCommand[^,]*\).*|\1|p'| # Remove all params after the first one
|
||||
sed 's|\(".*"\).*|\1)|'| # Close the parantheses
|
||||
cat >> $TMP
|
||||
}
|
||||
|
||||
# Count occurrences
|
||||
cat $TMP | sort | uniq -c | sort -n > $SDK_DEST
|
||||
rm $TMP
|
||||
|
||||
echo Wrote uses of the vscode SDK to $(realpath $SDK_DEST)
|
||||
|
||||
{
|
||||
|
||||
@@ -63,6 +63,8 @@ function createHandlerForProvider(apiProvider: string | undefined, options: Omit
|
||||
awsSecretKey: options.awsSecretKey,
|
||||
awsSessionToken: options.awsSessionToken,
|
||||
awsRegion: options.awsRegion,
|
||||
awsAuthentication: options.awsAuthentication,
|
||||
awsBedrockApiKey: options.awsBedrockApiKey,
|
||||
awsUseCrossRegionInference: options.awsUseCrossRegionInference,
|
||||
awsBedrockUsePromptCache: options.awsBedrockUsePromptCache,
|
||||
awsUseProfile: options.awsUseProfile,
|
||||
|
||||
@@ -182,6 +182,24 @@ describe("AwsBedrockHandler", () => {
|
||||
|
||||
process.env["AWS_PROFILE"]!.should.equal(preAWSProfile)
|
||||
})
|
||||
|
||||
it("should work with AWS_BEARER_TOKEN_BEDROCK", async () => {
|
||||
process.env["AWS_BEARER_TOKEN_BEDROCK"] = "test-key"
|
||||
|
||||
const preAWSProfile = process.env["AWS_BEARER_TOKEN_BEDROCK"]
|
||||
|
||||
await AwsBedrockHandler["withTempEnv"](
|
||||
() => {
|
||||
delete process.env["AWS_BEARER_TOKEN_BEDROCK"]
|
||||
},
|
||||
async () => {
|
||||
should.not.exist(process.env["AWS_BEARER_TOKEN_BEDROCK"])
|
||||
return "test"
|
||||
},
|
||||
)
|
||||
|
||||
process.env["AWS_BEARER_TOKEN_BEDROCK"]!.should.equal(preAWSProfile)
|
||||
})
|
||||
})
|
||||
|
||||
const mockOptions: ApiHandlerOptions = {
|
||||
@@ -192,6 +210,7 @@ describe("AwsBedrockHandler", () => {
|
||||
awsSessionToken: "",
|
||||
awsUseProfile: false,
|
||||
awsProfile: "",
|
||||
awsBedrockApiKey: "",
|
||||
awsBedrockUsePromptCache: false,
|
||||
awsUseCrossRegionInference: false,
|
||||
awsBedrockEndpoint: "",
|
||||
|
||||
@@ -22,6 +22,8 @@ interface AwsBedrockHandlerOptions {
|
||||
awsSecretKey?: string
|
||||
awsSessionToken?: string
|
||||
awsRegion?: string
|
||||
awsAuthentication?: string
|
||||
awsBedrockApiKey?: string
|
||||
awsUseCrossRegionInference?: boolean
|
||||
awsBedrockUsePromptCache?: boolean
|
||||
awsUseProfile?: boolean
|
||||
@@ -186,7 +188,10 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
}> {
|
||||
// Configure provider options
|
||||
const providerOptions: ProviderChainOptions = {}
|
||||
if (this.options.awsUseProfile) {
|
||||
const useProfile =
|
||||
(this.options.awsAuthentication === undefined && this.options.awsUseProfile) ||
|
||||
this.options.awsAuthentication === "profile"
|
||||
if (useProfile) {
|
||||
// For profile-based auth, always use ignoreCache to detect credential file changes
|
||||
// This solves the AWS Identity Manager issue where credential files change externally
|
||||
providerOptions.ignoreCache = true
|
||||
@@ -200,7 +205,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
return await AwsBedrockHandler.withTempEnv(
|
||||
() => {
|
||||
AwsBedrockHandler.setEnv("AWS_REGION", this.options.awsRegion)
|
||||
if (this.options.awsUseProfile) {
|
||||
if (useProfile) {
|
||||
AwsBedrockHandler.setEnv("AWS_PROFILE", this.options.awsProfile)
|
||||
} else {
|
||||
delete process.env["AWS_PROFILE"]
|
||||
@@ -224,15 +229,26 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
* Creates a BedrockRuntimeClient with the appropriate credentials
|
||||
*/
|
||||
private async getBedrockClient(): Promise<BedrockRuntimeClient> {
|
||||
const credentials = await this.getAwsCredentials()
|
||||
let auth: any
|
||||
|
||||
if (this.options.awsAuthentication === "apikey") {
|
||||
auth = {
|
||||
token: { token: this.options.awsBedrockApiKey },
|
||||
authSchemePreference: ["httpBearerAuth"],
|
||||
}
|
||||
} else {
|
||||
const credentials = await this.getAwsCredentials()
|
||||
auth = {
|
||||
credentials: {
|
||||
accessKeyId: credentials.accessKeyId,
|
||||
secretAccessKey: credentials.secretAccessKey,
|
||||
sessionToken: credentials.sessionToken,
|
||||
},
|
||||
}
|
||||
}
|
||||
return new BedrockRuntimeClient({
|
||||
region: this.getRegion(),
|
||||
credentials: {
|
||||
accessKeyId: credentials.accessKeyId,
|
||||
secretAccessKey: credentials.secretAccessKey,
|
||||
sessionToken: credentials.sessionToken,
|
||||
},
|
||||
...auth,
|
||||
...(this.options.awsBedrockEndpoint && { endpoint: this.options.awsBedrockEndpoint }),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { withRetry } from "../retry"
|
||||
import { AuthService } from "@/services/auth/AuthService"
|
||||
import OpenAI from "openai"
|
||||
import { version as extensionVersion } from "../../../package.json"
|
||||
import { shouldSkipReasoningForModel } from "@utils/model-utils"
|
||||
|
||||
interface ClineHandlerOptions {
|
||||
taskId?: string
|
||||
@@ -127,7 +128,8 @@ export class ClineHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
// Reasoning tokens are returned separately from the content
|
||||
if ("reasoning" in delta && delta.reasoning) {
|
||||
// Skip reasoning content for Grok 4 models since it only displays "thinking" without providing useful information
|
||||
if ("reasoning" in delta && delta.reasoning && !shouldSkipReasoningForModel(this.options.openRouterModelId)) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
// @ts-ignore-next-line
|
||||
|
||||
@@ -8,6 +8,7 @@ import { withRetry } from "../retry"
|
||||
import { createOpenRouterStream } from "../transform/openrouter-stream"
|
||||
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
|
||||
import { OpenRouterErrorResponse } from "./types"
|
||||
import { shouldSkipReasoningForModel } from "@utils/model-utils"
|
||||
|
||||
interface OpenRouterHandlerOptions {
|
||||
openRouterApiKey?: string
|
||||
@@ -112,7 +113,8 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
// Reasoning tokens are returned separately from the content
|
||||
if ("reasoning" in delta && delta.reasoning) {
|
||||
// Skip reasoning content for Grok 4 models since it only displays "thinking" without providing useful information
|
||||
if ("reasoning" in delta && delta.reasoning && !shouldSkipReasoningForModel(this.options.openRouterModelId)) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
// @ts-ignore-next-line
|
||||
|
||||
@@ -6,6 +6,7 @@ import { convertToOpenAiMessages } from "@api/transform/openai-format"
|
||||
import { ApiStream } from "@api/transform/stream"
|
||||
import { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
|
||||
import { withRetry } from "../retry"
|
||||
import { shouldSkipReasoningForModel } from "@utils/model-utils"
|
||||
|
||||
interface XAIHandlerOptions {
|
||||
xaiApiKey?: string
|
||||
@@ -70,10 +71,13 @@ export class XAIHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
// @ts-ignore-next-line
|
||||
reasoning: delta.reasoning_content,
|
||||
// Skip reasoning content for Grok 4 models since it only displays "thinking" without providing useful information
|
||||
if (!shouldSkipReasoningForModel(modelId)) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
// @ts-ignore-next-line
|
||||
reasoning: delta.reasoning_content,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -139,6 +139,10 @@ export async function createOpenRouterStream(
|
||||
shouldApplyMiddleOutTransform = true
|
||||
}
|
||||
|
||||
// hardcoded provider sorting for kimi-k2
|
||||
const isKimiK2 = model.id.startsWith("moonshotai/kimi-k2")
|
||||
openRouterProviderSorting = isKimiK2 ? undefined : openRouterProviderSorting
|
||||
|
||||
// @ts-ignore-next-line
|
||||
const stream = await client.chat.completions.create({
|
||||
model: model.id,
|
||||
@@ -153,6 +157,8 @@ export async function createOpenRouterStream(
|
||||
...(model.id.startsWith("openai/o") ? { reasoning_effort: reasoningEffort || "medium" } : {}),
|
||||
...(reasoning ? { reasoning } : {}),
|
||||
...(openRouterProviderSorting ? { provider: { sort: openRouterProviderSorting } } : {}),
|
||||
// limit providers to only those that support the 131k context window
|
||||
...(isKimiK2 ? { provider: { order: ["groq", "together"], allow_fallbacks: false } } : {}),
|
||||
})
|
||||
|
||||
return stream
|
||||
|
||||
@@ -834,7 +834,7 @@ export class Controller {
|
||||
chatSettings: storedChatSettings,
|
||||
userInfo,
|
||||
mcpMarketplaceEnabled,
|
||||
mcpRichDisplayEnabled,
|
||||
mcpDisplayMode,
|
||||
telemetrySetting,
|
||||
planActSeparateModelsSetting,
|
||||
enableCheckpointsSetting,
|
||||
@@ -887,7 +887,7 @@ export class Controller {
|
||||
chatSettings,
|
||||
userInfo,
|
||||
mcpMarketplaceEnabled,
|
||||
mcpRichDisplayEnabled,
|
||||
mcpDisplayMode,
|
||||
telemetrySetting,
|
||||
planActSeparateModelsSetting,
|
||||
enableCheckpointsSetting: enableCheckpointsSetting ?? true,
|
||||
|
||||
@@ -104,6 +104,12 @@ export async function refreshOpenRouterModels(
|
||||
modelInfo.cacheWritesPrice = 0.75
|
||||
modelInfo.cacheReadsPrice = 0
|
||||
break
|
||||
case "moonshotai/kimi-k2":
|
||||
// forcing kimi-k2 to use the together provider for full context and best throughput
|
||||
modelInfo.inputPrice = 1
|
||||
modelInfo.outputPrice = 3
|
||||
modelInfo.contextWindow = 131_000
|
||||
break
|
||||
default:
|
||||
if (rawModel.id.startsWith("openai/")) {
|
||||
modelInfo.cacheReadsPrice = parsePrice(rawModel.pricing?.input_cache_read)
|
||||
|
||||
@@ -50,9 +50,9 @@ 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 MCP display mode setting
|
||||
if (request.mcpDisplayMode !== undefined) {
|
||||
await controller.context.globalState.update("mcpDisplayMode", request.mcpDisplayMode)
|
||||
}
|
||||
|
||||
// Update chat settings
|
||||
|
||||
@@ -5,6 +5,7 @@ export type SecretKey =
|
||||
| "awsAccessKey"
|
||||
| "awsSecretKey"
|
||||
| "awsSessionToken"
|
||||
| "awsBedrockApiKey"
|
||||
| "openAiApiKey"
|
||||
| "geminiApiKey"
|
||||
| "openAiNativeApiKey"
|
||||
@@ -31,6 +32,8 @@ export type GlobalStateKey =
|
||||
| "awsBedrockUsePromptCache"
|
||||
| "awsBedrockEndpoint"
|
||||
| "awsProfile"
|
||||
| "awsBedrockApiKey"
|
||||
| "awsAuthentication"
|
||||
| "awsUseProfile"
|
||||
| "vertexProjectId"
|
||||
| "vertexRegion"
|
||||
@@ -73,7 +76,7 @@ export type GlobalStateKey =
|
||||
| "isNewUser"
|
||||
| "welcomeViewCompleted"
|
||||
| "terminalOutputLineLimit"
|
||||
| "mcpRichDisplayEnabled"
|
||||
| "mcpDisplayMode"
|
||||
| "sapAiCoreTokenUrl"
|
||||
| "sapAiCoreBaseUrl"
|
||||
| "sapAiResourceGroup"
|
||||
|
||||
@@ -11,6 +11,7 @@ import { StoredChatSettings } from "@shared/ChatSettings"
|
||||
import { TelemetrySetting } from "@shared/TelemetrySetting"
|
||||
import { UserInfo } from "@shared/UserInfo"
|
||||
import { ClineRulesToggles } from "@shared/cline-rules"
|
||||
import { DEFAULT_MCP_DISPLAY_MODE, McpDisplayMode } from "@shared/McpDisplayMode"
|
||||
import { migrateEnableCheckpointsSetting, migrateMcpMarketplaceEnableSetting } from "./state-migrations"
|
||||
/*
|
||||
Storage
|
||||
@@ -124,7 +125,9 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
awsBedrockUsePromptCache,
|
||||
awsBedrockEndpoint,
|
||||
awsProfile,
|
||||
awsBedrockApiKey,
|
||||
awsUseProfile,
|
||||
awsAuthentication,
|
||||
vertexProjectId,
|
||||
vertexRegion,
|
||||
openAiBaseUrl,
|
||||
@@ -171,7 +174,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
shellIntegrationTimeout,
|
||||
enableCheckpointsSettingRaw,
|
||||
mcpMarketplaceEnabledRaw,
|
||||
mcpRichDisplayEnabled,
|
||||
mcpDisplayMode,
|
||||
mcpResponsesCollapsedRaw,
|
||||
globalWorkflowToggles,
|
||||
terminalReuseEnabled,
|
||||
@@ -197,7 +200,9 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
getGlobalState(context, "awsBedrockUsePromptCache") as Promise<boolean | undefined>,
|
||||
getGlobalState(context, "awsBedrockEndpoint") as Promise<string | undefined>,
|
||||
getGlobalState(context, "awsProfile") as Promise<string | undefined>,
|
||||
getSecret(context, "awsBedrockApiKey") as Promise<string | undefined>,
|
||||
getGlobalState(context, "awsUseProfile") as Promise<boolean | undefined>,
|
||||
getGlobalState(context, "awsAuthentication") as Promise<string | undefined>,
|
||||
getGlobalState(context, "vertexProjectId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "vertexRegion") as Promise<string | undefined>,
|
||||
getGlobalState(context, "openAiBaseUrl") as Promise<string | undefined>,
|
||||
@@ -244,7 +249,7 @@ 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, "mcpDisplayMode") as Promise<McpDisplayMode | undefined>,
|
||||
getGlobalState(context, "mcpResponsesCollapsed") as Promise<boolean | undefined>,
|
||||
getGlobalState(context, "globalWorkflowToggles") as Promise<ClineRulesToggles | undefined>,
|
||||
getGlobalState(context, "terminalReuseEnabled") as Promise<boolean | undefined>,
|
||||
@@ -380,7 +385,9 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
awsBedrockUsePromptCache,
|
||||
awsBedrockEndpoint,
|
||||
awsProfile,
|
||||
awsBedrockApiKey,
|
||||
awsUseProfile,
|
||||
awsAuthentication,
|
||||
awsBedrockCustomSelected,
|
||||
awsBedrockCustomModelBaseId,
|
||||
vertexProjectId,
|
||||
@@ -464,7 +471,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
previousModeAwsBedrockCustomModelBaseId,
|
||||
previousModeSapAiCoreModelId,
|
||||
mcpMarketplaceEnabled: mcpMarketplaceEnabled,
|
||||
mcpRichDisplayEnabled: mcpRichDisplayEnabled ?? true,
|
||||
mcpDisplayMode: mcpDisplayMode ?? DEFAULT_MCP_DISPLAY_MODE,
|
||||
mcpResponsesCollapsed: mcpResponsesCollapsed,
|
||||
telemetrySetting: telemetrySetting || "unset",
|
||||
planActSeparateModelsSetting,
|
||||
@@ -490,8 +497,10 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
awsUseCrossRegionInference,
|
||||
awsBedrockUsePromptCache,
|
||||
awsBedrockEndpoint,
|
||||
awsBedrockApiKey,
|
||||
awsProfile,
|
||||
awsUseProfile,
|
||||
awsAuthentication,
|
||||
awsBedrockCustomSelected,
|
||||
awsBedrockCustomModelBaseId,
|
||||
vertexProjectId,
|
||||
@@ -584,6 +593,7 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
awsBedrockEndpoint,
|
||||
awsProfile,
|
||||
awsUseProfile,
|
||||
awsAuthentication,
|
||||
vertexProjectId,
|
||||
vertexRegion,
|
||||
openAiBaseUrl,
|
||||
@@ -617,6 +627,7 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
awsAccessKey,
|
||||
awsSecretKey,
|
||||
awsSessionToken,
|
||||
awsBedrockApiKey,
|
||||
openAiApiKey,
|
||||
geminiApiKey,
|
||||
openAiNativeApiKey,
|
||||
@@ -658,6 +669,7 @@ export async function resetGlobalState(context: vscode.ExtensionContext) {
|
||||
"awsAccessKey",
|
||||
"awsSecretKey",
|
||||
"awsSessionToken",
|
||||
"awsBedrockApiKey",
|
||||
"openAiApiKey",
|
||||
"geminiApiKey",
|
||||
"openAiNativeApiKey",
|
||||
|
||||
@@ -121,15 +121,9 @@ export abstract class DiffViewProvider {
|
||||
|
||||
// Replace all content up to the current line with accumulated lines
|
||||
// This is necessary (as compared to inserting one line at a time) to handle cases where html tags on previous lines are auto closed for example
|
||||
const edit = new vscode.WorkspaceEdit()
|
||||
const rangeToReplace = new vscode.Range(0, 0, currentLine + 1, 0)
|
||||
const contentToReplace = accumulatedLines.slice(0, currentLine + 1).join("\n") + "\n"
|
||||
edit.replace(document.uri, rangeToReplace, contentToReplace)
|
||||
await vscode.workspace.applyEdit(edit)
|
||||
|
||||
// Update decorations for the entire changed section
|
||||
this.activeLineController.setActiveLine(currentLine)
|
||||
this.fadedOverlayController.updateOverlayAfterLine(currentLine, document.lineCount)
|
||||
const rangeToReplace = { startLine: 0, endLine: currentLine + 1 }
|
||||
await this.replaceText(contentToReplace, rangeToReplace, currentLine)
|
||||
|
||||
// Scroll to the actual change location if provided.
|
||||
if (changeLocation) {
|
||||
@@ -186,6 +180,24 @@ export abstract class DiffViewProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces text in the diff editor with the specified content.
|
||||
*
|
||||
* This abstract method must be implemented by subclasses to handle the actual
|
||||
* text replacement in their specific diff editor implementation. It's called
|
||||
* during the streaming update process to progressively show changes.
|
||||
*
|
||||
* @param content The new content to insert into the document
|
||||
* @param rangeToReplace An object specifying the line range to replace
|
||||
* @param currentLine The current line number being edited, used for scroll positioning
|
||||
* @returns A promise that resolves when the text replacement is complete
|
||||
*/
|
||||
abstract replaceText(
|
||||
content: string,
|
||||
rangeToReplace: { startLine: number; endLine: number },
|
||||
currentLine: number,
|
||||
): Promise<void>
|
||||
|
||||
async saveChanges(): Promise<{
|
||||
newProblemsMessage: string | undefined
|
||||
userEdits: string | undefined
|
||||
|
||||
@@ -77,4 +77,24 @@ export class VscodeDiffViewProvider extends DiffViewProvider {
|
||||
// Apply faded overlay to all lines initially
|
||||
this.fadedOverlayController.addLines(0, this.activeDiffEditor.document.lineCount)
|
||||
}
|
||||
|
||||
override async replaceText(
|
||||
content: string,
|
||||
rangeToReplace: { startLine: number; endLine: number },
|
||||
currentLine: number,
|
||||
): Promise<void> {
|
||||
const document = this.activeDiffEditor?.document
|
||||
if (!document) {
|
||||
throw new Error("User closed text editor, unable to edit file...")
|
||||
}
|
||||
|
||||
const edit = new vscode.WorkspaceEdit()
|
||||
const range = new vscode.Range(rangeToReplace.startLine, 0, rangeToReplace.endLine, 0)
|
||||
edit.replace(document.uri, range, content)
|
||||
await vscode.workspace.applyEdit(edit)
|
||||
|
||||
// Update decorations for the entire changed section
|
||||
this.activeLineController?.setActiveLine(currentLine)
|
||||
this.fadedOverlayController?.updateOverlayAfterLine(currentLine, document.lineCount)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { HistoryItem } from "./HistoryItem"
|
||||
import { TelemetrySetting } from "./TelemetrySetting"
|
||||
import { ClineRulesToggles } from "./cline-rules"
|
||||
import { UserInfo } from "./UserInfo"
|
||||
import { McpDisplayMode, DEFAULT_MCP_DISPLAY_MODE } from "./McpDisplayMode"
|
||||
|
||||
// webview will hold state
|
||||
export interface ExtensionMessage {
|
||||
@@ -37,7 +38,7 @@ export interface ExtensionState {
|
||||
clineMessages: ClineMessage[]
|
||||
currentTaskItem?: HistoryItem
|
||||
mcpMarketplaceEnabled?: boolean
|
||||
mcpRichDisplayEnabled: boolean
|
||||
mcpDisplayMode: McpDisplayMode
|
||||
planActSeparateModelsSetting: boolean
|
||||
enableCheckpointsSetting?: boolean
|
||||
platform: Platform
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Represents the different display modes available for MCP responses
|
||||
*/
|
||||
export type McpDisplayMode = "rich" | "plain" | "markdown"
|
||||
|
||||
/**
|
||||
* Default display mode for MCP responses
|
||||
*/
|
||||
export const DEFAULT_MCP_DISPLAY_MODE: McpDisplayMode = "plain"
|
||||
+4
-1
@@ -50,8 +50,10 @@ export interface ApiHandlerOptions {
|
||||
awsRegion?: string
|
||||
awsUseCrossRegionInference?: boolean
|
||||
awsBedrockUsePromptCache?: boolean
|
||||
awsAuthentication?: string
|
||||
awsUseProfile?: boolean
|
||||
awsProfile?: string
|
||||
awsBedrockApiKey?: string
|
||||
awsBedrockEndpoint?: string
|
||||
awsBedrockCustomSelected?: boolean
|
||||
awsBedrockCustomModelBaseId?: BedrockModelId
|
||||
@@ -2094,7 +2096,8 @@ export const xaiModels = {
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 3.0, // will have different pricing for long context vs short context
|
||||
outputPrice: 6.0,
|
||||
cacheReadsPrice: 0.75,
|
||||
outputPrice: 15.0,
|
||||
},
|
||||
"grok-3-beta": {
|
||||
maxTokens: 8192,
|
||||
|
||||
@@ -328,7 +328,9 @@ export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoA
|
||||
awsUseCrossRegionInference: config.awsUseCrossRegionInference,
|
||||
awsBedrockUsePromptCache: config.awsBedrockUsePromptCache,
|
||||
awsUseProfile: config.awsUseProfile,
|
||||
awsAuthentication: config.awsAuthentication,
|
||||
awsProfile: config.awsProfile,
|
||||
awsBedrockApiKey: config.awsBedrockApiKey,
|
||||
awsBedrockEndpoint: config.awsBedrockEndpoint,
|
||||
awsBedrockCustomSelected: config.awsBedrockCustomSelected,
|
||||
awsBedrockCustomModelBaseId: config.awsBedrockCustomModelBaseId as string | undefined,
|
||||
@@ -407,7 +409,9 @@ export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguratio
|
||||
awsUseCrossRegionInference: protoConfig.awsUseCrossRegionInference,
|
||||
awsBedrockUsePromptCache: protoConfig.awsBedrockUsePromptCache,
|
||||
awsUseProfile: protoConfig.awsUseProfile,
|
||||
awsAuthentication: protoConfig.awsAuthentication,
|
||||
awsProfile: protoConfig.awsProfile,
|
||||
awsBedrockApiKey: protoConfig.awsBedrockApiKey,
|
||||
awsBedrockEndpoint: protoConfig.awsBedrockEndpoint,
|
||||
awsBedrockCustomSelected: protoConfig.awsBedrockCustomSelected,
|
||||
awsBedrockCustomModelBaseId: protoConfig.awsBedrockCustomModelBaseId as BedrockModelId | undefined,
|
||||
|
||||
@@ -60,7 +60,9 @@ export function convertApiConfigurationToProtoApiConfiguration(config: ApiConfig
|
||||
awsUseCrossRegionInference: config.awsUseCrossRegionInference,
|
||||
awsBedrockUsePromptCache: config.awsBedrockUsePromptCache,
|
||||
awsUseProfile: config.awsUseProfile,
|
||||
awsAuthentication: config.awsAuthentication,
|
||||
awsProfile: config.awsProfile,
|
||||
awsBedrockApiKey: config.awsBedrockApiKey,
|
||||
awsBedrockEndpoint: config.awsBedrockEndpoint,
|
||||
|
||||
// Vertex AI fields
|
||||
@@ -177,6 +179,8 @@ export function convertProtoApiConfigurationToApiConfiguration(protoConfig: Prot
|
||||
awsBedrockUsePromptCache: protoConfig.awsBedrockUsePromptCache,
|
||||
awsUseProfile: protoConfig.awsUseProfile,
|
||||
awsProfile: protoConfig.awsProfile,
|
||||
awsAuthentication: protoConfig.awsAuthentication,
|
||||
awsBedrockApiKey: protoConfig.awsBedrockApiKey,
|
||||
awsBedrockEndpoint: protoConfig.awsBedrockEndpoint,
|
||||
|
||||
// Vertex AI fields
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { DiffViewProvider } from "@/integrations/editor/DiffViewProvider"
|
||||
|
||||
export class ExternalDiffViewProvider extends DiffViewProvider {
|
||||
override async openDiffEditor(): Promise<void> {
|
||||
// The host bridge proto changes are not submitted yet.
|
||||
//getHostBridgeProvider().diffClient.openDiff(this.absolutePath)
|
||||
if (!this.absolutePath) {
|
||||
return
|
||||
}
|
||||
getHostBridgeProvider().diffClient.openDiff({ path: this.absolutePath, content: this.originalContent ?? "" })
|
||||
}
|
||||
override replaceText(
|
||||
content: string,
|
||||
rangeToReplace: { startLine: number; endLine: number },
|
||||
currentLine: number,
|
||||
): Promise<void> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { describe, it } from "mocha"
|
||||
import "should"
|
||||
import { shouldSkipReasoningForModel } from "../model-utils"
|
||||
|
||||
describe("shouldSkipReasoningForModel", () => {
|
||||
it("should return true for grok-4 models", () => {
|
||||
shouldSkipReasoningForModel("grok-4").should.equal(true)
|
||||
shouldSkipReasoningForModel("x-ai/grok-4").should.equal(true)
|
||||
shouldSkipReasoningForModel("openrouter/grok-4-turbo").should.equal(true)
|
||||
shouldSkipReasoningForModel("some-provider/grok-4-mini").should.equal(true)
|
||||
})
|
||||
|
||||
it("should return false for non-grok-4 models", () => {
|
||||
shouldSkipReasoningForModel("grok-3").should.equal(false)
|
||||
shouldSkipReasoningForModel("grok-2").should.equal(false)
|
||||
shouldSkipReasoningForModel("claude-3-sonnet").should.equal(false)
|
||||
shouldSkipReasoningForModel("gpt-4").should.equal(false)
|
||||
shouldSkipReasoningForModel("gemini-pro").should.equal(false)
|
||||
})
|
||||
|
||||
it("should return false for undefined or empty model IDs", () => {
|
||||
shouldSkipReasoningForModel(undefined).should.equal(false)
|
||||
shouldSkipReasoningForModel("").should.equal(false)
|
||||
})
|
||||
|
||||
it("should be case sensitive", () => {
|
||||
shouldSkipReasoningForModel("GROK-4").should.equal(false)
|
||||
shouldSkipReasoningForModel("Grok-4").should.equal(false)
|
||||
})
|
||||
})
|
||||
@@ -13,3 +13,14 @@ export function isGemini2dot5ModelFamily(api: ApiHandler): boolean {
|
||||
const modelId = model.id
|
||||
return modelId.includes("gemini-2.5")
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if reasoning content should be skipped for a given model
|
||||
* Currently skips reasoning for Grok-4 models since they only display "thinking" without useful information
|
||||
*/
|
||||
export function shouldSkipReasoningForModel(modelId?: string): boolean {
|
||||
if (!modelId) {
|
||||
return false
|
||||
}
|
||||
return modelId.includes("grok-4")
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "cline-standalone",
|
||||
"name": "cline-core",
|
||||
"version": "0.0.1",
|
||||
"main": "standalone.js",
|
||||
"main": "cline-core.js",
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "^1.13.3",
|
||||
"@grpc/reflection": "^1.0.4",
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from "react"
|
||||
import { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
|
||||
import { McpDisplayMode } from "@shared/McpDisplayMode"
|
||||
|
||||
interface McpDisplayModeDropdownProps {
|
||||
value: McpDisplayMode
|
||||
onChange: (mode: McpDisplayMode) => void
|
||||
id?: string
|
||||
className?: string
|
||||
style?: React.CSSProperties
|
||||
onClick?: (e: React.MouseEvent) => void
|
||||
}
|
||||
|
||||
const McpDisplayModeDropdown: React.FC<McpDisplayModeDropdownProps> = ({ value, onChange, id, className, style, onClick }) => {
|
||||
const handleChange = (e: any) => {
|
||||
const newMode = e.target.value as McpDisplayMode
|
||||
onChange(newMode)
|
||||
}
|
||||
|
||||
return (
|
||||
<VSCodeDropdown id={id} value={value} onChange={handleChange} onClick={onClick} className={className} style={style}>
|
||||
<VSCodeOption value="plain">Plain Text</VSCodeOption>
|
||||
<VSCodeOption value="rich">Rich Display</VSCodeOption>
|
||||
<VSCodeOption value="markdown">Markdown</VSCodeOption>
|
||||
</VSCodeDropdown>
|
||||
)
|
||||
}
|
||||
|
||||
export default McpDisplayModeDropdown
|
||||
@@ -1,12 +1,17 @@
|
||||
import React, { useEffect, useState, useCallback } from "react"
|
||||
import { VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react" // Import ProgressRing
|
||||
import { VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useExtensionState } from "../../../context/ExtensionStateContext"
|
||||
import LinkPreview from "./LinkPreview"
|
||||
import ImagePreview from "./ImagePreview"
|
||||
import styled from "styled-components"
|
||||
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
|
||||
import ChatErrorBoundary from "@/components/chat/ChatErrorBoundary"
|
||||
import { isUrl, isLocalhostUrl, formatUrlForOpening, checkIfImageUrl } from "./utils/mcpRichUtil"
|
||||
import MarkdownBlock from "@/components/common/MarkdownBlock"
|
||||
import McpDisplayModeDropdown from "./McpDisplayModeDropdown"
|
||||
import { DropdownContainer } from "@/components/settings/ApiOptions"
|
||||
import { updateSetting } from "@/components/settings/utils/settingsHandlers"
|
||||
import { McpDisplayMode } from "@shared/McpDisplayMode"
|
||||
import { UrlMatch, processResponseUrls, DisplaySegment, buildDisplaySegments } from "./utils/mcpRichUtil"
|
||||
|
||||
// Maximum number of URLs to process in total, per response
|
||||
export const MAX_URLS = 50
|
||||
@@ -36,46 +41,6 @@ const ResponseHeader = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const ToggleSwitch = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
|
||||
.toggle-label {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.toggle-container {
|
||||
position: relative;
|
||||
width: 40px;
|
||||
height: 20px;
|
||||
background-color: var(--vscode-button-secondaryBackground);
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.toggle-container.active {
|
||||
background-color: var(--vscode-button-background);
|
||||
}
|
||||
|
||||
.toggle-handle {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-color: var(--vscode-button-foreground);
|
||||
border-radius: 50%;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.toggle-container.active .toggle-handle {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
`
|
||||
|
||||
const ResponseContainer = styled.div`
|
||||
position: relative;
|
||||
font-family: var(--vscode-editor-font-family, monospace);
|
||||
@@ -107,28 +72,16 @@ interface McpResponseDisplayProps {
|
||||
responseText: string
|
||||
}
|
||||
|
||||
// Represents a URL found in the text with its position and metadata
|
||||
interface UrlMatch {
|
||||
url: string // The actual URL
|
||||
fullMatch: string // The full matched text
|
||||
index: number // Position in the text
|
||||
isImage: boolean // Whether this URL is an image
|
||||
isProcessed: boolean // Whether we've already processed this URL (to avoid duplicates)
|
||||
}
|
||||
|
||||
const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText }) => {
|
||||
const { mcpResponsesCollapsed, mcpRichDisplayEnabled } = useExtensionState() // Get setting from context
|
||||
const { mcpResponsesCollapsed, mcpDisplayMode } = useExtensionState() // Get setting from context
|
||||
const [isExpanded, setIsExpanded] = useState(!mcpResponsesCollapsed) // Initialize with context setting
|
||||
const [isLoading, setIsLoading] = useState(false) // Initial loading state for rich content
|
||||
const [displayMode, setDisplayMode] = useState<"rich" | "plain">(() => {
|
||||
// Initialize directly from the global setting.
|
||||
return mcpRichDisplayEnabled ? "rich" : "plain"
|
||||
})
|
||||
|
||||
const [urlMatches, setUrlMatches] = useState<UrlMatch[]>([])
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const toggleDisplayMode = useCallback(() => {
|
||||
setDisplayMode((prevMode) => (prevMode === "rich" ? "plain" : "rich"))
|
||||
const handleDisplayModeChange = useCallback((newMode: McpDisplayMode) => {
|
||||
updateSetting("mcpDisplayMode", newMode)
|
||||
}, [])
|
||||
|
||||
const toggleExpand = useCallback(() => {
|
||||
@@ -142,138 +95,89 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
|
||||
|
||||
// Find all URLs in the text and determine if they're images
|
||||
useEffect(() => {
|
||||
// Skip all processing if in plain mode
|
||||
if (!isExpanded || displayMode === "plain") {
|
||||
// Skip all processing if in plain mode or markdown mode
|
||||
if (!isExpanded || mcpDisplayMode === "plain" || mcpDisplayMode === "markdown") {
|
||||
setIsLoading(false)
|
||||
if (urlMatches.length > 0) {
|
||||
setUrlMatches([]) // Clear any existing matches when in plain mode
|
||||
setUrlMatches([]) // Clear any existing matches when not in rich mode
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Use a direct boolean for cancellation that's scoped to this effect run
|
||||
let processingCanceled = false
|
||||
const processResponse = async () => {
|
||||
console.log("Processing MCP response for URL extraction")
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const text = responseText || ""
|
||||
const matches: UrlMatch[] = []
|
||||
const urlRegex = /(?:https?:\/\/|data:image)[^\s<>"']+/g
|
||||
let urlMatch: RegExpExecArray | null
|
||||
let urlCount = 0
|
||||
console.log("Processing MCP response for URL extraction")
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
// First pass: Extract all URLs and immediately make them available for rendering
|
||||
while ((urlMatch = urlRegex.exec(text)) !== null && urlCount < MAX_URLS) {
|
||||
// Get the original URL from the match - never modify the original URL text
|
||||
const url = urlMatch[0]
|
||||
|
||||
// Skip invalid URLs
|
||||
if (!isUrl(url)) {
|
||||
console.log("Skipping invalid URL:", url)
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip localhost URLs to prevent security issues
|
||||
if (isLocalhostUrl(url)) {
|
||||
console.log("Skipping localhost URL:", url)
|
||||
continue
|
||||
}
|
||||
|
||||
matches.push({
|
||||
url,
|
||||
fullMatch: url,
|
||||
index: urlMatch.index,
|
||||
isImage: false, // Will check later
|
||||
isProcessed: false,
|
||||
})
|
||||
|
||||
urlCount++
|
||||
}
|
||||
|
||||
console.log(`Found ${matches.length} URLs in text, will check if they are images`)
|
||||
|
||||
// Set matches immediately so UI can start rendering with loading states
|
||||
setUrlMatches(matches.sort((a, b) => a.index - b.index))
|
||||
|
||||
// Mark loading as complete to show content immediately
|
||||
// Use the orchestrator function from mcpRichUtil
|
||||
const cleanup = processResponseUrls(
|
||||
responseText || "",
|
||||
MAX_URLS,
|
||||
(matches) => {
|
||||
setUrlMatches(matches)
|
||||
setIsLoading(false)
|
||||
|
||||
// Process image checks in the background - one at a time to avoid network flooding
|
||||
const processImageChecks = async () => {
|
||||
console.log(`Starting sequential URL processing for ${matches.length} URLs`)
|
||||
|
||||
for (let i = 0; i < matches.length; i++) {
|
||||
// Skip already processed URLs (from extension check)
|
||||
if (matches[i].isProcessed) continue
|
||||
|
||||
// Check if processing has been canceled (switched to plain mode)
|
||||
if (processingCanceled) {
|
||||
console.log("URL processing canceled - display mode changed to plain")
|
||||
return
|
||||
}
|
||||
|
||||
const match = matches[i]
|
||||
console.log(`Processing URL ${i + 1} of ${matches.length}: ${match.url}`)
|
||||
|
||||
try {
|
||||
// Process each URL individually
|
||||
const isImage = await checkIfImageUrl(match.url)
|
||||
|
||||
// Skip if processing has been canceled
|
||||
if (processingCanceled) return
|
||||
|
||||
// Update the match in place
|
||||
match.isImage = isImage
|
||||
match.isProcessed = true
|
||||
|
||||
// Update state after each URL to show progress
|
||||
// Create a new array to ensure React detects the state change
|
||||
setUrlMatches([...matches])
|
||||
} catch (err) {
|
||||
console.log(`URL check error: ${match.url}`, err)
|
||||
match.isProcessed = true
|
||||
|
||||
// Update state even on error
|
||||
if (!processingCanceled) {
|
||||
setUrlMatches([...matches])
|
||||
}
|
||||
}
|
||||
|
||||
// Delay between URL processing to avoid overwhelming the network
|
||||
if (!processingCanceled && i < matches.length - 1) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`URL processing complete. Found ${matches.filter((m) => m.isImage).length} image URLs`)
|
||||
}
|
||||
|
||||
// Start the background processing
|
||||
processImageChecks()
|
||||
} catch (error) {
|
||||
setError("Failed to process response content. Switch to plain text mode to view safely.")
|
||||
},
|
||||
(updatedMatches) => {
|
||||
setUrlMatches(updatedMatches)
|
||||
},
|
||||
(errorMessage) => {
|
||||
setError(errorMessage)
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
processResponse()
|
||||
return cleanup
|
||||
}, [responseText, mcpDisplayMode, isExpanded])
|
||||
|
||||
// Cleanup function to cancel processing if component unmounts or dependencies change
|
||||
return () => {
|
||||
processingCanceled = true
|
||||
console.log("Cleaning up URL processing")
|
||||
// Helper function to render a display segment
|
||||
const renderSegment = (segment: DisplaySegment): JSX.Element => {
|
||||
switch (segment.type) {
|
||||
case "text":
|
||||
case "url":
|
||||
return <UrlText key={segment.key}>{segment.content}</UrlText>
|
||||
|
||||
case "image":
|
||||
return (
|
||||
<div key={segment.key}>
|
||||
<ImagePreview url={segment.url!} />
|
||||
</div>
|
||||
)
|
||||
|
||||
case "link":
|
||||
return (
|
||||
<div key={segment.key} style={{ margin: "10px 0" }}>
|
||||
<LinkPreview url={segment.url!} />
|
||||
</div>
|
||||
)
|
||||
|
||||
case "error":
|
||||
return (
|
||||
<div
|
||||
key={segment.key}
|
||||
style={{
|
||||
margin: "10px 0",
|
||||
padding: "8px",
|
||||
color: "var(--vscode-errorForeground)",
|
||||
border: "1px solid var(--vscode-editorError-foreground)",
|
||||
borderRadius: "4px",
|
||||
height: "128px",
|
||||
overflow: "auto",
|
||||
}}>
|
||||
{segment.content}
|
||||
</div>
|
||||
)
|
||||
|
||||
default:
|
||||
return <React.Fragment key={segment.key} />
|
||||
}
|
||||
}, [responseText, displayMode, isExpanded])
|
||||
}
|
||||
|
||||
// Function to render content based on display mode
|
||||
const renderContent = () => {
|
||||
if (!isExpanded) {
|
||||
return null // Don't render content if not expanded
|
||||
return null
|
||||
}
|
||||
|
||||
if (isLoading && displayMode === "rich") {
|
||||
if (isLoading && mcpDisplayMode === "rich") {
|
||||
return (
|
||||
<div style={{ display: "flex", justifyContent: "center", alignItems: "center", height: "50px" }}>
|
||||
<VSCodeProgressRing />
|
||||
@@ -281,12 +185,14 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
|
||||
)
|
||||
}
|
||||
|
||||
// For plain text mode, just show the text
|
||||
if (displayMode === "plain") {
|
||||
if (mcpDisplayMode === "plain") {
|
||||
return <UrlText>{responseText}</UrlText>
|
||||
}
|
||||
|
||||
// Show error message if there was an error
|
||||
if (mcpDisplayMode === "markdown") {
|
||||
return <MarkdownBlock markdown={responseText} />
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<>
|
||||
@@ -296,97 +202,9 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
|
||||
)
|
||||
}
|
||||
|
||||
// For rich display mode, show the text with embedded content
|
||||
if (displayMode === "rich") {
|
||||
// We already know displayMode is "rich" if we get here
|
||||
// Create an array of text segments and embedded content
|
||||
const segments: JSX.Element[] = []
|
||||
let lastIndex = 0
|
||||
let segmentIndex = 0
|
||||
|
||||
// Track embed count for logging
|
||||
let embedCount = 0
|
||||
|
||||
// Add the text before the first URL
|
||||
if (urlMatches.length === 0) {
|
||||
segments.push(<UrlText key={`segment-${segmentIndex}`}>{responseText}</UrlText>)
|
||||
} else {
|
||||
for (let i = 0; i < urlMatches.length; i++) {
|
||||
const match = urlMatches[i]
|
||||
const { url, fullMatch, index } = match
|
||||
|
||||
// Add text segment before this URL
|
||||
if (index > lastIndex) {
|
||||
segments.push(
|
||||
<UrlText key={`segment-${segmentIndex++}`}>{responseText.substring(lastIndex, index)}</UrlText>,
|
||||
)
|
||||
}
|
||||
|
||||
// Add the URL text itself
|
||||
segments.push(<UrlText key={`url-${segmentIndex++}`}>{fullMatch}</UrlText>)
|
||||
|
||||
// Calculate the end position of this URL in the text
|
||||
const urlEndIndex = index + fullMatch.length
|
||||
|
||||
// Add embedded content after the URL
|
||||
// For images, use the ImagePreview component
|
||||
if (match.isImage) {
|
||||
segments.push(
|
||||
<div key={`embed-image-${url}-${segmentIndex++}`}>
|
||||
{/* Use formatUrlForOpening for network calls but preserve original URL in display */}
|
||||
<ImagePreview url={formatUrlForOpening(url)} />
|
||||
</div>,
|
||||
)
|
||||
embedCount++
|
||||
// console.log(`Added image embed for ${url}, embed count: ${embedCount}`);
|
||||
} else if (match.isProcessed) {
|
||||
// For non-image URLs or URLs we haven't processed yet, show link preview
|
||||
try {
|
||||
// Skip localhost URLs
|
||||
if (!isLocalhostUrl(url)) {
|
||||
// Use a unique key that includes the URL to ensure each preview is isolated
|
||||
segments.push(
|
||||
<div key={`embed-${url}-${segmentIndex++}`} style={{ margin: "10px 0" }}>
|
||||
{/* Already using formatUrlForOpening for link previews */}
|
||||
<LinkPreview url={formatUrlForOpening(url)} />
|
||||
</div>,
|
||||
)
|
||||
|
||||
embedCount++
|
||||
// console.log(`Added link preview for ${url}, embed count: ${embedCount}`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Link preview could not be created")
|
||||
// Show error message for failed link preview
|
||||
segments.push(
|
||||
<div
|
||||
key={`embed-error-${segmentIndex++}`}
|
||||
style={{
|
||||
margin: "10px 0",
|
||||
padding: "8px",
|
||||
color: "var(--vscode-errorForeground)",
|
||||
border: "1px solid var(--vscode-editorError-foreground)",
|
||||
borderRadius: "4px",
|
||||
height: "128px", // Fixed height
|
||||
overflow: "auto", // Allow scrolling if content overflows
|
||||
}}>
|
||||
Failed to create preview for: {url}
|
||||
</div>,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Update lastIndex for next segment
|
||||
lastIndex = urlEndIndex
|
||||
}
|
||||
|
||||
// Add any remaining text after the last URL
|
||||
if (lastIndex < responseText.length) {
|
||||
segments.push(<UrlText key={`segment-${segmentIndex++}`}>{responseText.substring(lastIndex)}</UrlText>)
|
||||
}
|
||||
}
|
||||
|
||||
return <>{segments}</>
|
||||
if (mcpDisplayMode === "rich") {
|
||||
const segments = buildDisplaySegments(responseText, urlMatches)
|
||||
return <>{segments.map(renderSegment)}</>
|
||||
}
|
||||
|
||||
return null
|
||||
@@ -405,16 +223,15 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
|
||||
<span className={`codicon codicon-chevron-${isExpanded ? "down" : "right"} header-icon`}></span>
|
||||
Response
|
||||
</div>
|
||||
<div style={{ minWidth: isExpanded ? "auto" : "0", visibility: isExpanded ? "visible" : "hidden" }}>
|
||||
<ToggleSwitch onClick={(e) => e.stopPropagation()}>
|
||||
<span className="toggle-label">{displayMode === "rich" ? "Rich Display" : "Plain Text"}</span>
|
||||
<div
|
||||
className={`toggle-container ${displayMode === "rich" ? "active" : ""}`}
|
||||
onClick={toggleDisplayMode}>
|
||||
<div className="toggle-handle"></div>
|
||||
</div>
|
||||
</ToggleSwitch>
|
||||
</div>
|
||||
<DropdownContainer
|
||||
style={{ minWidth: isExpanded ? "auto" : "0", visibility: isExpanded ? "visible" : "hidden" }}>
|
||||
<McpDisplayModeDropdown
|
||||
value={mcpDisplayMode}
|
||||
onChange={handleDisplayModeChange}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{ minWidth: "120px" }}
|
||||
/>
|
||||
</DropdownContainer>
|
||||
</ResponseHeader>
|
||||
|
||||
{isExpanded && <div className="response-content">{renderContent()}</div>}
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
import { WebServiceClient } from "@/services/grpc-client"
|
||||
import { StringRequest } from "@shared/proto/common"
|
||||
|
||||
// Represents a URL found in the text with its position and metadata
|
||||
export interface UrlMatch {
|
||||
url: string // The actual URL
|
||||
fullMatch: string // The full matched text
|
||||
index: number // Position in the text
|
||||
isImage: boolean // Whether this URL is an image
|
||||
isProcessed: boolean // Whether we've already processed this URL (to avoid duplicates)
|
||||
}
|
||||
|
||||
// Display segment interface
|
||||
export interface DisplaySegment {
|
||||
type: "text" | "url" | "image" | "link" | "error"
|
||||
content: string
|
||||
url?: string
|
||||
key: string // Pre-computed key for React
|
||||
}
|
||||
|
||||
// Safely create a URL object with error handling and ensure HTTPS
|
||||
export const safeCreateUrl = (url: string): URL | null => {
|
||||
try {
|
||||
@@ -168,3 +185,224 @@ export const checkIfImageUrl = async (url: string): Promise<boolean> => {
|
||||
console.log(`URL protocol not supported for image check: ${url}`)
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all valid URLs from the given text
|
||||
* @param text - The text to search for URLs
|
||||
* @param maxUrls - Maximum number of URLs to extract (default: 50)
|
||||
* @returns Array of URL matches sorted by position in text
|
||||
*/
|
||||
export const extractUrlsFromText = (text: string, maxUrls: number = 50): UrlMatch[] => {
|
||||
const matches: UrlMatch[] = []
|
||||
const urlRegex = /(?:https?:\/\/|data:image)[^\s<>"']+/g
|
||||
let urlMatch: RegExpExecArray | null
|
||||
let urlCount = 0
|
||||
|
||||
while ((urlMatch = urlRegex.exec(text)) !== null && urlCount < maxUrls) {
|
||||
const url = urlMatch[0]
|
||||
|
||||
// Skip invalid URLs
|
||||
if (!isUrl(url)) {
|
||||
console.log("Skipping invalid URL:", url)
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip localhost URLs to prevent security issues
|
||||
if (isLocalhostUrl(url)) {
|
||||
console.log("Skipping localhost URL:", url)
|
||||
continue
|
||||
}
|
||||
|
||||
matches.push({
|
||||
url,
|
||||
fullMatch: url,
|
||||
index: urlMatch.index,
|
||||
isImage: false, // Will be determined later
|
||||
isProcessed: false,
|
||||
})
|
||||
|
||||
urlCount++
|
||||
}
|
||||
|
||||
console.log(`Found ${matches.length} URLs in text`)
|
||||
return matches.sort((a, b) => a.index - b.index)
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes URLs to determine their types (e.g., image vs link)
|
||||
* Processes URLs sequentially to avoid network flooding
|
||||
* @param matches - Array of URL matches to process
|
||||
* @param onProgress - Callback for progress updates with updated matches
|
||||
* @param cancellationToken - Object to check if processing should be cancelled
|
||||
* @returns Promise that resolves when processing is complete
|
||||
*/
|
||||
export const processUrlTypes = async (
|
||||
matches: UrlMatch[],
|
||||
onProgress: (updatedMatches: UrlMatch[]) => void,
|
||||
cancellationToken: { cancelled: boolean },
|
||||
): Promise<void> => {
|
||||
console.log(`Starting sequential URL processing for ${matches.length} URLs`)
|
||||
|
||||
for (let i = 0; i < matches.length; i++) {
|
||||
// Skip already processed URLs
|
||||
if (matches[i].isProcessed) continue
|
||||
|
||||
// Check if processing has been canceled
|
||||
if (cancellationToken.cancelled) {
|
||||
console.log("URL processing canceled")
|
||||
return
|
||||
}
|
||||
|
||||
const match = matches[i]
|
||||
console.log(`Processing URL ${i + 1} of ${matches.length}: ${match.url}`)
|
||||
|
||||
try {
|
||||
// Check if URL is an image
|
||||
const isImage = await checkIfImageUrl(match.url)
|
||||
|
||||
// Skip if processing has been canceled
|
||||
if (cancellationToken.cancelled) return
|
||||
|
||||
// Update the match
|
||||
match.isImage = isImage
|
||||
match.isProcessed = true
|
||||
|
||||
// Notify progress with a new array to ensure React detects changes
|
||||
onProgress([...matches])
|
||||
} catch (err) {
|
||||
console.log(`URL check error: ${match.url}`, err)
|
||||
match.isProcessed = true
|
||||
|
||||
// Update state even on error
|
||||
if (!cancellationToken.cancelled) {
|
||||
onProgress([...matches])
|
||||
}
|
||||
}
|
||||
|
||||
// Delay between URL processing to avoid overwhelming the network
|
||||
if (!cancellationToken.cancelled && i < matches.length - 1) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`URL processing complete. Found ${matches.filter((m) => m.isImage).length} image URLs`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Orchestrates the URL extraction and processing pipeline
|
||||
* @param text - The response text to process
|
||||
* @param maxUrls - Maximum number of URLs to process
|
||||
* @param onMatchesFound - Callback when initial URLs are extracted
|
||||
* @param onMatchesUpdated - Callback when URL types are determined
|
||||
* @param onError - Error handler callback
|
||||
* @returns Cleanup function to cancel processing
|
||||
*/
|
||||
export const processResponseUrls = (
|
||||
text: string,
|
||||
maxUrls: number,
|
||||
onMatchesFound: (matches: UrlMatch[]) => void,
|
||||
onMatchesUpdated: (matches: UrlMatch[]) => void,
|
||||
onError: (error: string) => void,
|
||||
): (() => void) => {
|
||||
const cancellationToken = { cancelled: false }
|
||||
|
||||
const process = async () => {
|
||||
try {
|
||||
// Extract URLs from text
|
||||
const matches = extractUrlsFromText(text, maxUrls)
|
||||
|
||||
// Immediately notify about found matches
|
||||
onMatchesFound(matches)
|
||||
|
||||
// Process URLs in the background
|
||||
await processUrlTypes(matches, onMatchesUpdated, cancellationToken)
|
||||
} catch (error) {
|
||||
onError("Failed to process response content. Switch to plain text mode to view safely.")
|
||||
}
|
||||
}
|
||||
|
||||
// Start processing
|
||||
process()
|
||||
|
||||
// Return cleanup function
|
||||
return () => {
|
||||
cancellationToken.cancelled = true
|
||||
console.log("Cleaning up URL processing")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an array of display segments from response text and URL matches
|
||||
* @param responseText - The full response text
|
||||
* @param urlMatches - Array of URL matches with their positions and types
|
||||
* @returns Array of display segments describing how to render the content
|
||||
*/
|
||||
export const buildDisplaySegments = (responseText: string, urlMatches: UrlMatch[]): DisplaySegment[] => {
|
||||
const segments: DisplaySegment[] = []
|
||||
let lastIndex = 0
|
||||
let segmentIndex = 0
|
||||
|
||||
// Handle case with no URLs
|
||||
if (urlMatches.length === 0) {
|
||||
return [
|
||||
{
|
||||
type: "text",
|
||||
content: responseText,
|
||||
key: "segment-0",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
// Process each URL match
|
||||
for (let i = 0; i < urlMatches.length; i++) {
|
||||
const match = urlMatches[i]
|
||||
const { url, fullMatch, index } = match
|
||||
|
||||
// Add text segment before this URL
|
||||
if (index > lastIndex) {
|
||||
segments.push({
|
||||
type: "text",
|
||||
content: responseText.substring(lastIndex, index),
|
||||
key: `segment-${segmentIndex++}`,
|
||||
})
|
||||
}
|
||||
|
||||
// Add the URL text itself
|
||||
segments.push({
|
||||
type: "url",
|
||||
content: fullMatch,
|
||||
key: `url-${segmentIndex++}`,
|
||||
})
|
||||
|
||||
// Add embedded content after the URL
|
||||
if (match.isImage) {
|
||||
segments.push({
|
||||
type: "image",
|
||||
content: url,
|
||||
url: formatUrlForOpening(url),
|
||||
key: `embed-image-${url}-${segmentIndex++}`,
|
||||
})
|
||||
} else if (match.isProcessed && !isLocalhostUrl(url)) {
|
||||
segments.push({
|
||||
type: "link",
|
||||
content: url,
|
||||
url: formatUrlForOpening(url),
|
||||
key: `embed-${url}-${segmentIndex++}`,
|
||||
})
|
||||
}
|
||||
|
||||
// Update lastIndex for next segment
|
||||
lastIndex = index + fullMatch.length
|
||||
}
|
||||
|
||||
// Add any remaining text after the last URL
|
||||
if (lastIndex < responseText.length) {
|
||||
segments.push({
|
||||
type: "text",
|
||||
content: responseText.substring(lastIndex),
|
||||
key: `segment-${segmentIndex++}`,
|
||||
})
|
||||
}
|
||||
|
||||
return segments
|
||||
}
|
||||
|
||||
@@ -54,9 +54,9 @@ const featuredModels = [
|
||||
label: "Trending",
|
||||
},
|
||||
{
|
||||
id: "x-ai/grok-4",
|
||||
description: "Latest flagship model from xAI",
|
||||
label: "Fast & Cheap",
|
||||
id: "moonshotai/kimi-k2",
|
||||
description: "Open source model topping coding benchmarks",
|
||||
label: "New",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -32,28 +32,40 @@ export const BedrockProvider = ({ showModelOptions, isPopup }: BedrockProviderPr
|
||||
gap: 5,
|
||||
}}>
|
||||
<VSCodeRadioGroup
|
||||
value={apiConfiguration?.awsUseProfile ? "profile" : "credentials"}
|
||||
value={apiConfiguration?.awsAuthentication ?? (apiConfiguration?.awsProfile ? "profile" : "credentials")}
|
||||
onChange={(e) => {
|
||||
const value = (e.target as HTMLInputElement)?.value
|
||||
const useProfile = value === "profile"
|
||||
|
||||
handleFieldChange("awsUseProfile", useProfile)
|
||||
handleFieldChange("awsAuthentication", value)
|
||||
}}>
|
||||
<VSCodeRadio value="credentials">AWS Credentials</VSCodeRadio>
|
||||
<VSCodeRadio value="apikey">API Key</VSCodeRadio>
|
||||
<VSCodeRadio value="profile">AWS Profile</VSCodeRadio>
|
||||
<VSCodeRadio value="credentials">AWS Credentials</VSCodeRadio>
|
||||
</VSCodeRadioGroup>
|
||||
|
||||
{apiConfiguration?.awsUseProfile ? (
|
||||
{(apiConfiguration?.awsAuthentication === undefined && apiConfiguration?.awsUseProfile) ||
|
||||
apiConfiguration?.awsAuthentication == "profile" ? (
|
||||
<DebouncedTextField
|
||||
initialValue={apiConfiguration?.awsProfile || ""}
|
||||
onChange={(value) => handleFieldChange("awsProfile", value)}
|
||||
key="profile"
|
||||
initialValue={apiConfiguration?.awsProfile ?? ""}
|
||||
style={{ width: "100%" }}
|
||||
onChange={(value) => handleFieldChange("awsProfile", value)}
|
||||
placeholder="Enter profile name (default if empty)">
|
||||
<span style={{ fontWeight: 500 }}>AWS Profile Name</span>
|
||||
</DebouncedTextField>
|
||||
) : apiConfiguration?.awsAuthentication == "apikey" ? (
|
||||
<DebouncedTextField
|
||||
key="apikey"
|
||||
type="password"
|
||||
initialValue={apiConfiguration?.awsBedrockApiKey ?? ""}
|
||||
style={{ width: "100%" }}
|
||||
onChange={(value) => handleFieldChange("awsBedrockApiKey", value)}
|
||||
placeholder="Enter Bedrock Api Key">
|
||||
<span style={{ fontWeight: 500 }}>AWS Bedrock Api Key</span>
|
||||
</DebouncedTextField>
|
||||
) : (
|
||||
<>
|
||||
<DebouncedTextField
|
||||
key="accessKey"
|
||||
initialValue={apiConfiguration?.awsAccessKey || ""}
|
||||
onChange={(value) => handleFieldChange("awsAccessKey", value)}
|
||||
style={{ width: "100%" }}
|
||||
|
||||
@@ -4,6 +4,8 @@ import { memo } from "react"
|
||||
import { OpenAIReasoningEffort } from "@shared/ChatSettings"
|
||||
import { updateSetting } from "../utils/settingsHandlers"
|
||||
import { convertChatSettingsToProtoChatSettings } from "@shared/proto-conversions/state/chat-settings-conversion"
|
||||
import { McpDisplayMode } from "@shared/McpDisplayMode"
|
||||
import McpDisplayModeDropdown from "@/components/mcp/chat-display/McpDisplayModeDropdown"
|
||||
import Section from "../Section"
|
||||
|
||||
interface FeatureSettingsSectionProps {
|
||||
@@ -11,7 +13,7 @@ interface FeatureSettingsSectionProps {
|
||||
}
|
||||
|
||||
const FeatureSettingsSection = ({ renderSectionHeader }: FeatureSettingsSectionProps) => {
|
||||
const { enableCheckpointsSetting, mcpMarketplaceEnabled, mcpRichDisplayEnabled, mcpResponsesCollapsed, chatSettings } =
|
||||
const { enableCheckpointsSetting, mcpMarketplaceEnabled, mcpDisplayMode, mcpResponsesCollapsed, chatSettings } =
|
||||
useExtensionState()
|
||||
|
||||
const handleReasoningEffortChange = (newValue: OpenAIReasoningEffort) => {
|
||||
@@ -59,16 +61,20 @@ const FeatureSettingsSection = ({ renderSectionHeader }: FeatureSettingsSectionP
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<VSCodeCheckbox
|
||||
checked={mcpRichDisplayEnabled}
|
||||
onChange={(e: any) => {
|
||||
const checked = e.target.checked === true
|
||||
updateSetting("mcpRichDisplayEnabled", 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.
|
||||
<label
|
||||
htmlFor="mcp-display-mode-dropdown"
|
||||
className="block text-sm font-medium text-[var(--vscode-foreground)] mb-1">
|
||||
MCP Display Mode
|
||||
</label>
|
||||
<McpDisplayModeDropdown
|
||||
id="mcp-display-mode-dropdown"
|
||||
value={mcpDisplayMode}
|
||||
onChange={(newMode: McpDisplayMode) => updateSetting("mcpDisplayMode", newMode)}
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="text-xs mt-[5px] text-[var(--vscode-descriptionForeground)]">
|
||||
Controls how MCP responses are displayed: plain text, rich formatting with links/images, or markdown
|
||||
rendering.
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
|
||||
@@ -14,13 +14,11 @@ 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"
|
||||
import { DEFAULT_BROWSER_SETTINGS, BrowserSettings } from "@shared/BrowserSettings"
|
||||
import { DEFAULT_BROWSER_SETTINGS } from "@shared/BrowserSettings"
|
||||
import { ChatSettings, DEFAULT_CHAT_SETTINGS } from "@shared/ChatSettings"
|
||||
import { DEFAULT_PLATFORM, ExtensionMessage, ExtensionState } from "@shared/ExtensionMessage"
|
||||
import { TelemetrySetting } from "@shared/TelemetrySetting"
|
||||
import { DEFAULT_PLATFORM, ExtensionState } from "@shared/ExtensionMessage"
|
||||
import { findLastIndex } from "@shared/array"
|
||||
import {
|
||||
ApiConfiguration,
|
||||
ModelInfo,
|
||||
openRouterDefaultModelId,
|
||||
openRouterDefaultModelInfo,
|
||||
@@ -31,6 +29,7 @@ import { McpMarketplaceCatalog, McpServer, McpViewTab } from "../../../src/share
|
||||
import { convertTextMateToHljs } from "../utils/textMateToHljs"
|
||||
import { OpenRouterCompatibleModelInfo } from "@shared/proto/models"
|
||||
import { UserInfo } from "@shared/proto/account"
|
||||
import { DEFAULT_MCP_DISPLAY_MODE } from "@shared/McpDisplayMode"
|
||||
|
||||
interface ExtensionStateContextType extends ExtensionState {
|
||||
didHydrateState: boolean
|
||||
@@ -54,19 +53,8 @@ interface ExtensionStateContextType extends ExtensionState {
|
||||
showAnnouncement: boolean
|
||||
|
||||
// Setters
|
||||
setApiConfiguration: (config: ApiConfiguration) => void
|
||||
setTelemetrySetting: (value: TelemetrySetting) => void
|
||||
setShowAnnouncement: (value: boolean) => void
|
||||
setShouldShowAnnouncement: (value: boolean) => void
|
||||
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
|
||||
setRequestyModels: (value: Record<string, ModelInfo>) => void
|
||||
@@ -78,8 +66,6 @@ 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
|
||||
setBrowserSettings: (value: BrowserSettings) => void
|
||||
|
||||
// Refresh functions
|
||||
refreshOpenRouterModels: () => void
|
||||
@@ -190,7 +176,7 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
distinctId: "",
|
||||
planActSeparateModelsSetting: true,
|
||||
enableCheckpointsSetting: true,
|
||||
mcpRichDisplayEnabled: true,
|
||||
mcpDisplayMode: DEFAULT_MCP_DISPLAY_MODE,
|
||||
globalClineRulesToggles: {},
|
||||
localClineRulesToggles: {},
|
||||
localCursorRulesToggles: {},
|
||||
@@ -676,72 +662,15 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
hideHistory,
|
||||
hideAccount,
|
||||
hideAnnouncement,
|
||||
setApiConfiguration: (value) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
apiConfiguration: value,
|
||||
})),
|
||||
setTelemetrySetting: (value) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
telemetrySetting: value,
|
||||
})),
|
||||
setPlanActSeparateModelsSetting: (value) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
planActSeparateModelsSetting: value,
|
||||
})),
|
||||
setEnableCheckpointsSetting: (value) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
enableCheckpointsSetting: value,
|
||||
})),
|
||||
setMcpMarketplaceEnabled: (value) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
mcpMarketplaceEnabled: value,
|
||||
})),
|
||||
setMcpRichDisplayEnabled: (value) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
mcpRichDisplayEnabled: value,
|
||||
})),
|
||||
setMcpResponsesCollapsed: (value) => {
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
mcpResponsesCollapsed: value,
|
||||
}))
|
||||
},
|
||||
setShowAnnouncement,
|
||||
setShouldShowAnnouncement: (value) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
shouldShowAnnouncement: value,
|
||||
})),
|
||||
setShellIntegrationTimeout: (value) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
shellIntegrationTimeout: value,
|
||||
})),
|
||||
setTerminalReuseEnabled: (value) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
terminalReuseEnabled: value,
|
||||
})),
|
||||
setTerminalOutputLineLimit: (value) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
terminalOutputLineLimit: value,
|
||||
})),
|
||||
setDefaultTerminalProfile: (value) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
defaultTerminalProfile: value,
|
||||
})),
|
||||
setMcpServers: (mcpServers: McpServer[]) => setMcpServers(mcpServers),
|
||||
setRequestyModels: (models: Record<string, ModelInfo>) => setRequestyModels(models),
|
||||
setMcpMarketplaceCatalog: (catalog: McpMarketplaceCatalog) => setMcpMarketplaceCatalog(catalog),
|
||||
setAvailableTerminalProfiles,
|
||||
setShowMcp,
|
||||
closeMcpView,
|
||||
setChatSettings: async (value) => {
|
||||
@@ -768,7 +697,7 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
planActSeparateModelsSetting: state.planActSeparateModelsSetting,
|
||||
enableCheckpointsSetting: state.enableCheckpointsSetting,
|
||||
mcpMarketplaceEnabled: state.mcpMarketplaceEnabled,
|
||||
mcpRichDisplayEnabled: state.mcpRichDisplayEnabled,
|
||||
mcpDisplayMode: state.mcpDisplayMode,
|
||||
mcpResponsesCollapsed: state.mcpResponsesCollapsed,
|
||||
}),
|
||||
)
|
||||
@@ -811,11 +740,6 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
refreshOpenRouterModels,
|
||||
onRelinquishControl,
|
||||
setUserInfo: (userInfo?: UserInfo) => setState((prevState) => ({ ...prevState, userInfo })),
|
||||
setBrowserSettings: (value: BrowserSettings) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
browserSettings: value,
|
||||
})),
|
||||
}
|
||||
|
||||
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
|
||||
|
||||
Reference in New Issue
Block a user