mirror of
https://github.com/cline/cline.git
synced 2026-09-13 09:50:12 +08:00
Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
924e28c6b5 | ||
|
|
309e3bd85c | ||
|
|
eb6eb371e4 | ||
|
|
419e3e4677 | ||
|
|
e95eecd65f | ||
|
|
e83e71cc6d | ||
|
|
a9e526e99b | ||
|
|
72f16a8c30 | ||
|
|
1a3ec9f024 | ||
|
|
b3fa3ad0d3 | ||
|
|
45241fcccf | ||
|
|
2a3f0e9418 | ||
|
|
4334764903 | ||
|
|
b3a10243b8 | ||
|
|
8e95c136a6 | ||
|
|
0b66faa1dd | ||
|
|
3d2dc1c5c4 | ||
|
|
028412579b | ||
|
|
f04788c2ec |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: mcp servers are not started when disabled
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Improve the Claude Code error messages
|
||||
@@ -94,6 +94,7 @@ jobs:
|
||||
env:
|
||||
VSCE_PAT: ${{ secrets.VSCE_PAT }}
|
||||
OVSX_PAT: ${{ secrets.OVSX_PAT }}
|
||||
CLINE_ENVIRONMENT: production
|
||||
run: |
|
||||
# Required to generate the .vsix
|
||||
vsce package --out "cline-${{ steps.get_version.outputs.version }}.vsix"
|
||||
|
||||
Vendored
+4
-2
@@ -14,7 +14,8 @@
|
||||
"preLaunchTask": "${defaultBuildTask}",
|
||||
"env": {
|
||||
"IS_DEV": "true",
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}"
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
|
||||
"CLINE_ENVIRONMENT": "local"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -37,7 +38,8 @@
|
||||
"env": {
|
||||
"IS_DEV": "true",
|
||||
"TEMP_PROFILE": "true",
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}"
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
|
||||
"CLINE_ENVIRONMENT": "local"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## [3.19.7]
|
||||
|
||||
- Add Hugging Face as a new API provider with support for their inference API models
|
||||
- Improve Claude Code error messages with better guidance for common setup issues (Thanks @BarreiroT!)
|
||||
- Fix authentication sync issues when using multiple VSCode windows
|
||||
|
||||
## [3.19.6]
|
||||
|
||||
- Improve Kimi K2 model provider routing with additional provider options for better availability and performance
|
||||
|
||||
+2
-1
@@ -159,7 +159,8 @@
|
||||
"provider-config/openai",
|
||||
"provider-config/openai-compatible",
|
||||
"provider-config/openrouter",
|
||||
"provider-config/requesty"
|
||||
"provider-config/requesty",
|
||||
"provider-config/sap-aicore"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
title: "SAP AI Core"
|
||||
description: "Learn how to configure and use LLM models from Generative AI Hub in SAP AI Core with Cline."
|
||||
---
|
||||
|
||||
SAP AI Core and the generative AI hub help you to integrate LLMs and AI into new business processes in a cost-efficient manner.
|
||||
|
||||
**Website:** [SAP Help Portal](https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/what-is-sap-ai-core)
|
||||
|
||||
### Getting a Service Binding
|
||||
|
||||
> 💡 **Information**
|
||||
>
|
||||
> SAP AI Core, and Generative AI Hub, are offerings from SAP BTP.
|
||||
> You need an active SAP BTP contract and a existing subaccount with a SAP AI Core instance to perform these steps.
|
||||
|
||||
1. **Access:** Go to your subaccount via [BTP Cloud Cockpit](cockpit.btp.cloud.sap/cockpit)
|
||||
2. **Create a Service Binding:** Go to "Instances and Subscriptions", select your SAP AI Core service instance and click on Service Bindings > Create.
|
||||
3. **Copy the Service Binding:** Copy the service binding values.
|
||||
|
||||
### Supported Models
|
||||
|
||||
SAP AI Core supports a large and growing number of models.
|
||||
Refer to the [Generative AI Hub Supported Models page](https://me.sap.com/notes/3437766) for the complete and up-to-date list.
|
||||
|
||||
### Configuration in Cline
|
||||
|
||||
1. **Open Cline Settings:** Click the settings icon (⚙️) in the Cline panel.
|
||||
2. **Select Provider:** Choose "SAP AI Core" from the "API Provider" dropdown.
|
||||
3. **Enter Client Id:** Add the `.clientid` field from the service binding into the "AI Core Client Id" field.
|
||||
4. **Enter Client Secret:** Add the `.clientsecret` field from the service binding into the "AI Core Client Secret" field.
|
||||
5. **Enter Base URL:** Add the `.serviceurls.AI_API_URL` field from the service binding into the "AI Core Base URL" field.
|
||||
6. **Enter Auth URL:** Add the `.url` field from the service binding into the "AI Core Auth URL" field.
|
||||
7. **Enter Resource Group:** Add the resource group where you have your model deployments. See [Create a Deployment for a Generative AI Model](https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/create-deployment-for-generative-ai-model-in-sap-ai-core).
|
||||
8. **Select Model:** Choose your desired model from the "Model" dropdown.
|
||||
|
||||
### Tips and Notes
|
||||
|
||||
- **Model Selection:** SAP AI Core offers a wide range of models. You won't be able to use the model, even if selected, if a deployment doesn't exist in the provided resource group.
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-dev",
|
||||
"version": "3.19.6",
|
||||
"version": "3.19.7",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-dev",
|
||||
"version": "3.19.6",
|
||||
"version": "3.19.7",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.37.0",
|
||||
|
||||
+1
-1
@@ -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.19.6",
|
||||
"version": "3.19.7",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
|
||||
+40
-2
@@ -10,7 +10,16 @@ import "common.proto";
|
||||
service DiffService {
|
||||
// Open the diff view/editor.
|
||||
rpc openDiff(OpenDiffRequest) returns (OpenDiffResponse);
|
||||
// Get the contents of the diff view.
|
||||
rpc getDocumentText(GetDocumentTextRequest) returns (GetDocumentTextResponse);
|
||||
// Replace a text selection in the diff.
|
||||
rpc replaceText(ReplaceTextRequest) returns (ReplaceTextResponse);
|
||||
// Truncate the diff document.
|
||||
rpc truncateDocument(TruncateDocumentRequest) returns (TruncateDocumentResponse);
|
||||
// Save the diff document.
|
||||
rpc saveDocument(SaveDocumentRequest) returns (SaveDocumentResponse);
|
||||
// Close the diff editor UI.
|
||||
rpc closeDiff(CloseDiffRequest) returns (CloseDiffResponse);
|
||||
}
|
||||
|
||||
message OpenDiffRequest {
|
||||
@@ -26,6 +35,15 @@ message OpenDiffResponse {
|
||||
optional string diff_id = 1;
|
||||
}
|
||||
|
||||
message GetDocumentTextRequest {
|
||||
optional cline.Metadata metadata = 1;
|
||||
optional string diff_id = 2;
|
||||
}
|
||||
|
||||
message GetDocumentTextResponse {
|
||||
optional string content = 1;
|
||||
}
|
||||
|
||||
message ReplaceTextRequest {
|
||||
optional cline.Metadata metadata = 1;
|
||||
optional string diff_id = 2;
|
||||
@@ -34,6 +52,26 @@ message ReplaceTextRequest {
|
||||
optional int32 end_line = 5;
|
||||
}
|
||||
|
||||
message ReplaceTextResponse {
|
||||
// TBD
|
||||
message ReplaceTextResponse {}
|
||||
|
||||
message TruncateDocumentRequest {
|
||||
optional cline.Metadata metadata = 1;
|
||||
optional string diff_id = 2;
|
||||
optional int32 end_line = 5;
|
||||
}
|
||||
|
||||
message TruncateDocumentResponse {}
|
||||
|
||||
message CloseDiffRequest {
|
||||
optional cline.Metadata metadata = 1;
|
||||
optional string diff_id = 2;
|
||||
}
|
||||
|
||||
message CloseDiffResponse {}
|
||||
|
||||
message SaveDocumentRequest {
|
||||
optional cline.Metadata metadata = 1;
|
||||
optional string diff_id = 2;
|
||||
}
|
||||
|
||||
message SaveDocumentResponse {}
|
||||
|
||||
@@ -6,6 +6,10 @@ option java_multiple_files = true;
|
||||
|
||||
import "common.proto";
|
||||
|
||||
/**
|
||||
* The watch service is only here as example of a streaming rpc in the host bridge.
|
||||
* This being replaced with a native JS file watcher.
|
||||
*/
|
||||
// WatchService provides methods for watching files in the IDE
|
||||
service WatchService {
|
||||
// Subscribe to file changes
|
||||
|
||||
@@ -15,6 +15,8 @@ service ModelsService {
|
||||
rpc getVsCodeLmModels(EmptyRequest) returns (VsCodeLmModelsArray);
|
||||
// Refreshes and returns OpenRouter models
|
||||
rpc refreshOpenRouterModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
// Refreshes and returns Hugging Face models
|
||||
rpc refreshHuggingFaceModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
// Refreshes and returns OpenAI models
|
||||
rpc refreshOpenAiModels(OpenAiModelsRequest) returns (StringArray);
|
||||
// Refreshes and returns Requesty models
|
||||
@@ -126,6 +128,7 @@ enum ApiProvider {
|
||||
SAPAICORE = 25;
|
||||
CLAUDE_CODE = 26;
|
||||
MOONSHOT = 27;
|
||||
HUGGINGFACE = 28;
|
||||
}
|
||||
|
||||
// Model info for OpenAI-compatible models
|
||||
@@ -246,4 +249,7 @@ message ModelsApiConfiguration {
|
||||
optional string groq_api_key = 78;
|
||||
optional string groq_model_id = 79;
|
||||
optional OpenRouterModelInfo groq_model_info = 80;
|
||||
optional string hugging_face_api_key = 81;
|
||||
optional string hugging_face_model_id = 82;
|
||||
optional OpenRouterModelInfo hugging_face_model_info = 83;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import { SapAiCoreHandler } from "./providers/sapaicore"
|
||||
import { ClaudeCodeHandler } from "./providers/claude-code"
|
||||
import { MoonshotHandler } from "./providers/moonshot"
|
||||
import { GroqHandler } from "./providers/groq"
|
||||
import { HuggingFaceHandler } from "./providers/huggingface"
|
||||
|
||||
export interface ApiHandler {
|
||||
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream
|
||||
@@ -195,6 +196,11 @@ function createHandlerForProvider(apiProvider: string | undefined, options: Omit
|
||||
moonshotApiLine: options.moonshotApiLine,
|
||||
apiModelId: options.apiModelId,
|
||||
})
|
||||
case "huggingface":
|
||||
return new HuggingFaceHandler({
|
||||
huggingFaceApiKey: options.huggingFaceApiKey,
|
||||
apiModelId: options.apiModelId,
|
||||
})
|
||||
case "nebius":
|
||||
return new NebiusHandler({
|
||||
nebiusApiKey: options.nebiusApiKey,
|
||||
|
||||
@@ -177,17 +177,7 @@ export class ClineHandler implements ApiHandler {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Cline API Error:", error)
|
||||
const requestId = error?.request_id ? `\n | Request ID: ${error.request_id}` : ""
|
||||
if (error.code === "ERR_BAD_REQUEST" || error.status === 401) {
|
||||
throw new Error(CLINE_ACCOUNT_AUTH_ERROR_MESSAGE + requestId)
|
||||
} else if (error.code === "insufficient_credits" || error.status === 402) {
|
||||
if (error.error) {
|
||||
throw new Error(JSON.stringify(error.error))
|
||||
}
|
||||
}
|
||||
const _error = error instanceof Error ? error : new Error(String(error))
|
||||
_error.message = _error.message + requestId
|
||||
throw _error
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiHandler } from "../"
|
||||
import { ApiHandlerOptions, HuggingFaceModelId, ModelInfo, huggingFaceDefaultModelId, huggingFaceModels } from "@shared/api"
|
||||
import { calculateApiCostOpenAI } from "../../utils/cost"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
interface HuggingFaceHandlerOptions {
|
||||
huggingFaceApiKey?: string
|
||||
apiModelId?: string
|
||||
}
|
||||
|
||||
export class HuggingFaceHandler implements ApiHandler {
|
||||
private options: HuggingFaceHandlerOptions
|
||||
private client: OpenAI | undefined
|
||||
private cachedModel: { id: HuggingFaceModelId; info: ModelInfo } | undefined
|
||||
|
||||
constructor(options: HuggingFaceHandlerOptions) {
|
||||
this.options = options
|
||||
}
|
||||
|
||||
private ensureClient(): OpenAI {
|
||||
if (!this.client) {
|
||||
if (!this.options.huggingFaceApiKey) {
|
||||
throw new Error("Hugging Face API key is required")
|
||||
}
|
||||
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://router.huggingface.co/v1",
|
||||
apiKey: this.options.huggingFaceApiKey,
|
||||
defaultHeaders: {
|
||||
"User-Agent": "Cline/1.0",
|
||||
},
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw new Error(`Error creating Hugging Face client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
|
||||
private async *yieldUsage(info: ModelInfo, usage: OpenAI.Completions.CompletionUsage | undefined): ApiStream {
|
||||
if (!usage) {
|
||||
return
|
||||
}
|
||||
|
||||
const inputTokens = usage.prompt_tokens || 0
|
||||
const outputTokens = usage.completion_tokens || 0
|
||||
const totalCost = calculateApiCostOpenAI(info, inputTokens, outputTokens)
|
||||
|
||||
const usageData = {
|
||||
type: "usage" as const,
|
||||
inputTokens: inputTokens,
|
||||
outputTokens: outputTokens,
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
totalCost: totalCost,
|
||||
}
|
||||
|
||||
yield usageData
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
try {
|
||||
const client = this.ensureClient()
|
||||
const model = this.getModel()
|
||||
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
const requestParams = {
|
||||
model: model.id,
|
||||
max_tokens: model.info.maxTokens,
|
||||
messages: openAiMessages,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
temperature: 0,
|
||||
}
|
||||
|
||||
const stream = (await client.chat.completions.create(requestParams)) as any
|
||||
|
||||
let chunkCount = 0
|
||||
let totalContent = ""
|
||||
|
||||
for await (const chunk of stream) {
|
||||
chunkCount++
|
||||
const delta = chunk.choices[0]?.delta
|
||||
if (delta?.content) {
|
||||
totalContent += delta.content
|
||||
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
yield* this.yieldUsage(model.info, chunk.usage)
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
getModel(): { id: HuggingFaceModelId; info: ModelInfo } {
|
||||
// Return cached model if available
|
||||
if (this.cachedModel) {
|
||||
return this.cachedModel
|
||||
}
|
||||
|
||||
const modelId = this.options.apiModelId
|
||||
|
||||
// List all available models for debugging
|
||||
const availableModels = Object.keys(huggingFaceModels)
|
||||
let result: { id: HuggingFaceModelId; info: ModelInfo }
|
||||
|
||||
if (modelId && modelId in huggingFaceModels) {
|
||||
const id = modelId as HuggingFaceModelId
|
||||
const modelInfo = huggingFaceModels[id]
|
||||
result = { id, info: modelInfo }
|
||||
} else {
|
||||
const defaultInfo = huggingFaceModels[huggingFaceDefaultModelId]
|
||||
result = {
|
||||
id: huggingFaceDefaultModelId,
|
||||
info: defaultInfo,
|
||||
}
|
||||
}
|
||||
|
||||
// Cache the result for future calls
|
||||
this.cachedModel = result
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
export type Environment = "production" | "staging" | "local"
|
||||
|
||||
const CURRENT_ENVIRONMENT: Environment = "production"
|
||||
const CLINE_ENVIRONMENT: Environment = (process.env.CLINE_ENVIRONMENT as Environment) || "production"
|
||||
|
||||
interface EnvironmentConfig {
|
||||
appBaseUrl: string
|
||||
@@ -55,4 +55,4 @@ const configs: Record<Environment, EnvironmentConfig> = {
|
||||
},
|
||||
}
|
||||
|
||||
export const clineEnvConfig = configs[CURRENT_ENVIRONMENT]
|
||||
export const clineEnvConfig = configs[CLINE_ENVIRONMENT]
|
||||
|
||||
@@ -44,12 +44,10 @@ export const createRuleFile: FileMethodHandler = async (controller: Controller,
|
||||
|
||||
if (fileExists) {
|
||||
const message = `${fileTypeName} file "${request.filename}" already exists.`
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.WARNING,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.WARNING,
|
||||
message,
|
||||
})
|
||||
// Still open it for editing
|
||||
await handleFileServiceRequest(controller, "openFile", { value: filePath })
|
||||
} else {
|
||||
@@ -63,12 +61,10 @@ export const createRuleFile: FileMethodHandler = async (controller: Controller,
|
||||
await handleFileServiceRequest(controller, "openFile", { value: filePath })
|
||||
|
||||
const message = `Created new ${request.isGlobal ? "global" : "workspace"} ${fileTypeName} file: ${request.filename}`
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message,
|
||||
})
|
||||
}
|
||||
|
||||
return RuleFile.create({
|
||||
|
||||
@@ -46,12 +46,10 @@ export const deleteRuleFile: FileMethodHandler = async (controller: Controller,
|
||||
const fileTypeName = request.type === "workflow" ? "workflow" : "rule"
|
||||
|
||||
const message = `${fileTypeName} file "${fileName}" deleted successfully`
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message,
|
||||
})
|
||||
|
||||
return RuleFile.create({
|
||||
filePath: request.rulePath,
|
||||
|
||||
@@ -114,19 +114,15 @@ export class Controller {
|
||||
await updateGlobalState(this.context, "userInfo", undefined)
|
||||
await updateGlobalState(this.context, "apiProvider", "openrouter")
|
||||
await this.postStateToWebview()
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Successfully logged out of Cline",
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Successfully logged out of Cline",
|
||||
})
|
||||
} catch (error) {
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Logout failed",
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Logout failed",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -485,12 +481,10 @@ export class Controller {
|
||||
await this.postStateToWebview()
|
||||
} catch (error) {
|
||||
console.error("Failed to handle auth callback:", error)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Failed to log in to Cline",
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Failed to log in to Cline",
|
||||
})
|
||||
// Even on login failure, we preserve any existing tokens
|
||||
// Only clear tokens on explicit logout
|
||||
}
|
||||
@@ -525,12 +519,10 @@ export class Controller {
|
||||
console.error("Failed to fetch MCP marketplace:", error)
|
||||
if (!silent) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Failed to fetch MCP marketplace"
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: errorMessage,
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: errorMessage,
|
||||
})
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
@@ -614,12 +606,10 @@ export class Controller {
|
||||
} catch (error) {
|
||||
console.error("Failed to handle cached MCP marketplace:", error)
|
||||
const errorMessage = error instanceof Error ? error.message : "Failed to handle cached MCP marketplace"
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: errorMessage,
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: errorMessage,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -982,24 +972,20 @@ export class Controller {
|
||||
// Check if there's a workspace folder open
|
||||
const cwd = await getCwd()
|
||||
if (!cwd) {
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "No workspace folder open",
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "No workspace folder open",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Get the git diff
|
||||
const gitDiff = await getWorkingState(cwd)
|
||||
if (gitDiff === "No changes in working directory") {
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "No changes in workspace for commit message",
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "No changes in workspace for commit message",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1067,58 +1053,46 @@ Commit message:`
|
||||
const repo = api.repositories[0]
|
||||
repo.inputBox.value = commitMessage
|
||||
const message = "Commit message generated and applied"
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message,
|
||||
})
|
||||
} else {
|
||||
const message = "No Git repositories found"
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
const message = "Git extension not found"
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
const message = "Failed to generate commit message"
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message,
|
||||
})
|
||||
}
|
||||
} catch (innerError) {
|
||||
const innerErrorMessage = innerError instanceof Error ? innerError.message : String(innerError)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Failed to generate commit message: ${innerErrorMessage}`,
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Failed to generate commit message: ${innerErrorMessage}`,
|
||||
})
|
||||
}
|
||||
},
|
||||
)
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Failed to generate commit message: ${errorMessage}`,
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Failed to generate commit message: ${errorMessage}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { Controller } from ".."
|
||||
import { EmptyRequest } from "../../../shared/proto/common"
|
||||
import { OpenRouterCompatibleModelInfo, OpenRouterModelInfo } from "../../../shared/proto/models"
|
||||
import axios from "axios"
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { fileExistsAtPath } from "@utils/fs"
|
||||
import { GlobalFileNames } from "@core/storage/disk"
|
||||
import { huggingFaceModels } from "@shared/api"
|
||||
|
||||
/**
|
||||
* Ensures the cache directory exists and returns its path
|
||||
*/
|
||||
async function ensureCacheDirectoryExists(controller: Controller): Promise<string> {
|
||||
const cacheDir = path.join(controller.context.globalStorageUri.fsPath, "cache")
|
||||
try {
|
||||
await fs.mkdir(cacheDir, { recursive: true })
|
||||
} catch (error) {
|
||||
// Directory might already exist
|
||||
}
|
||||
return cacheDir
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the Hugging Face models and returns the updated model list
|
||||
* @param controller The controller instance
|
||||
* @param request Empty request object
|
||||
* @returns Response containing the Hugging Face models
|
||||
*/
|
||||
export async function refreshHuggingFaceModels(
|
||||
controller: Controller,
|
||||
_request: EmptyRequest,
|
||||
): Promise<OpenRouterCompatibleModelInfo> {
|
||||
const huggingFaceModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), "huggingface_models.json")
|
||||
|
||||
let models: Record<string, OpenRouterModelInfo> = {}
|
||||
|
||||
try {
|
||||
// Fetch models from Hugging Face API
|
||||
const response = await axios.get("https://router.huggingface.co/v1/models", {
|
||||
timeout: 10000,
|
||||
})
|
||||
|
||||
if (response.data?.data) {
|
||||
const rawModels = response.data.data
|
||||
|
||||
// Transform HF models to OpenRouter-compatible format
|
||||
for (const rawModel of rawModels) {
|
||||
const modelInfo = OpenRouterModelInfo.create({
|
||||
maxTokens: 8192, // HF doesn't provide max_tokens, use default
|
||||
contextWindow: 128_000, // FIXME: HF doesn't provide context window, use default
|
||||
supportsImages: false, // Most models don't support images
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0, // Will be set based on providers
|
||||
outputPrice: 0, // Will be set based on providers
|
||||
cacheWritesPrice: 0,
|
||||
cacheReadsPrice: 0,
|
||||
description: `Available on providers: ${rawModel.providers?.join(", ") || "unknown"}`,
|
||||
})
|
||||
|
||||
// Add model-specific configurations if we have them in our static models
|
||||
if (rawModel.id in huggingFaceModels) {
|
||||
const staticModel = huggingFaceModels[rawModel.id as keyof typeof huggingFaceModels]
|
||||
modelInfo.maxTokens = staticModel.maxTokens
|
||||
modelInfo.contextWindow = staticModel.contextWindow
|
||||
modelInfo.supportsImages = staticModel.supportsImages
|
||||
modelInfo.supportsPromptCache = staticModel.supportsPromptCache
|
||||
modelInfo.inputPrice = staticModel.inputPrice
|
||||
modelInfo.outputPrice = staticModel.outputPrice
|
||||
modelInfo.description = staticModel.description || modelInfo.description
|
||||
}
|
||||
|
||||
models[rawModel.id] = modelInfo
|
||||
}
|
||||
|
||||
// Save to cache
|
||||
await fs.writeFile(huggingFaceModelsFilePath, JSON.stringify(models, null, 2))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching Hugging Face models:", error)
|
||||
|
||||
// Try to load from cache
|
||||
try {
|
||||
if (await fileExistsAtPath(huggingFaceModelsFilePath)) {
|
||||
const cachedModels = await fs.readFile(huggingFaceModelsFilePath, "utf-8")
|
||||
const parsedModels = JSON.parse(cachedModels)
|
||||
models = parsedModels
|
||||
}
|
||||
} catch (cacheError) {
|
||||
console.error("Error loading cached Hugging Face models:", cacheError)
|
||||
}
|
||||
|
||||
// If no cache available, use static models as fallback
|
||||
if (Object.keys(models).length === 0) {
|
||||
for (const [modelId, modelInfo] of Object.entries(huggingFaceModels)) {
|
||||
models[modelId] = OpenRouterModelInfo.create({
|
||||
maxTokens: modelInfo.maxTokens,
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
supportsImages: modelInfo.supportsImages,
|
||||
supportsPromptCache: modelInfo.supportsPromptCache,
|
||||
inputPrice: modelInfo.inputPrice,
|
||||
outputPrice: modelInfo.outputPrice,
|
||||
cacheWritesPrice: (modelInfo as any).cacheWritesPrice || 0,
|
||||
cacheReadsPrice: (modelInfo as any).cacheReadsPrice || 0,
|
||||
description: modelInfo.description || "",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return OpenRouterCompatibleModelInfo.create({ models })
|
||||
}
|
||||
@@ -15,20 +15,16 @@ import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
export async function resetState(controller: Controller, request: ResetStateRequest): Promise<Empty> {
|
||||
try {
|
||||
if (request.global) {
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Resetting global state...",
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Resetting global state...",
|
||||
})
|
||||
await resetGlobalState(controller.context)
|
||||
} else {
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Resetting workspace state...",
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Resetting workspace state...",
|
||||
})
|
||||
await resetWorkspaceState(controller.context)
|
||||
}
|
||||
|
||||
@@ -37,12 +33,10 @@ export async function resetState(controller: Controller, request: ResetStateRequ
|
||||
controller.task = undefined
|
||||
}
|
||||
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "State reset",
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "State reset",
|
||||
})
|
||||
await controller.postStateToWebview()
|
||||
|
||||
await sendChatButtonClickedEvent(controller.id)
|
||||
@@ -50,12 +44,10 @@ export async function resetState(controller: Controller, request: ResetStateRequ
|
||||
return Empty.create()
|
||||
} catch (error) {
|
||||
console.error("Error resetting state:", error)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Failed to reset state: ${error instanceof Error ? error.message : String(error)}`,
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Failed to reset state: ${error instanceof Error ? error.message : String(error)}`,
|
||||
})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,12 +27,10 @@ export async function updateDefaultTerminalProfile(
|
||||
// Show information message if terminals were closed
|
||||
if (closedCount > 0) {
|
||||
const message = `Closed ${closedCount} ${closedCount === 1 ? "terminal" : "terminals"} with different profile.`
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message,
|
||||
})
|
||||
}
|
||||
|
||||
// Show warning if there are busy terminals that couldn't be closed
|
||||
@@ -40,12 +38,10 @@ export async function updateDefaultTerminalProfile(
|
||||
const message =
|
||||
`${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.`
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.WARNING,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.WARNING,
|
||||
message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ export async function deleteAllTaskHistory(controller: Controller): Promise<Dele
|
||||
},
|
||||
}),
|
||||
)
|
||||
)?.selectedOption
|
||||
).selectedOption
|
||||
|
||||
// Default VS Code Cancel button returns `undefined` - don't delete anything
|
||||
if (userChoice === undefined) {
|
||||
@@ -67,17 +67,15 @@ export async function deleteAllTaskHistory(controller: Controller): Promise<Dele
|
||||
} else {
|
||||
// No favorited tasks found - show warning and ask user what to do
|
||||
const answer = (
|
||||
await getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.WARNING,
|
||||
message: "No favorited tasks found. Would you like to delete all tasks anyway?",
|
||||
options: {
|
||||
modal: true,
|
||||
items: ["Delete All Tasks"],
|
||||
},
|
||||
}),
|
||||
)
|
||||
)?.selectedOption
|
||||
await getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.WARNING,
|
||||
message: "No favorited tasks found. Would you like to delete all tasks anyway?",
|
||||
options: {
|
||||
modal: true,
|
||||
items: ["Delete All Tasks"],
|
||||
},
|
||||
})
|
||||
).selectedOption
|
||||
|
||||
// User cancelled - don't delete anything
|
||||
if (answer === undefined) {
|
||||
@@ -105,12 +103,10 @@ export async function deleteAllTaskHistory(controller: Controller): Promise<Dele
|
||||
await fs.rm(checkpointsDirPath, { recursive: true, force: true })
|
||||
}
|
||||
} catch (error) {
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Encountered error while deleting task history, there may be some files left behind. Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Encountered error while deleting task history, there may be some files left behind. Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
})
|
||||
}
|
||||
|
||||
// Update webview
|
||||
|
||||
@@ -28,13 +28,11 @@ export const deleteTasksWithIds: TaskMethodHandler = async (
|
||||
? "Are you sure you want to delete this task? This action cannot be undone."
|
||||
: `Are you sure you want to delete these ${taskCount} tasks? This action cannot be undone.`
|
||||
|
||||
const userChoice = await getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.WARNING,
|
||||
message,
|
||||
options: { modal: true, items: ["Delete"] },
|
||||
}),
|
||||
)
|
||||
const userChoice = await getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.WARNING,
|
||||
message,
|
||||
options: { modal: true, items: ["Delete"] },
|
||||
})
|
||||
|
||||
if (userChoice === undefined) {
|
||||
return Empty.create()
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import * as vscode from "vscode"
|
||||
import type { Controller } from "../index"
|
||||
import type { EmptyRequest } from "../../../shared/proto/common"
|
||||
import { Empty } from "../../../shared/proto/common"
|
||||
import { telemetryService } from "../../../services/posthog/telemetry/TelemetryService"
|
||||
|
||||
/**
|
||||
* Opens the Cline walkthrough in VSCode
|
||||
* @param controller The controller instance
|
||||
* @param request Empty request
|
||||
* @returns Empty response
|
||||
*/
|
||||
export async function openWalkthrough(controller: Controller, request: EmptyRequest): Promise<Empty> {
|
||||
try {
|
||||
await vscode.commands.executeCommand("workbench.action.openWalkthrough", "saoudrizwan.claude-dev#ClineWalkthrough")
|
||||
telemetryService.captureButtonClick("webview_openWalkthrough")
|
||||
return Empty.create({})
|
||||
} catch (error) {
|
||||
console.error(`Failed to open walkthrough: ${error}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -78,12 +78,10 @@ export async function parseMentions(
|
||||
await urlContentFetcher.launchBrowser()
|
||||
} catch (error) {
|
||||
launchBrowserError = error
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Error fetching content for ${urlMention}: ${error.message}`,
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Error fetching content for ${urlMention}: ${error.message}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,12 +98,10 @@ export async function parseMentions(
|
||||
const markdown = await urlContentFetcher.urlToMarkdown(mention)
|
||||
result = markdown
|
||||
} catch (error) {
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Error fetching content for ${mention}: ${error.message}`,
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Error fetching content for ${mention}: ${error.message}`,
|
||||
})
|
||||
result = `Error fetching content: ${error.message}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ export type SecretKey =
|
||||
| "asksageApiKey"
|
||||
| "xaiApiKey"
|
||||
| "moonshotApiKey"
|
||||
| "huggingFaceApiKey"
|
||||
| "nebiusApiKey"
|
||||
| "sambanovaApiKey"
|
||||
| "cerebrasApiKey"
|
||||
@@ -107,6 +108,8 @@ export type GlobalStateKey =
|
||||
| "requestyModelInfo"
|
||||
| "togetherModelId"
|
||||
| "fireworksModelId"
|
||||
| "huggingFaceModelId"
|
||||
| "huggingFaceModelInfo"
|
||||
| "sapAiCoreModelId"
|
||||
// Previous mode saved configurations (per workspace)
|
||||
| "previousModeApiProvider"
|
||||
|
||||
@@ -170,6 +170,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
groqApiKey,
|
||||
moonshotApiKey,
|
||||
nebiusApiKey,
|
||||
huggingFaceApiKey,
|
||||
planActSeparateModelsSettingRaw,
|
||||
favoritedModelIds,
|
||||
globalClineRulesToggles,
|
||||
@@ -250,6 +251,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
getSecret(context, "groqApiKey") as Promise<string | undefined>,
|
||||
getSecret(context, "moonshotApiKey") as Promise<string | undefined>,
|
||||
getSecret(context, "nebiusApiKey") as Promise<string | undefined>,
|
||||
getSecret(context, "huggingFaceApiKey") as Promise<string | undefined>,
|
||||
getGlobalState(context, "planActSeparateModelsSetting") as Promise<boolean | undefined>,
|
||||
getGlobalState(context, "favoritedModelIds") as Promise<string[] | undefined>,
|
||||
getGlobalState(context, "globalClineRulesToggles") as Promise<ClineRulesToggles | undefined>,
|
||||
@@ -308,6 +310,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
previousModeAwsBedrockCustomModelBaseId,
|
||||
previousModeSapAiCoreModelId,
|
||||
sapAiCoreModelId,
|
||||
huggingFaceModelId,
|
||||
huggingFaceModelInfo,
|
||||
] = await Promise.all([
|
||||
getGlobalState(context, "chatSettings") as Promise<StoredChatSettings | undefined>,
|
||||
getGlobalState(context, "mode") as Promise<"plan" | "act" | undefined>,
|
||||
@@ -340,6 +344,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
getGlobalState(context, "previousModeAwsBedrockCustomModelBaseId") as Promise<BedrockModelId | undefined>,
|
||||
getGlobalState(context, "previousModeSapAiCoreModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "sapAiCoreModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "huggingFaceModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "huggingFaceModelInfo") as Promise<ModelInfo | undefined>,
|
||||
])
|
||||
|
||||
const processingStart = performance.now()
|
||||
@@ -462,6 +468,9 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup,
|
||||
sapAiCoreModelId,
|
||||
huggingFaceApiKey,
|
||||
huggingFaceModelId,
|
||||
huggingFaceModelInfo,
|
||||
},
|
||||
isNewUser: isNewUser ?? true,
|
||||
welcomeViewCompleted,
|
||||
@@ -581,6 +590,9 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
sapAiResourceGroup,
|
||||
sapAiCoreModelId,
|
||||
claudeCodePath,
|
||||
huggingFaceApiKey,
|
||||
huggingFaceModelId,
|
||||
huggingFaceModelInfo,
|
||||
} = apiConfiguration
|
||||
|
||||
// OPTIMIZED: Batch all global state updates into 2 operations instead of 47
|
||||
@@ -608,6 +620,8 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
groqModelId,
|
||||
groqModelInfo,
|
||||
sapAiCoreModelId,
|
||||
huggingFaceModelId,
|
||||
huggingFaceModelInfo,
|
||||
|
||||
// Global state updates (27 keys)
|
||||
awsRegion,
|
||||
@@ -672,6 +686,7 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
nebiusApiKey,
|
||||
sapAiCoreClientId,
|
||||
sapAiCoreClientSecret,
|
||||
huggingFaceApiKey,
|
||||
}
|
||||
|
||||
// Execute batched operations in parallel for maximum performance
|
||||
@@ -715,6 +730,7 @@ export async function resetGlobalState(context: vscode.ExtensionContext) {
|
||||
"groqApiKey",
|
||||
"moonshotApiKey",
|
||||
"nebiusApiKey",
|
||||
"huggingFaceApiKey",
|
||||
]
|
||||
for (const key of secretKeys) {
|
||||
await storeSecret(context, key, undefined)
|
||||
|
||||
@@ -634,7 +634,7 @@ export class ToolExecutor {
|
||||
}
|
||||
await this.diffViewProvider.update(newContent, true)
|
||||
await setTimeoutPromise(300) // wait for diff view to update
|
||||
this.diffViewProvider.scrollToFirstDiff()
|
||||
await this.diffViewProvider.scrollToFirstDiff()
|
||||
// showOmissionWarning(this.diffViewProvider.originalContent || "", newContent)
|
||||
|
||||
const completeMessage = JSON.stringify({
|
||||
|
||||
+61
-46
@@ -81,9 +81,10 @@ import { refreshWorkflowToggles } from "../context/instructions/user-instruction
|
||||
import { MessageStateHandler } from "./message-state"
|
||||
import { TaskState } from "./TaskState"
|
||||
import { ToolExecutor } from "./ToolExecutor"
|
||||
import { extractErrorDetails, formatErrorWithStatusCode, updateApiReqMsg } from "./utils"
|
||||
import { createDiffViewProvider, getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
|
||||
import { updateApiReqMsg } from "./utils"
|
||||
import { createDiffViewProvider } from "@/hosts/host-providers"
|
||||
import { ErrorService } from "@/services/error/ErrorService"
|
||||
import { ClineErrorType } from "@/services/error/ClineError"
|
||||
export const USE_EXPERIMENTAL_CLAUDE4_FEATURES = false
|
||||
|
||||
export type ToolResponse = string | Array<Anthropic.TextBlockParam | Anthropic.ImageBlockParam>
|
||||
@@ -165,8 +166,19 @@ export class Task {
|
||||
this.reinitExistingTaskFromId = reinitExistingTaskFromId
|
||||
this.cancelTask = cancelTask
|
||||
this.clineIgnoreController = new ClineIgnoreController(cwd)
|
||||
// Initialization moved to startTask/resumeTaskFromHistory
|
||||
this.terminalManager = new TerminalManager()
|
||||
|
||||
// TODO(ae) this is a hack to replace the terminal manager for standalone,
|
||||
// until we have proper host bridge support for terminal execution. The
|
||||
// standaloneTerminalManager is defined in the vscode-impls and injected
|
||||
// during compilation of the standalone manager only, so this variable only
|
||||
// exists in that case
|
||||
if ((global as any).standaloneTerminalManager) {
|
||||
console.log("[DEBUG] Using vscode-impls.js terminal manager")
|
||||
this.terminalManager = (global as any).standaloneTerminalManager
|
||||
} else {
|
||||
console.log("[DEBUG] Using built in terminal manager")
|
||||
this.terminalManager = new TerminalManager()
|
||||
}
|
||||
this.terminalManager.setShellIntegrationTimeout(shellIntegrationTimeout)
|
||||
this.terminalManager.setTerminalReuseEnabled(terminalReuseEnabled ?? true)
|
||||
this.terminalManager.setTerminalOutputLineLimit(terminalOutputLineLimit)
|
||||
@@ -1577,6 +1589,12 @@ export class Task {
|
||||
}
|
||||
}
|
||||
|
||||
private async getCurrentProviderInfo(): Promise<{ modelId: string; providerId: string }> {
|
||||
const modelId = this.api.getModel()?.id
|
||||
const providerId = (await getGlobalState(this.getContext(), "apiProvider")) as string
|
||||
return { modelId, providerId }
|
||||
}
|
||||
|
||||
async *attemptApiRequest(previousApiReqIndex: number): ApiStream {
|
||||
// Wait for MCP servers to be connected before generating system prompt
|
||||
await pWaitFor(() => this.mcpHub.isConnecting !== true, { timeout: 10_000 }).catch(() => {
|
||||
@@ -1671,16 +1689,17 @@ export class Task {
|
||||
const isAnthropic = this.api instanceof AnthropicHandler
|
||||
const isOpenRouterContextWindowError = checkIsOpenRouterContextWindowError(error) && isOpenRouter
|
||||
const isAnthropicContextWindowError = checkIsAnthropicContextWindowError(error) && isAnthropic
|
||||
const { modelId, providerId } = await this.getCurrentProviderInfo()
|
||||
const clineError = ErrorService.toClineError(error, modelId, providerId)
|
||||
|
||||
const { statusCode, message, requestId } = extractErrorDetails(error)
|
||||
|
||||
// Capture provider failure telemetry
|
||||
// Capture provider failure telemetry using clineError
|
||||
// TODO: Move into ErrorService
|
||||
telemetryService.captureProviderApiError({
|
||||
taskId: this.taskId,
|
||||
model: modelInfo.id,
|
||||
errorMessage: message,
|
||||
errorStatus: statusCode,
|
||||
requestId,
|
||||
errorMessage: clineError.message,
|
||||
errorStatus: clineError._error?.status,
|
||||
requestId: clineError._error?.request_id,
|
||||
})
|
||||
|
||||
if (isAnthropic && isAnthropicContextWindowError && !this.taskState.didAutomaticallyRetryFailedApiRequest) {
|
||||
@@ -1726,12 +1745,12 @@ export class Task {
|
||||
// If the conversation has more than 3 messages, we can truncate again. If not, then the conversation is bricked.
|
||||
// ToDo: Allow the user to change their input if this is the case.
|
||||
if (truncatedConversationHistory.length > 3) {
|
||||
error = new Error("Context window exceeded. Click retry to truncate the conversation and try again.")
|
||||
clineError.message = "Context window exceeded. Click retry to truncate the conversation and try again."
|
||||
this.taskState.didAutomaticallyRetryFailedApiRequest = false
|
||||
}
|
||||
}
|
||||
|
||||
const errorMessage = formatErrorWithStatusCode(error)
|
||||
const streamingFailedMessage = clineError.serialize()
|
||||
|
||||
// Update the 'api_req_started' message to reflect final failure before asking user to manually retry
|
||||
const lastApiReqStartedIndex = findLastIndex(
|
||||
@@ -1747,19 +1766,24 @@ export class Task {
|
||||
text: JSON.stringify({
|
||||
...currentApiReqInfo, // Spread the modified info (with retryStatus removed)
|
||||
// cancelReason: "retries_exhausted", // Indicate that automatic retries failed
|
||||
streamingFailedMessage: errorMessage,
|
||||
streamingFailedMessage,
|
||||
} satisfies ClineApiReqInfo),
|
||||
})
|
||||
// this.ask will trigger postStateToWebview, so this change should be picked up.
|
||||
}
|
||||
|
||||
const { response } = await this.ask("api_req_failed", errorMessage)
|
||||
const { response } = await this.ask("api_req_failed", streamingFailedMessage)
|
||||
|
||||
if (response !== "yesButtonClicked") {
|
||||
// this will never happen since if noButtonClicked, we will clear current task, aborting this instance
|
||||
throw new Error("API request failed")
|
||||
}
|
||||
|
||||
// Do not retry automatically again if currently unauthenticated
|
||||
if (clineError.isErrorType(ClineErrorType.Auth)) {
|
||||
return
|
||||
}
|
||||
|
||||
await this.say("api_req_retried")
|
||||
}
|
||||
// delegate generator output from the recursive call
|
||||
@@ -1895,10 +1919,10 @@ export class Task {
|
||||
}
|
||||
|
||||
// Used to know what models were used in the task if user wants to export metadata for error reporting purposes
|
||||
const currentProviderId = (await getGlobalState(this.getContext(), "apiProvider")) as string
|
||||
if (currentProviderId && this.api.getModel().id) {
|
||||
const { modelId, providerId } = await this.getCurrentProviderInfo()
|
||||
if (providerId && modelId) {
|
||||
try {
|
||||
await this.modelContextTracker.recordModelUsage(currentProviderId, this.api.getModel().id, this.chatSettings.mode)
|
||||
await this.modelContextTracker.recordModelUsage(providerId, modelId, this.chatSettings.mode)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
@@ -2076,7 +2100,7 @@ export class Task {
|
||||
content: userContent,
|
||||
})
|
||||
|
||||
telemetryService.captureConversationTurnEvent(this.taskId, currentProviderId, this.api.getModel().id, "user")
|
||||
telemetryService.captureConversationTurnEvent(this.taskId, providerId, modelId, "user")
|
||||
|
||||
// since we sent off a placeholder api_req_started message to update the webview while waiting to actually start the API request (to load potential details for example), we need to update the text of that message
|
||||
const lastApiReqIndex = findLastIndex(this.messageStateHandler.getClineMessages(), (m) => m.say === "api_req_started")
|
||||
@@ -2141,19 +2165,13 @@ export class Task {
|
||||
})
|
||||
await this.messageStateHandler.saveClineMessagesAndUpdateHistory()
|
||||
|
||||
telemetryService.captureConversationTurnEvent(
|
||||
this.taskId,
|
||||
currentProviderId,
|
||||
this.api.getModel().id,
|
||||
"assistant",
|
||||
{
|
||||
tokensIn: inputTokens,
|
||||
tokensOut: outputTokens,
|
||||
cacheWriteTokens,
|
||||
cacheReadTokens,
|
||||
totalCost,
|
||||
},
|
||||
)
|
||||
telemetryService.captureConversationTurnEvent(this.taskId, providerId, this.api.getModel().id, "assistant", {
|
||||
tokensIn: inputTokens,
|
||||
tokensOut: outputTokens,
|
||||
cacheWriteTokens,
|
||||
cacheReadTokens,
|
||||
totalCost,
|
||||
})
|
||||
|
||||
// signals to provider that it can retrieve the saved messages from disk, as abortTask can not be awaited on in nature
|
||||
this.taskState.didFinishAbortingStream = true
|
||||
@@ -2250,7 +2268,8 @@ export class Task {
|
||||
// abandoned happens when extension is no longer waiting for the cline instance to finish aborting (error is thrown here when any function in the for loop throws due to this.abort)
|
||||
if (!this.taskState.abandoned) {
|
||||
this.abortTask() // if the stream failed, there's various states the task could be in (i.e. could have streamed some tools the user may have executed), so we just resort to replicating a cancel task
|
||||
const errorMessage = formatErrorWithStatusCode(error)
|
||||
const clineError = ErrorService.toClineError(error, this.api.getModel().id)
|
||||
const errorMessage = clineError.serialize()
|
||||
|
||||
await abortStream("streaming_failed", errorMessage)
|
||||
await this.reinitExistingTaskFromId(this.taskId)
|
||||
@@ -2320,19 +2339,13 @@ export class Task {
|
||||
// need to save assistant responses to file before proceeding to tool use since user can exit at any moment and we wouldn't be able to save the assistant's response
|
||||
let didEndLoop = false
|
||||
if (assistantMessage.length > 0) {
|
||||
telemetryService.captureConversationTurnEvent(
|
||||
this.taskId,
|
||||
currentProviderId,
|
||||
this.api.getModel().id,
|
||||
"assistant",
|
||||
{
|
||||
tokensIn: inputTokens,
|
||||
tokensOut: outputTokens,
|
||||
cacheWriteTokens,
|
||||
cacheReadTokens,
|
||||
totalCost,
|
||||
},
|
||||
)
|
||||
telemetryService.captureConversationTurnEvent(this.taskId, providerId, modelId, "assistant", {
|
||||
tokensIn: inputTokens,
|
||||
tokensOut: outputTokens,
|
||||
cacheWriteTokens,
|
||||
cacheReadTokens,
|
||||
totalCost,
|
||||
})
|
||||
|
||||
await this.messageStateHandler.addToApiConversationHistory({
|
||||
role: "assistant",
|
||||
@@ -2378,6 +2391,8 @@ export class Task {
|
||||
},
|
||||
],
|
||||
})
|
||||
// Returns early to avoid retry since no assistant message was received
|
||||
return true
|
||||
}
|
||||
|
||||
return didEndLoop // will always be false for now
|
||||
|
||||
@@ -1,25 +1,9 @@
|
||||
import { showSystemNotification } from "@/integrations/notifications"
|
||||
import { ClineApiReqCancelReason, ClineApiReqInfo } from "@/shared/ExtensionMessage"
|
||||
import { serializeError } from "serialize-error"
|
||||
import { MessageStateHandler } from "./message-state"
|
||||
import { calculateApiCostAnthropic } from "@/utils/cost"
|
||||
import { ApiHandler } from "@/api"
|
||||
|
||||
export function formatErrorWithStatusCode(error: any): string {
|
||||
const { statusCode, message } = extractErrorDetails(error)
|
||||
|
||||
// Only prepend the statusCode if it's not already part of the message
|
||||
return statusCode && !message.includes(statusCode.toString()) ? `${statusCode} - ${message}` : message
|
||||
}
|
||||
|
||||
export function extractErrorDetails(error: any): { message: string; statusCode?: number; requestId?: string } {
|
||||
const statusCode = error.status || error.statusCode || (error.response && error.response?.status)
|
||||
const message = error.message ?? JSON.stringify(serializeError(error), null, 2)
|
||||
const requestId = error.request_id || error.response?.request_id || undefined
|
||||
|
||||
return { message, statusCode, requestId }
|
||||
}
|
||||
|
||||
export const showNotificationForApprovalIfAutoApprovalEnabled = (
|
||||
message: string,
|
||||
autoApprovalSettingsEnabled: boolean,
|
||||
|
||||
@@ -264,13 +264,11 @@ export abstract class WebviewProvider {
|
||||
} catch (error) {
|
||||
// Only show the error message if not in development mode.
|
||||
if (!process.env.IS_DEV) {
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message:
|
||||
"Cline: Local webview dev server is not running, HMR will not work. Please run 'npm run dev:webview' before launching the extension to enable HMR. Using bundled assets.",
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message:
|
||||
"Cline: Local webview dev server is not running, HMR will not work. Please run 'npm run dev:webview' before launching the extension to enable HMR. Using bundled assets.",
|
||||
})
|
||||
}
|
||||
|
||||
return this.getHtmlContent()
|
||||
|
||||
@@ -99,12 +99,10 @@ export function registerTaskCommands(context: vscode.ExtensionContext, controlle
|
||||
await controller.postStateToWebview()
|
||||
|
||||
const message = `Created ${tasksCount} test tasks`
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message,
|
||||
})
|
||||
},
|
||||
)
|
||||
}),
|
||||
|
||||
+31
-32
@@ -106,12 +106,10 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
const message = `Cline has been updated to v${currentVersion}`
|
||||
await vscode.commands.executeCommand("claude-dev.SidebarProvider.focus")
|
||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message,
|
||||
})
|
||||
// Record that we've shown the popup for this version.
|
||||
await context.globalState.update("clineLastPopupNotificationVersion", currentVersion)
|
||||
}
|
||||
@@ -413,12 +411,10 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
// Ensure clipboard is restored even if an error occurs
|
||||
await writeTextToClipboard(tempCopyBuffer)
|
||||
console.error("Error getting terminal contents:", error)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Failed to get terminal contents",
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Failed to get terminal contents",
|
||||
})
|
||||
}
|
||||
}),
|
||||
)
|
||||
@@ -554,12 +550,10 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
}
|
||||
const selectedText = editor.document.getText(range)
|
||||
if (!selectedText.trim()) {
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Please select some code to explain.",
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Please select some code to explain.",
|
||||
})
|
||||
return
|
||||
}
|
||||
const filePath = editor.document.uri.fsPath
|
||||
@@ -581,12 +575,10 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
}
|
||||
const selectedText = editor.document.getText(range)
|
||||
if (!selectedText.trim()) {
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Please select some code to improve.",
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Please select some code to improve.",
|
||||
})
|
||||
return
|
||||
}
|
||||
const filePath = editor.document.uri.fsPath
|
||||
@@ -651,12 +643,10 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
sendFocusChatInputEvent(clientId)
|
||||
} else {
|
||||
console.error("FocusChatInput: Could not find or activate a Cline webview to focus.")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Could not activate Cline view. Please try opening it manually from the Activity Bar.",
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Could not activate Cline view. Please try opening it manually from the Activity Bar.",
|
||||
})
|
||||
}
|
||||
telemetryService.captureButtonClick("command_focusChatInput", activeWebviewProvider?.controller.task?.taskId)
|
||||
}),
|
||||
@@ -691,9 +681,18 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
)
|
||||
|
||||
context.subscriptions.push(
|
||||
context.secrets.onDidChange((event) => {
|
||||
context.secrets.onDidChange(async (event) => {
|
||||
if (event.key === "clineAccountId") {
|
||||
AuthService.getInstance(context)?.restoreRefreshTokenAndRetrieveAuthInfo()
|
||||
// Check if the secret was removed (logout) or added/updated (login)
|
||||
const secretValue = await context.secrets.get("clineAccountId")
|
||||
const authService = AuthService.getInstance(context)
|
||||
if (secretValue) {
|
||||
// Secret was added or updated - restore auth info (login from another window)
|
||||
authService?.restoreRefreshTokenAndRetrieveAuthInfo()
|
||||
} else {
|
||||
// Secret was removed - handle logout for all windows
|
||||
authService?.handleDeauth()
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -5,6 +5,9 @@ import { DecorationController } from "@integrations/editor/DecorationController"
|
||||
import { DIFF_VIEW_URI_SCHEME, DiffViewProvider } from "@integrations/editor/DiffViewProvider"
|
||||
|
||||
export class VscodeDiffViewProvider extends DiffViewProvider {
|
||||
private fadedOverlayController?: DecorationController
|
||||
private activeLineController?: DecorationController
|
||||
|
||||
override async openDiffEditor(): Promise<void> {
|
||||
if (!this.absolutePath) {
|
||||
throw new Error("No file path set")
|
||||
@@ -81,20 +84,100 @@ export class VscodeDiffViewProvider extends DiffViewProvider {
|
||||
override async replaceText(
|
||||
content: string,
|
||||
rangeToReplace: { startLine: number; endLine: number },
|
||||
currentLine: number,
|
||||
currentLine: number | undefined,
|
||||
): Promise<void> {
|
||||
const document = this.activeDiffEditor?.document
|
||||
if (!document) {
|
||||
if (!this.activeDiffEditor || !this.activeDiffEditor.document) {
|
||||
throw new Error("User closed text editor, unable to edit file...")
|
||||
}
|
||||
// Place cursor at the beginning of the diff editor to keep it out of the way of the stream animation
|
||||
const beginningOfDocument = new vscode.Position(0, 0)
|
||||
this.activeDiffEditor.selection = new vscode.Selection(beginningOfDocument, beginningOfDocument)
|
||||
|
||||
// Replace the text in the diff editor document.
|
||||
const document = this.activeDiffEditor?.document
|
||||
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)
|
||||
if (currentLine !== undefined) {
|
||||
// Update decorations for the entire changed section
|
||||
this.activeLineController?.setActiveLine(currentLine)
|
||||
this.fadedOverlayController?.updateOverlayAfterLine(currentLine, document.lineCount)
|
||||
}
|
||||
}
|
||||
|
||||
override async scrollEditorToLine(line: number): Promise<void> {
|
||||
if (!this.activeDiffEditor) {
|
||||
return
|
||||
}
|
||||
const scrollLine = line + 4
|
||||
this.activeDiffEditor.revealRange(new vscode.Range(scrollLine, 0, scrollLine, 0), vscode.TextEditorRevealType.InCenter)
|
||||
}
|
||||
|
||||
override async scrollAnimation(startLine: number, endLine: number): Promise<void> {
|
||||
if (!this.activeDiffEditor) {
|
||||
return
|
||||
}
|
||||
const totalLines = endLine - startLine
|
||||
const numSteps = 10 // Adjust this number to control animation speed
|
||||
const stepSize = Math.max(1, Math.floor(totalLines / numSteps))
|
||||
|
||||
// Create and await the smooth scrolling animation
|
||||
for (let line = startLine; line <= endLine; line += stepSize) {
|
||||
this.activeDiffEditor.revealRange(new vscode.Range(line, 0, line, 0), vscode.TextEditorRevealType.InCenter)
|
||||
await new Promise((resolve) => setTimeout(resolve, 16)) // ~60fps
|
||||
}
|
||||
}
|
||||
|
||||
override async truncateDocument(lineNumber: number): Promise<void> {
|
||||
if (!this.activeDiffEditor) {
|
||||
return
|
||||
}
|
||||
const document = this.activeDiffEditor.document
|
||||
if (lineNumber < document.lineCount) {
|
||||
const edit = new vscode.WorkspaceEdit()
|
||||
edit.delete(document.uri, new vscode.Range(lineNumber, 0, document.lineCount, 0))
|
||||
await vscode.workspace.applyEdit(edit)
|
||||
}
|
||||
// Clear all decorations at the end (before applying final edit)
|
||||
this.fadedOverlayController?.clear()
|
||||
this.activeLineController?.clear()
|
||||
}
|
||||
|
||||
protected override async getDocumentText(): Promise<string | undefined> {
|
||||
if (!this.activeDiffEditor || !this.activeDiffEditor.document) {
|
||||
return undefined
|
||||
}
|
||||
return this.activeDiffEditor.document.getText()
|
||||
}
|
||||
|
||||
protected override async saveDocument(): Promise<void> {
|
||||
if (!this.activeDiffEditor) {
|
||||
return
|
||||
}
|
||||
if (this.activeDiffEditor.document.isDirty) {
|
||||
await this.activeDiffEditor.document.save()
|
||||
}
|
||||
}
|
||||
|
||||
protected async closeDiffView(): Promise<void> {
|
||||
// Close all the cline diff views.
|
||||
const tabs = vscode.window.tabGroups.all
|
||||
.flatMap((tg) => tg.tabs)
|
||||
.filter((tab) => tab.input instanceof vscode.TabInputTextDiff && tab.input?.original?.scheme === DIFF_VIEW_URI_SCHEME)
|
||||
for (const tab of tabs) {
|
||||
// trying to close dirty views results in save popup
|
||||
if (!tab.isDirty) {
|
||||
await vscode.window.tabGroups.close(tab)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override async resetDiffView(): Promise<void> {
|
||||
this.activeDiffEditor = undefined
|
||||
this.fadedOverlayController = undefined
|
||||
this.activeLineController = undefined
|
||||
this.preDiagnostics = []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { CloseDiffRequest, CloseDiffResponse } from "@/shared/proto/index.host"
|
||||
|
||||
export async function closeDiff(_request: CloseDiffRequest): Promise<CloseDiffResponse> {
|
||||
throw new Error("diffService is not supported. Use the VscodeDiffViewProvider.")
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { GetDocumentTextRequest, GetDocumentTextResponse } from "@/shared/proto/index.host"
|
||||
|
||||
export async function getDocumentText(_request: GetDocumentTextRequest): Promise<GetDocumentTextResponse> {
|
||||
throw new Error("diffService is not supported. Use the VscodeDiffViewProvider.")
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { OpenDiffRequest, OpenDiffResponse } from "@/shared/proto/index.host"
|
||||
|
||||
export async function openDiff(_request: OpenDiffRequest): Promise<OpenDiffResponse> {
|
||||
throw new Error("diffService.openDiff is not supported. Use the VscodeDiffViewProvider.")
|
||||
throw new Error("diffService is not supported. Use the VscodeDiffViewProvider.")
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ReplaceTextRequest, ReplaceTextResponse } from "@/shared/proto/index.host"
|
||||
|
||||
export async function replaceText(_request: ReplaceTextRequest): Promise<ReplaceTextResponse> {
|
||||
throw new Error("diffService.replaceText is not supported. Use the VscodeDiffViewProvider.")
|
||||
throw new Error("diffService is not supported. Use the VscodeDiffViewProvider.")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { SaveDocumentRequest, SaveDocumentResponse } from "@/shared/proto/index.host"
|
||||
|
||||
export async function saveDocument(_request: SaveDocumentRequest): Promise<SaveDocumentResponse> {
|
||||
throw new Error("diffService is not supported. Use the VscodeDiffViewProvider.")
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { TruncateDocumentRequest, TruncateDocumentResponse } from "@/shared/proto/index.host"
|
||||
|
||||
export async function truncateDocument(_request: TruncateDocumentRequest): Promise<TruncateDocumentResponse> {
|
||||
throw new Error("diffService is not supported. Use the VscodeDiffViewProvider.")
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { SelectedResponse, ShowMessageRequest, ShowMessageType } from "@/shared/
|
||||
|
||||
const DEFAULT_OPTIONS = { modal: false, items: [] } as const
|
||||
|
||||
export async function showMessage(request: ShowMessageRequest): Promise<SelectedResponse | undefined> {
|
||||
export async function showMessage(request: ShowMessageRequest): Promise<SelectedResponse> {
|
||||
const { message, type, options } = request
|
||||
const { modal, detail, items } = { ...DEFAULT_OPTIONS, ...options }
|
||||
const option = { modal, detail }
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import * as vscode from "vscode"
|
||||
|
||||
interface DebugSession {
|
||||
id: string
|
||||
name: string
|
||||
output: string[]
|
||||
lastRetrievedIndex: number
|
||||
}
|
||||
|
||||
export class DebugConsoleManager {
|
||||
private sessions: Map<string, DebugSession> = new Map()
|
||||
private disposables: vscode.Disposable[] = []
|
||||
|
||||
constructor() {
|
||||
// Listen for debug session start events
|
||||
this.disposables.push(
|
||||
vscode.debug.onDidStartDebugSession((session) => {
|
||||
this.sessions.set(session.id, {
|
||||
id: session.id,
|
||||
name: session.name,
|
||||
output: [],
|
||||
lastRetrievedIndex: -1,
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
// Listen for debug session end events
|
||||
this.disposables.push(
|
||||
vscode.debug.onDidTerminateDebugSession((session) => {
|
||||
this.sessions.delete(session.id)
|
||||
}),
|
||||
)
|
||||
|
||||
// Listen for debug console output
|
||||
this.disposables.push(
|
||||
vscode.debug.onDidReceiveDebugSessionCustomEvent((e: vscode.DebugSessionCustomEvent) => {
|
||||
if (e.event === "output" && e.body?.output) {
|
||||
const session = this.sessions.get(e.session.id)
|
||||
if (session) {
|
||||
session.output.push(e.body.output)
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active debug sessions
|
||||
*/
|
||||
getActiveSessions(): { id: string; name: string }[] {
|
||||
return Array.from(this.sessions.values()).map(({ id, name }) => ({ id, name }))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get any new output since the last retrieval for a specific debug session
|
||||
*/
|
||||
getUnretrievedOutput(sessionId: string): string | undefined {
|
||||
const session = this.sessions.get(sessionId)
|
||||
if (!session) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const newOutput = session.output.slice(session.lastRetrievedIndex + 1).join("")
|
||||
session.lastRetrievedIndex = session.output.length - 1
|
||||
return newOutput || undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up resources
|
||||
*/
|
||||
dispose() {
|
||||
this.disposables.forEach((d) => d.dispose())
|
||||
this.sessions.clear()
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
/*
|
||||
import * as vscode from "vscode"
|
||||
import deepEqual from "fast-deep-equal"
|
||||
|
||||
type FileDiagnostics = [vscode.Uri, vscode.Diagnostic[]][]
|
||||
|
||||
|
||||
About Diagnostics:
|
||||
The Problems tab shows diagnostics that have been reported for your project. These diagnostics are categorized into:
|
||||
Errors: Critical issues that usually prevent your code from compiling or running correctly.
|
||||
Warnings: Potential problems in the code that may not prevent it from running but could cause issues (e.g., bad practices, unused variables).
|
||||
Information: Non-critical suggestions or tips (e.g., formatting issues or notes from linters).
|
||||
The Problems tab displays diagnostics from various sources:
|
||||
1. Language Servers:
|
||||
- TypeScript: Type errors, missing imports, syntax issues
|
||||
- Python: Syntax errors, invalid type hints, undefined variables
|
||||
- JavaScript/Node.js: Parsing and execution errors
|
||||
2. Linters:
|
||||
- ESLint: Code style, best practices, potential bugs
|
||||
- Pylint: Unused imports, naming conventions
|
||||
- TSLint: Style and correctness issues in TypeScript
|
||||
3. Build Tools:
|
||||
- Webpack: Module resolution failures, build errors
|
||||
- Gulp: Build errors during task execution
|
||||
4. Custom Validators:
|
||||
- Extensions can generate custom diagnostics for specific languages or tools
|
||||
Each problem typically indicates its source (e.g., language server, linter, build tool).
|
||||
Diagnostics update in real-time as you edit code, helping identify issues quickly. For example, if you introduce a syntax error in a TypeScript file, the Problems tab will immediately display the new error.
|
||||
|
||||
Notes on diagnostics:
|
||||
- linter diagnostics are only captured for open editors
|
||||
- this works great for us since when cline edits/creates files its through vscode's textedit api's and we get those diagnostics for free
|
||||
- some tools might require you to save the file or manually refresh to clear the problem from the list.
|
||||
|
||||
System Prompt
|
||||
- You will automatically receive workspace error diagnostics in environment_details. Be mindful that this may include issues beyond the scope of your task or the user's request. Only address errors relevant to your work, and avoid fixing pre-existing or unrelated issues unless the user specifically instructs you to do so.
|
||||
- If you are unable to resolve errors provided in environment_details after two attempts, consider using ask_followup_question to ask the user for additional information, such as the latest documentation related to a problematic framework, to help you make progress on the task. If the error remains unresolved after this step, proceed with your task while disregarding the error.
|
||||
|
||||
class DiagnosticsMonitor {
|
||||
private diagnosticsChangeEmitter: vscode.EventEmitter<void> = new vscode.EventEmitter<void>()
|
||||
private disposables: vscode.Disposable[] = []
|
||||
private lastDiagnostics: FileDiagnostics = []
|
||||
|
||||
constructor() {
|
||||
this.disposables.push(
|
||||
vscode.languages.onDidChangeDiagnostics(() => {
|
||||
this.diagnosticsChangeEmitter.fire()
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
public async getCurrentDiagnostics(shouldWaitForChanges: boolean): Promise<FileDiagnostics> {
|
||||
const currentDiagnostics = this.getDiagnostics()
|
||||
if (!shouldWaitForChanges) {
|
||||
this.lastDiagnostics = currentDiagnostics
|
||||
return currentDiagnostics
|
||||
}
|
||||
|
||||
if (!deepEqual(this.lastDiagnostics, currentDiagnostics)) {
|
||||
this.lastDiagnostics = currentDiagnostics
|
||||
return currentDiagnostics
|
||||
}
|
||||
|
||||
let timeout = 300 // only way this happens is if there's no errors
|
||||
|
||||
// if diagnostics contain existing errors (since the check above didn't trigger) then it's likely cline just did something that should have fixed the error, so we'll give a longer grace period for diagnostics to catch up
|
||||
const hasErrors = currentDiagnostics.some(([_, diagnostics]) =>
|
||||
diagnostics.some((d) => d.severity === vscode.DiagnosticSeverity.Error)
|
||||
)
|
||||
if (hasErrors) {
|
||||
console.log("Existing errors detected, extending timeout", currentDiagnostics)
|
||||
timeout = 10_000
|
||||
}
|
||||
|
||||
return this.waitForUpdatedDiagnostics(timeout)
|
||||
}
|
||||
|
||||
private async waitForUpdatedDiagnostics(timeout: number): Promise<FileDiagnostics> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
cleanup()
|
||||
const finalDiagnostics = this.getDiagnostics()
|
||||
this.lastDiagnostics = finalDiagnostics
|
||||
resolve(finalDiagnostics)
|
||||
}, timeout)
|
||||
|
||||
const disposable = this.diagnosticsChangeEmitter.event(() => {
|
||||
const updatedDiagnostics = this.getDiagnostics() // I thought this would only trigger when diagnostics changed, but that's not the case.
|
||||
if (deepEqual(this.lastDiagnostics, updatedDiagnostics)) {
|
||||
// diagnostics have not changed, ignoring...
|
||||
return
|
||||
}
|
||||
cleanup()
|
||||
this.lastDiagnostics = updatedDiagnostics
|
||||
resolve(updatedDiagnostics)
|
||||
})
|
||||
|
||||
const cleanup = () => {
|
||||
clearTimeout(timer)
|
||||
disposable.dispose()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private getDiagnostics(): FileDiagnostics {
|
||||
const allDiagnostics = vscode.languages.getDiagnostics()
|
||||
return allDiagnostics
|
||||
.filter(([_, diagnostics]) => diagnostics.some((d) => d.severity === vscode.DiagnosticSeverity.Error))
|
||||
.map(([uri, diagnostics]) => [
|
||||
uri,
|
||||
diagnostics.filter((d) => d.severity === vscode.DiagnosticSeverity.Error),
|
||||
])
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.disposables.forEach((d) => d.dispose())
|
||||
this.disposables = []
|
||||
this.diagnosticsChangeEmitter.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
export default DiagnosticsMonitor
|
||||
*/
|
||||
@@ -4,13 +4,11 @@ import * as fs from "fs/promises"
|
||||
import { createDirectoriesForFile } from "@utils/fs"
|
||||
import { arePathsEqual, getCwd } from "@utils/path"
|
||||
import { formatResponse } from "@core/prompts/responses"
|
||||
import { DecorationController } from "./DecorationController"
|
||||
import * as diff from "diff"
|
||||
import { diagnosticsToProblemsString, getNewDiagnostics } from "../diagnostics"
|
||||
import { detectEncoding } from "../misc/extract-text"
|
||||
import * as iconv from "iconv-lite"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { ShowTextDocumentRequest, ShowTextDocumentOptions } from "@/shared/proto/host/window"
|
||||
|
||||
export const DIFF_VIEW_URI_SCHEME = "cline-diff"
|
||||
|
||||
@@ -27,8 +25,6 @@ export abstract class DiffViewProvider {
|
||||
private newContent?: string
|
||||
|
||||
protected activeDiffEditor?: vscode.TextEditor
|
||||
protected fadedOverlayController?: DecorationController
|
||||
protected activeLineController?: DecorationController
|
||||
protected preDiagnostics: [vscode.Uri, vscode.Diagnostic[]][] = []
|
||||
|
||||
constructor() {}
|
||||
@@ -62,17 +58,13 @@ export abstract class DiffViewProvider {
|
||||
await fs.writeFile(this.absolutePath, "")
|
||||
}
|
||||
await this.openDiffEditor()
|
||||
this.scrollEditorToLine(0) // will this crash for new files?
|
||||
await this.scrollEditorToLine(0)
|
||||
this.streamedLines = []
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a diff editor or viewer for the current file.
|
||||
*
|
||||
* This abstract method must be implemented by subclasses to create and display
|
||||
* a diff editor or viewer that shows the difference between the original and
|
||||
* modified content.
|
||||
*
|
||||
* Called automatically by the `open` method after ensuring the file exists and
|
||||
* creating any necessary directories.
|
||||
*
|
||||
@@ -80,13 +72,61 @@ export abstract class DiffViewProvider {
|
||||
*/
|
||||
protected abstract openDiffEditor(): Promise<void>
|
||||
|
||||
/**
|
||||
* Scrolls the diff editor to reveal a specific line.
|
||||
*
|
||||
* It's used during streaming updates to keep the user's view focused on the changing content.
|
||||
*
|
||||
* @param line The 0-based line number to scroll to
|
||||
*/
|
||||
protected abstract scrollEditorToLine(line: number): Promise<void>
|
||||
|
||||
/**
|
||||
* Creates a smooth scrolling animation between two lines in the diff editor.
|
||||
*
|
||||
* It's typically used when updates contain many lines, to help the user visually track the flow
|
||||
* of significant changes in the document.
|
||||
*
|
||||
* @param startLine The 0-based line number to begin the animation from
|
||||
* @param endLine The 0-based line number to animate to
|
||||
*/
|
||||
protected abstract scrollAnimation(startLine: number, endLine: number): Promise<void>
|
||||
|
||||
/**
|
||||
* Removes content from the specified line to the end of the document.
|
||||
* Called after the final update is received.
|
||||
*/
|
||||
protected abstract truncateDocument(lineNumber: number): Promise<void>
|
||||
|
||||
/**
|
||||
* Get the contents of the diff editor document.
|
||||
*
|
||||
* Returns undefined if the diff editor was closed.
|
||||
*/
|
||||
protected abstract getDocumentText(): Promise<string | undefined>
|
||||
|
||||
/**
|
||||
* Save the contents of the diff editor UI to the file.
|
||||
*/
|
||||
protected abstract saveDocument(): Promise<void>
|
||||
|
||||
/**
|
||||
* Closes the diff editor tab or window.
|
||||
*/
|
||||
protected abstract closeDiffView(): Promise<void>
|
||||
|
||||
/**
|
||||
* Cleans up the diff view resources and resets internal state.
|
||||
*/
|
||||
protected abstract resetDiffView(): Promise<void>
|
||||
|
||||
async update(
|
||||
accumulatedContent: string,
|
||||
isFinal: boolean,
|
||||
changeLocation?: { startLine: number; endLine: number; startChar: number; endChar: number },
|
||||
) {
|
||||
if (!this.relPath || !this.activeLineController || !this.fadedOverlayController) {
|
||||
throw new Error("Required values not set")
|
||||
if (!this.relPath) {
|
||||
throw new Error("Required value relPath not set")
|
||||
}
|
||||
|
||||
// --- Fix to prevent duplicate BOM ---
|
||||
@@ -104,16 +144,6 @@ export abstract class DiffViewProvider {
|
||||
}
|
||||
const diffLines = accumulatedLines.slice(this.streamedLines.length)
|
||||
|
||||
const diffEditor = this.activeDiffEditor
|
||||
const document = diffEditor?.document
|
||||
if (!diffEditor || !document) {
|
||||
throw new Error("User closed text editor, unable to edit file...")
|
||||
}
|
||||
|
||||
// Place cursor at the beginning of the diff editor to keep it out of the way of the stream animation
|
||||
const beginningOfDocument = new vscode.Position(0, 0)
|
||||
diffEditor.selection = new vscode.Selection(beginningOfDocument, beginningOfDocument)
|
||||
|
||||
// Instead of animating each line, we'll update in larger chunks
|
||||
const currentLine = this.streamedLines.length + diffLines.length - 1
|
||||
if (currentLine >= 0) {
|
||||
@@ -129,30 +159,19 @@ export abstract class DiffViewProvider {
|
||||
if (changeLocation) {
|
||||
// We have the actual location of the change, scroll to it
|
||||
const targetLine = changeLocation.startLine
|
||||
this.scrollEditorToLine(targetLine)
|
||||
await this.scrollEditorToLine(targetLine)
|
||||
} else {
|
||||
// Fallback to the old logic for non-replacement updates
|
||||
if (diffLines.length <= 5) {
|
||||
// For small changes, just jump directly to the line
|
||||
this.scrollEditorToLine(currentLine)
|
||||
await this.scrollEditorToLine(currentLine)
|
||||
} else {
|
||||
// For larger changes, create a quick scrolling animation
|
||||
const startLine = this.streamedLines.length
|
||||
const endLine = currentLine
|
||||
const totalLines = endLine - startLine
|
||||
const numSteps = 10 // Adjust this number to control animation speed
|
||||
const stepSize = Math.max(1, Math.floor(totalLines / numSteps))
|
||||
|
||||
// Create and await the smooth scrolling animation
|
||||
for (let line = startLine; line <= endLine; line += stepSize) {
|
||||
this.activeDiffEditor?.revealRange(
|
||||
new vscode.Range(line, 0, line, 0),
|
||||
vscode.TextEditorRevealType.InCenter,
|
||||
)
|
||||
await new Promise((resolve) => setTimeout(resolve, 16)) // ~60fps
|
||||
}
|
||||
await this.scrollAnimation(startLine, endLine)
|
||||
// Ensure we end at the final line
|
||||
this.scrollEditorToLine(currentLine)
|
||||
await this.scrollEditorToLine(currentLine)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,11 +180,8 @@ export abstract class DiffViewProvider {
|
||||
this.streamedLines = accumulatedLines
|
||||
if (isFinal) {
|
||||
// Handle any remaining lines if the new content is shorter than the original
|
||||
if (this.streamedLines.length < document.lineCount) {
|
||||
const edit = new vscode.WorkspaceEdit()
|
||||
edit.delete(document.uri, new vscode.Range(this.streamedLines.length, 0, document.lineCount, 0))
|
||||
await vscode.workspace.applyEdit(edit)
|
||||
}
|
||||
await this.truncateDocument(this.streamedLines.length)
|
||||
|
||||
// Add empty last line if original content had one
|
||||
const hasEmptyLastLine = this.originalContent?.endsWith("\n")
|
||||
if (hasEmptyLastLine) {
|
||||
@@ -174,9 +190,6 @@ export abstract class DiffViewProvider {
|
||||
accumulatedContent += "\n"
|
||||
}
|
||||
}
|
||||
// Clear all decorations at the end (before applying final edit)
|
||||
this.fadedOverlayController.clear()
|
||||
this.activeLineController.clear()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,7 +208,7 @@ export abstract class DiffViewProvider {
|
||||
abstract replaceText(
|
||||
content: string,
|
||||
rangeToReplace: { startLine: number; endLine: number },
|
||||
currentLine: number,
|
||||
currentLine: number | undefined,
|
||||
): Promise<void>
|
||||
|
||||
async saveChanges(): Promise<{
|
||||
@@ -204,7 +217,10 @@ export abstract class DiffViewProvider {
|
||||
autoFormattingEdits: string | undefined
|
||||
finalContent: string | undefined
|
||||
}> {
|
||||
if (!this.relPath || !this.newContent || !this.activeDiffEditor) {
|
||||
// get the contents before save operation which may do auto-formatting
|
||||
const preSaveContent = await this.getDocumentText()
|
||||
|
||||
if (!this.relPath || !this.absolutePath || !this.newContent || preSaveContent === undefined) {
|
||||
return {
|
||||
newProblemsMessage: undefined,
|
||||
userEdits: undefined,
|
||||
@@ -212,28 +228,19 @@ export abstract class DiffViewProvider {
|
||||
finalContent: undefined,
|
||||
}
|
||||
}
|
||||
const updatedDocument = this.activeDiffEditor.document
|
||||
|
||||
// get the contents before save operation which may do auto-formatting
|
||||
const preSaveContent = updatedDocument.getText()
|
||||
|
||||
if (updatedDocument.isDirty) {
|
||||
await updatedDocument.save()
|
||||
}
|
||||
|
||||
await this.saveDocument()
|
||||
// get text after save in case there is any auto-formatting done by the editor
|
||||
const postSaveContent = updatedDocument.getText()
|
||||
const postSaveContent = (await this.getDocumentText()) || ""
|
||||
|
||||
await getHostBridgeProvider().windowClient.showTextDocument(
|
||||
ShowTextDocumentRequest.create({
|
||||
path: this.absolutePath,
|
||||
options: ShowTextDocumentOptions.create({
|
||||
preview: false,
|
||||
preserveFocus: true,
|
||||
}),
|
||||
}),
|
||||
)
|
||||
await this.closeAllDiffViews()
|
||||
await getHostBridgeProvider().windowClient.showTextDocument({
|
||||
path: this.absolutePath,
|
||||
options: {
|
||||
preview: false,
|
||||
preserveFocus: true,
|
||||
},
|
||||
})
|
||||
await this.closeDiffView()
|
||||
|
||||
/*
|
||||
Getting diagnostics before and after the file edit is a better approach than
|
||||
@@ -299,12 +306,10 @@ export abstract class DiffViewProvider {
|
||||
return
|
||||
}
|
||||
const fileExists = this.editType === "modify"
|
||||
const updatedDocument = this.activeDiffEditor.document
|
||||
|
||||
if (!fileExists) {
|
||||
if (updatedDocument.isDirty) {
|
||||
await updatedDocument.save()
|
||||
}
|
||||
await this.closeAllDiffViews()
|
||||
await this.saveDocument()
|
||||
await this.closeDiffView()
|
||||
await fs.unlink(this.absolutePath)
|
||||
// Remove only the directories we created, in reverse order
|
||||
for (let i = this.createdDirs.length - 1; i >= 0; i--) {
|
||||
@@ -314,6 +319,7 @@ export abstract class DiffViewProvider {
|
||||
console.log(`File ${this.absolutePath} has been deleted.`)
|
||||
} else {
|
||||
// revert document
|
||||
const updatedDocument = this.activeDiffEditor.document
|
||||
const edit = new vscode.WorkspaceEdit()
|
||||
const fullRange = new vscode.Range(
|
||||
updatedDocument.positionAt(0),
|
||||
@@ -325,59 +331,32 @@ export abstract class DiffViewProvider {
|
||||
await updatedDocument.save()
|
||||
console.log(`File ${this.absolutePath} has been reverted to its original content.`)
|
||||
if (this.documentWasOpen) {
|
||||
await getHostBridgeProvider().windowClient.showTextDocument(
|
||||
ShowTextDocumentRequest.create({
|
||||
path: this.absolutePath,
|
||||
options: ShowTextDocumentOptions.create({
|
||||
preview: false,
|
||||
preserveFocus: true,
|
||||
}),
|
||||
}),
|
||||
)
|
||||
await getHostBridgeProvider().windowClient.showTextDocument({
|
||||
path: this.absolutePath,
|
||||
options: {
|
||||
preview: false,
|
||||
preserveFocus: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
await this.closeAllDiffViews()
|
||||
await this.closeDiffView()
|
||||
}
|
||||
|
||||
// edit is done
|
||||
await this.reset()
|
||||
}
|
||||
|
||||
private async closeAllDiffViews() {
|
||||
const tabs = vscode.window.tabGroups.all
|
||||
.flatMap((tg) => tg.tabs)
|
||||
.filter((tab) => tab.input instanceof vscode.TabInputTextDiff && tab.input?.original?.scheme === DIFF_VIEW_URI_SCHEME)
|
||||
for (const tab of tabs) {
|
||||
// trying to close dirty views results in save popup
|
||||
if (!tab.isDirty) {
|
||||
await vscode.window.tabGroups.close(tab)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private scrollEditorToLine(line: number) {
|
||||
if (this.activeDiffEditor) {
|
||||
const scrollLine = line + 4
|
||||
this.activeDiffEditor.revealRange(
|
||||
new vscode.Range(scrollLine, 0, scrollLine, 0),
|
||||
vscode.TextEditorRevealType.InCenter,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
scrollToFirstDiff() {
|
||||
if (!this.activeDiffEditor) {
|
||||
async scrollToFirstDiff() {
|
||||
if (!this.isEditing) {
|
||||
return
|
||||
}
|
||||
const currentContent = this.activeDiffEditor.document.getText()
|
||||
const currentContent = (await this.getDocumentText()) || ""
|
||||
const diffs = diff.diffLines(this.originalContent || "", currentContent)
|
||||
let lineCount = 0
|
||||
for (const part of diffs) {
|
||||
if (part.added || part.removed) {
|
||||
// Found the first diff, scroll to it
|
||||
this.activeDiffEditor.revealRange(
|
||||
new vscode.Range(lineCount, 0, lineCount, 0),
|
||||
vscode.TextEditorRevealType.InCenter,
|
||||
)
|
||||
this.scrollEditorToLine(lineCount)
|
||||
return
|
||||
}
|
||||
if (!part.removed) {
|
||||
@@ -393,10 +372,8 @@ export abstract class DiffViewProvider {
|
||||
this.originalContent = undefined
|
||||
this.createdDirs = []
|
||||
this.documentWasOpen = false
|
||||
this.activeDiffEditor = undefined
|
||||
this.fadedOverlayController = undefined
|
||||
this.activeLineController = undefined
|
||||
this.streamedLines = []
|
||||
this.preDiagnostics = []
|
||||
|
||||
await this.resetDiffView()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,12 +59,10 @@ export function extractCommitMessage(aiResponse: string): string {
|
||||
*/
|
||||
export async function copyCommitMessageToClipboard(message: string): Promise<void> {
|
||||
await writeTextToClipboard(message)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Commit message copied to clipboard",
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Commit message copied to clipboard",
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,18 +75,16 @@ export async function showCommitMessageOptions(message: string): Promise<void> {
|
||||
const editAction = "Edit Message"
|
||||
|
||||
const selectedAction = (
|
||||
await getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Commit message generated",
|
||||
options: {
|
||||
modal: false,
|
||||
detail: message,
|
||||
items: [copyAction, applyAction, editAction],
|
||||
},
|
||||
}),
|
||||
)
|
||||
)?.selectedOption
|
||||
await getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Commit message generated",
|
||||
options: {
|
||||
modal: false,
|
||||
detail: message,
|
||||
items: [copyAction, applyAction, editAction],
|
||||
},
|
||||
})
|
||||
).selectedOption
|
||||
|
||||
// Handle user dismissing the dialog (selectedAction is undefined)
|
||||
if (!selectedAction) {
|
||||
@@ -120,28 +116,22 @@ async function applyCommitMessageToGitInput(message: string): Promise<void> {
|
||||
if (api && api.repositories.length > 0) {
|
||||
const repo = api.repositories[0]
|
||||
repo.inputBox.value = message
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Commit message applied to Git input",
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Commit message applied to Git input",
|
||||
})
|
||||
} else {
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "No Git repositories found",
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "No Git repositories found",
|
||||
})
|
||||
await copyCommitMessageToClipboard(message)
|
||||
}
|
||||
} else {
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Git extension not found",
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Git extension not found",
|
||||
})
|
||||
await copyCommitMessageToClipboard(message)
|
||||
}
|
||||
}
|
||||
@@ -161,10 +151,8 @@ async function editCommitMessage(message: string): Promise<void> {
|
||||
path: document.uri.fsPath,
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Edit the commit message and copy when ready",
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Edit the commit message and copy when ready",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -48,12 +48,10 @@ export async function downloadTask(dateTs: number, conversationHistory: Anthropi
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Failed to save markdown file: ${error instanceof Error ? error.message : String(error)}`,
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Failed to save markdown file: ${error instanceof Error ? error.message : String(error)}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,18 +3,16 @@ import * as os from "os"
|
||||
import * as vscode from "vscode"
|
||||
import { arePathsEqual } from "@utils/path"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { ShowTextDocumentRequest, ShowTextDocumentOptions, ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { writeFile } from "@utils/fs"
|
||||
|
||||
export async function openImage(dataUri: string) {
|
||||
const matches = dataUri.match(/^data:image\/([a-zA-Z]+);base64,(.+)$/)
|
||||
if (!matches) {
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Invalid data URI format",
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Invalid data URI format",
|
||||
})
|
||||
return
|
||||
}
|
||||
const [, format, base64Data] = matches
|
||||
@@ -24,12 +22,10 @@ export async function openImage(dataUri: string) {
|
||||
await writeFile(tempFilePath, new Uint8Array(imageBuffer))
|
||||
await vscode.commands.executeCommand("vscode.open", vscode.Uri.file(tempFilePath))
|
||||
} catch (error) {
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Error opening image: ${error}`,
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Error opening image: ${error}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,22 +46,18 @@ export async function selectFiles(imagesAllowed: boolean): Promise<{ images: str
|
||||
const dimensions = sizeOf(uint8Array) // Get dimensions from Uint8Array
|
||||
if (dimensions.width! > 7500 || dimensions.height! > 7500) {
|
||||
console.warn(`Image dimensions exceed 7500px, skipping: ${filePath}`)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Image too large: ${path.basename(filePath)} was skipped (dimensions exceed 7500px).`,
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Image too large: ${path.basename(filePath)} was skipped (dimensions exceed 7500px).`,
|
||||
})
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error reading file or getting dimensions for ${filePath}:`, error)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Could not read dimensions for ${path.basename(filePath)}, skipping.`,
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Could not read dimensions for ${path.basename(filePath)}, skipping.`,
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -76,22 +72,18 @@ export async function selectFiles(imagesAllowed: boolean): Promise<{ images: str
|
||||
const stats = await fs.stat(filePath)
|
||||
if (stats.size > 20 * 1000 * 1024) {
|
||||
console.warn(`File too large, skipping: ${filePath}`)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `File too large: ${path.basename(filePath)} was skipped (size exceeds 20MB).`,
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `File too large: ${path.basename(filePath)} was skipped (size exceeds 20MB).`,
|
||||
})
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error checking file size for ${filePath}:`, error)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Could not check file size for ${path.basename(filePath)}, skipping.`,
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Could not check file size for ${path.basename(filePath)}, skipping.`,
|
||||
})
|
||||
return null
|
||||
}
|
||||
return { type: "file", data: filePath }
|
||||
|
||||
@@ -86,7 +86,9 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
|
||||
private async runWithShellIntegration(terminal: vscode.Terminal, command: string): Promise<void> {
|
||||
// Execute command and get stream
|
||||
const stream = await this.executeCommandWithShellIntegration(terminal, command)
|
||||
if (!stream) return
|
||||
if (!stream) {
|
||||
return
|
||||
}
|
||||
|
||||
// Initialize state for stream processing
|
||||
const streamState = this.initializeStreamState()
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import { CLINE_ACCOUNT_AUTH_ERROR_MESSAGE } from "../../shared/ClineAccount"
|
||||
import { serializeError } from "serialize-error"
|
||||
|
||||
export enum ClineErrorType {
|
||||
Auth = "auth",
|
||||
Network = "network",
|
||||
RateLimit = "rateLimit",
|
||||
Balance = "balance",
|
||||
}
|
||||
|
||||
interface ErrorDetails {
|
||||
/**
|
||||
* The HTTP status code of the error, if applicable.
|
||||
*/
|
||||
status?: number
|
||||
/**
|
||||
* The request ID associated with the error, if available.
|
||||
* This can be useful for debugging and support.
|
||||
*/
|
||||
request_id?: string
|
||||
/**
|
||||
* Specific error code provided by the API or service.
|
||||
*/
|
||||
code?: string
|
||||
/**
|
||||
* The model ID associated with the error, if applicable.
|
||||
* This is useful for identifying which model the error relates to.
|
||||
*/
|
||||
modelId?: string
|
||||
/**
|
||||
* The provider ID associated with the error, if applicable.
|
||||
* This is useful for identifying which provider the error relates to.
|
||||
*/
|
||||
providerId?: string
|
||||
/**
|
||||
* The error message associated with the error, if applicable.
|
||||
*/
|
||||
message?: string
|
||||
// Additional details that might be present in the error
|
||||
// This can include things like current balance, error messages, etc.
|
||||
details?: any
|
||||
}
|
||||
|
||||
const RATE_LIMIT_PATTERNS = [/status code 429/i, /rate limit/i, /too many requests/i, /quota exceeded/i, /resource exhausted/i]
|
||||
|
||||
export class ClineError extends Error {
|
||||
readonly title = "ClineError"
|
||||
readonly _error: ErrorDetails
|
||||
|
||||
// Error details per providers:
|
||||
// Cline: error?.error
|
||||
// Ollama: error?.cause
|
||||
// tbc
|
||||
constructor(
|
||||
raw: any,
|
||||
public readonly modelId?: string,
|
||||
public readonly providerId?: string,
|
||||
) {
|
||||
const error = serializeError(raw)
|
||||
|
||||
const message = error.message || String(error) || error?.cause?.means
|
||||
super(message)
|
||||
|
||||
// Extract status from multiple possible locations
|
||||
const status = error.status || error.statusCode || error.response?.status
|
||||
|
||||
// Construct the error details object to includes relevant information
|
||||
// And ensure it has a consistent structure
|
||||
this._error = {
|
||||
message: raw.message || message,
|
||||
status,
|
||||
request_id: error.request_id || error.response?.request_id,
|
||||
code: error.code || error?.cause?.code,
|
||||
modelId,
|
||||
providerId,
|
||||
details: error.details || error.error, // Additional details provided by the server
|
||||
...error,
|
||||
stack: undefined, // Avoid serializing stack trace to keep the error object clean
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the error to a JSON string that allows for easy transmission and storage.
|
||||
* This is useful for logging or sending error details to a webviews.
|
||||
*/
|
||||
public serialize(): string {
|
||||
return JSON.stringify({
|
||||
message: this.message,
|
||||
status: this._error.status,
|
||||
request_id: this._error.request_id,
|
||||
code: this._error.code,
|
||||
modelId: this.modelId,
|
||||
providerId: this.providerId,
|
||||
details: this._error.details,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a stringified error into a ClineError instance.
|
||||
*/
|
||||
static parse(errorStr?: string, modelId?: string): ClineError | undefined {
|
||||
if (!errorStr || typeof errorStr !== "string") {
|
||||
return undefined
|
||||
}
|
||||
return ClineError.transform(errorStr, modelId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms any object into a ClineError instance.
|
||||
* Always returns a ClineError, even if the input is not a valid error object.
|
||||
*/
|
||||
static transform(error: any, modelId?: string, providerId?: string): ClineError {
|
||||
try {
|
||||
return new ClineError(JSON.parse(error), modelId, providerId)
|
||||
} catch {
|
||||
return new ClineError(error, modelId, providerId)
|
||||
}
|
||||
}
|
||||
|
||||
public isErrorType(type: ClineErrorType): boolean {
|
||||
return ClineError.getErrorType(this) === type
|
||||
}
|
||||
|
||||
/**
|
||||
* Is known error type based on the error code, status, and details.
|
||||
* This is useful for determining how to handle the error in the UI or logic.
|
||||
*/
|
||||
static getErrorType(err: ClineError): ClineErrorType | undefined {
|
||||
const { code, status, details } = err._error
|
||||
|
||||
// Check balance error first (most specific)
|
||||
if (code === "insufficient_credits" && typeof details?.current_balance === "number") {
|
||||
return ClineErrorType.Balance
|
||||
}
|
||||
|
||||
// Check auth errors
|
||||
if (code === "ERR_BAD_REQUEST" || status === 401) {
|
||||
return ClineErrorType.Auth
|
||||
}
|
||||
|
||||
// Check for auth message (only if message exists)
|
||||
const message = err.message
|
||||
if (message?.includes(CLINE_ACCOUNT_AUTH_ERROR_MESSAGE)) {
|
||||
return ClineErrorType.Auth
|
||||
}
|
||||
|
||||
// Check rate limit patterns
|
||||
if (message) {
|
||||
const lowerMessage = message.toLowerCase()
|
||||
if (RATE_LIMIT_PATTERNS.some((pattern) => pattern.test(lowerMessage))) {
|
||||
return ClineErrorType.RateLimit
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import * as Sentry from "@sentry/browser"
|
||||
import * as vscode from "vscode"
|
||||
import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
|
||||
import { telemetryService } from "../posthog/telemetry/TelemetryService"
|
||||
import * as pkg from "../../../package.json"
|
||||
import { ClineError } from "./ClineError"
|
||||
|
||||
let telemetryLevel = vscode.workspace.getConfiguration("telemetry").get<string>("telemetryLevel", "all")
|
||||
let isTelemetryEnabled = ["all", "error"].includes(telemetryLevel)
|
||||
@@ -15,6 +16,8 @@ vscode.workspace.onDidChangeConfiguration(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const isDev = process.env.IS_DEV === "true"
|
||||
|
||||
export class ErrorService {
|
||||
private static serviceEnabled: boolean
|
||||
private static serviceLevel: string
|
||||
@@ -29,7 +32,7 @@ export class ErrorService {
|
||||
beforeSend(event) {
|
||||
// TelemetryService keeps track of whether the user has opted in to telemetry/error reporting
|
||||
const isUserManuallyOptedIn = telemetryService.isTelemetryEnabled()
|
||||
if (isUserManuallyOptedIn && ErrorService.isEnabled()) {
|
||||
if (isUserManuallyOptedIn && ErrorService.isEnabled() && !isDev) {
|
||||
return event
|
||||
}
|
||||
return null
|
||||
@@ -65,7 +68,7 @@ export class ErrorService {
|
||||
}
|
||||
}
|
||||
|
||||
static logException(error: Error): void {
|
||||
static logException(error: Error | ClineError): void {
|
||||
// Don't log if telemetry is off
|
||||
const isUserManuallyOptedIn = telemetryService.isTelemetryEnabled()
|
||||
if (!isUserManuallyOptedIn || !ErrorService.isEnabled()) {
|
||||
@@ -93,4 +96,8 @@ export class ErrorService {
|
||||
static isEnabled(): boolean {
|
||||
return ErrorService.serviceEnabled
|
||||
}
|
||||
|
||||
static toClineError(rawError: any, modelId?: string, providerId?: string): ClineError {
|
||||
return ClineError.transform(rawError, modelId, providerId)
|
||||
}
|
||||
}
|
||||
|
||||
+90
-71
@@ -35,10 +35,10 @@ import { secondsToMs } from "@utils/time"
|
||||
import { GlobalFileNames } from "@core/storage/disk"
|
||||
import { ExtensionMessage } from "@shared/ExtensionMessage"
|
||||
import { DEFAULT_REQUEST_TIMEOUT_MS } from "./constants"
|
||||
import { McpConnection, McpServerConfig } from "./types"
|
||||
import { McpConnection, McpServerConfig, Transport } from "./types"
|
||||
import { BaseConfigSchema, ServerConfigSchema, McpSettingsSchema } from "./schemas"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { ShowMessageType } from "@/shared/proto/host/window"
|
||||
export class McpHub {
|
||||
getMcpServersPath: () => Promise<string>
|
||||
private getSettingsDirectoryPath: () => Promise<string>
|
||||
@@ -109,24 +109,20 @@ export class McpHub {
|
||||
try {
|
||||
config = JSON.parse(content)
|
||||
} catch (error) {
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Invalid MCP settings format. Please ensure your settings follow the correct JSON format.",
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Invalid MCP settings format. Please ensure your settings follow the correct JSON format.",
|
||||
})
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Validate against schema
|
||||
const result = McpSettingsSchema.safeParse(config)
|
||||
if (!result.success) {
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Invalid MCP settings schema.",
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Invalid MCP settings schema.",
|
||||
})
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -158,12 +154,6 @@ export class McpHub {
|
||||
if (settings) {
|
||||
try {
|
||||
await this.updateServerConnections(settings.mcpServers)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "MCP servers updated",
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Failed to process MCP settings change:", error)
|
||||
}
|
||||
@@ -202,6 +192,23 @@ export class McpHub {
|
||||
// Remove existing connection if it exists (should never happen, the connection should be deleted beforehand)
|
||||
this.connections = this.connections.filter((conn) => conn.server.name !== name)
|
||||
|
||||
if (config.disabled) {
|
||||
console.log(`[MCP Debug] Creating disabled connection object for server "${name}"`)
|
||||
// Create a connection object for disabled server so it appears in UI
|
||||
const disabledConnection: McpConnection = {
|
||||
server: {
|
||||
name,
|
||||
config: JSON.stringify(config),
|
||||
status: "disconnected",
|
||||
disabled: true,
|
||||
},
|
||||
client: null as unknown as Client,
|
||||
transport: null as unknown as Transport,
|
||||
}
|
||||
this.connections.push(disabledConnection)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Each MCP server requires its own transport connection and has unique capabilities, configurations, and error handling. Having separate clients also allows proper scoping of resources/tools and independent server management like reconnection.
|
||||
const client = new Client(
|
||||
@@ -413,12 +420,10 @@ export class McpHub {
|
||||
console.log(`[MCP Fallback Notification] ${name}:`, JSON.stringify(notification, null, 2))
|
||||
|
||||
// Show in VS Code for visibility
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: `MCP ${name}: ${notification.method || "unknown"} - ${JSON.stringify(notification.params || {})}`,
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: `MCP ${name}: ${notification.method || "unknown"} - ${JSON.stringify(notification.params || {})}`,
|
||||
})
|
||||
}
|
||||
console.log(`[MCP Debug] Successfully set fallback notification handler for ${name}`)
|
||||
} catch (error) {
|
||||
@@ -453,6 +458,11 @@ export class McpHub {
|
||||
throw new Error(`No connection found for server: ${serverName}`)
|
||||
}
|
||||
|
||||
// Disabled servers don't have clients, so return empty tools list
|
||||
if (connection.server.disabled || !connection.client) {
|
||||
return []
|
||||
}
|
||||
|
||||
const response = await connection.client.request({ method: "tools/list" }, ListToolsResultSchema, {
|
||||
timeout: DEFAULT_REQUEST_TIMEOUT_MS,
|
||||
})
|
||||
@@ -478,9 +488,16 @@ export class McpHub {
|
||||
|
||||
private async fetchResourcesList(serverName: string): Promise<McpResource[]> {
|
||||
try {
|
||||
const response = await this.connections
|
||||
.find((conn) => conn.server.name === serverName)
|
||||
?.client.request({ method: "resources/list" }, ListResourcesResultSchema, { timeout: DEFAULT_REQUEST_TIMEOUT_MS })
|
||||
const connection = this.connections.find((conn) => conn.server.name === serverName)
|
||||
|
||||
// Disabled servers don't have clients, so return empty resources list
|
||||
if (!connection || connection.server.disabled || !connection.client) {
|
||||
return []
|
||||
}
|
||||
|
||||
const response = await connection.client.request({ method: "resources/list" }, ListResourcesResultSchema, {
|
||||
timeout: DEFAULT_REQUEST_TIMEOUT_MS,
|
||||
})
|
||||
return response?.resources || []
|
||||
} catch (error) {
|
||||
// console.error(`Failed to fetch resources for ${serverName}:`, error)
|
||||
@@ -490,11 +507,20 @@ export class McpHub {
|
||||
|
||||
private async fetchResourceTemplatesList(serverName: string): Promise<McpResourceTemplate[]> {
|
||||
try {
|
||||
const response = await this.connections
|
||||
.find((conn) => conn.server.name === serverName)
|
||||
?.client.request({ method: "resources/templates/list" }, ListResourceTemplatesResultSchema, {
|
||||
const connection = this.connections.find((conn) => conn.server.name === serverName)
|
||||
|
||||
// Disabled servers don't have clients, so return empty resource templates list
|
||||
if (!connection || connection.server.disabled || !connection.client) {
|
||||
return []
|
||||
}
|
||||
|
||||
const response = await connection.client.request(
|
||||
{ method: "resources/templates/list" },
|
||||
ListResourceTemplatesResultSchema,
|
||||
{
|
||||
timeout: DEFAULT_REQUEST_TIMEOUT_MS,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
return response?.resourceTemplates || []
|
||||
} catch (error) {
|
||||
@@ -507,8 +533,13 @@ export class McpHub {
|
||||
const connection = this.connections.find((conn) => conn.server.name === name)
|
||||
if (connection) {
|
||||
try {
|
||||
await connection.transport.close()
|
||||
await connection.client.close()
|
||||
// Only close transport and client if they exist (disabled servers don't have them)
|
||||
if (connection.transport) {
|
||||
await connection.transport.close()
|
||||
}
|
||||
if (connection.client) {
|
||||
await connection.client.close()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to close transport for ${name}:`, error)
|
||||
}
|
||||
@@ -671,12 +702,10 @@ export class McpHub {
|
||||
const connection = this.connections.find((conn) => conn.server.name === serverName)
|
||||
const config = connection?.server.config
|
||||
if (config) {
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: `Restarting ${serverName} MCP server...`,
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: `Restarting ${serverName} MCP server...`,
|
||||
})
|
||||
connection.server.status = "connecting"
|
||||
connection.server.error = ""
|
||||
await this.notifyWebviewOfServerChanges()
|
||||
@@ -685,20 +714,16 @@ export class McpHub {
|
||||
await this.deleteConnection(serverName)
|
||||
// Try to connect again using existing config
|
||||
await this.connectToServer(serverName, JSON.parse(config), "internal")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: `${serverName} MCP server connected`,
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: `${serverName} MCP server connected`,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(`Failed to restart connection for ${serverName}:`, error)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Failed to connect to ${serverName} MCP server`,
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Failed to connect to ${serverName} MCP server`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -784,12 +809,10 @@ export class McpHub {
|
||||
if (error instanceof Error) {
|
||||
console.error("Error details:", error.message, error.stack)
|
||||
}
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Failed to update server state: ${error instanceof Error ? error.message : String(error)}`,
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Failed to update server state: ${error instanceof Error ? error.message : String(error)}`,
|
||||
})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -946,12 +969,10 @@ export class McpHub {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to update autoApprove settings:", error)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Failed to update autoApprove settings",
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Failed to update autoApprove settings",
|
||||
})
|
||||
throw error // Re-throw to ensure the error is properly handled
|
||||
}
|
||||
}
|
||||
@@ -1069,12 +1090,10 @@ export class McpHub {
|
||||
if (error instanceof Error) {
|
||||
console.error("Error details:", error.message, error.stack)
|
||||
}
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Failed to update server timeout: ${error instanceof Error ? error.message : String(error)}`,
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Failed to update server timeout: ${error instanceof Error ? error.message : String(error)}`,
|
||||
})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PostHog } from "posthog-node"
|
||||
import { posthogConfig } from "@/shared/services/config/posthog-config"
|
||||
import { posthogConfig } from "../../shared/services/config/posthog-config"
|
||||
|
||||
class PostHogClientProvider {
|
||||
private static instance: PostHogClientProvider
|
||||
|
||||
@@ -7,7 +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"
|
||||
import { McpDisplayMode } from "./McpDisplayMode"
|
||||
|
||||
// webview will hold state
|
||||
export interface ExtensionMessage {
|
||||
|
||||
@@ -29,6 +29,7 @@ export type ApiProvider =
|
||||
| "cerebras"
|
||||
| "sapaicore"
|
||||
| "groq"
|
||||
| "huggingface"
|
||||
|
||||
export interface ApiHandlerOptions {
|
||||
apiModelId?: string
|
||||
@@ -92,6 +93,9 @@ export interface ApiHandlerOptions {
|
||||
qwenApiLine?: string
|
||||
moonshotApiLine?: string
|
||||
moonshotApiKey?: string
|
||||
huggingFaceApiKey?: string
|
||||
huggingFaceModelId?: string
|
||||
huggingFaceModelInfo?: ModelInfo
|
||||
nebiusApiKey?: string
|
||||
asksageApiUrl?: string
|
||||
asksageApiKey?: string
|
||||
@@ -1074,6 +1078,49 @@ export const deepSeekModels = {
|
||||
},
|
||||
} as const satisfies Record<string, ModelInfo>
|
||||
|
||||
// Hugging Face Inference Providers
|
||||
// https://huggingface.co/docs/inference-providers/en/index
|
||||
export type HuggingFaceModelId = keyof typeof huggingFaceModels
|
||||
export const huggingFaceDefaultModelId: HuggingFaceModelId = "moonshotai/Kimi-K2-Instruct"
|
||||
export const huggingFaceModels = {
|
||||
"moonshotai/Kimi-K2-Instruct": {
|
||||
maxTokens: 131_072,
|
||||
contextWindow: 131_072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "Advanced reasoning model with superior performance across coding, math, and general capabilities.",
|
||||
},
|
||||
"deepseek-ai/DeepSeek-V3-0324": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 64_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "Advanced reasoning model with superior performance across coding, math, and general capabilities.",
|
||||
},
|
||||
"deepseek-ai/DeepSeek-R1": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 64_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "DeepSeek's reasoning model with step-by-step thinking capabilities.",
|
||||
},
|
||||
"meta-llama/Llama-3.1-8B-Instruct": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 128_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "Efficient 8B parameter Llama model for general-purpose tasks.",
|
||||
},
|
||||
} as const satisfies Record<string, ModelInfo>
|
||||
|
||||
// Qwen
|
||||
// https://bailian.console.aliyun.com/
|
||||
export type MainlandQwenModelId = keyof typeof mainlandQwenModels
|
||||
|
||||
@@ -224,6 +224,8 @@ function convertApiProviderToProto(provider: string | undefined): ProtoApiProvid
|
||||
return ProtoApiProvider.LITELLM
|
||||
case "moonshot":
|
||||
return ProtoApiProvider.MOONSHOT
|
||||
case "huggingface":
|
||||
return ProtoApiProvider.HUGGINGFACE
|
||||
case "nebius":
|
||||
return ProtoApiProvider.NEBIUS
|
||||
case "fireworks":
|
||||
@@ -288,6 +290,8 @@ function convertProtoToApiProvider(provider: ProtoApiProvider): ApiProvider {
|
||||
return "litellm"
|
||||
case ProtoApiProvider.MOONSHOT:
|
||||
return "moonshot"
|
||||
case ProtoApiProvider.HUGGINGFACE:
|
||||
return "huggingface"
|
||||
case ProtoApiProvider.NEBIUS:
|
||||
return "nebius"
|
||||
case ProtoApiProvider.FIREWORKS:
|
||||
@@ -374,6 +378,9 @@ export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoA
|
||||
qwenApiLine: config.qwenApiLine,
|
||||
moonshotApiLine: config.moonshotApiLine,
|
||||
moonshotApiKey: config.moonshotApiKey,
|
||||
huggingFaceApiKey: config.huggingFaceApiKey,
|
||||
huggingFaceModelId: config.huggingFaceModelId,
|
||||
huggingFaceModelInfo: convertModelInfoToProtoOpenRouter(config.huggingFaceModelInfo),
|
||||
nebiusApiKey: config.nebiusApiKey,
|
||||
asksageApiUrl: config.asksageApiUrl,
|
||||
asksageApiKey: config.asksageApiKey,
|
||||
@@ -460,6 +467,9 @@ export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguratio
|
||||
qwenApiLine: protoConfig.qwenApiLine,
|
||||
moonshotApiLine: protoConfig.moonshotApiLine,
|
||||
moonshotApiKey: protoConfig.moonshotApiKey,
|
||||
huggingFaceApiKey: protoConfig.huggingFaceApiKey,
|
||||
huggingFaceModelId: protoConfig.huggingFaceModelId,
|
||||
huggingFaceModelInfo: convertProtoToModelInfo(protoConfig.huggingFaceModelInfo),
|
||||
nebiusApiKey: protoConfig.nebiusApiKey,
|
||||
asksageApiUrl: protoConfig.asksageApiUrl,
|
||||
asksageApiKey: protoConfig.asksageApiKey,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { DiffViewProvider } from "@/integrations/editor/DiffViewProvider"
|
||||
|
||||
export class ExternalDiffViewProvider extends DiffViewProvider {
|
||||
private activeDiffEditorId: string | undefined
|
||||
|
||||
override async openDiffEditor(): Promise<void> {
|
||||
if (!this.absolutePath) {
|
||||
return
|
||||
@@ -13,10 +14,11 @@ export class ExternalDiffViewProvider extends DiffViewProvider {
|
||||
})
|
||||
this.activeDiffEditorId = response.diffId
|
||||
}
|
||||
|
||||
override async replaceText(
|
||||
content: string,
|
||||
rangeToReplace: { startLine: number; endLine: number },
|
||||
_currentLine: number,
|
||||
_currentLine: number | undefined,
|
||||
): Promise<void> {
|
||||
await getHostBridgeProvider().diffClient.replaceText({
|
||||
diffId: this.activeDiffEditorId,
|
||||
@@ -25,4 +27,48 @@ export class ExternalDiffViewProvider extends DiffViewProvider {
|
||||
endLine: rangeToReplace.endLine,
|
||||
})
|
||||
}
|
||||
|
||||
protected override async truncateDocument(lineNumber: number): Promise<void> {
|
||||
if (!this.activeDiffEditorId) {
|
||||
return
|
||||
}
|
||||
await getHostBridgeProvider().diffClient.truncateDocument({
|
||||
diffId: this.activeDiffEditorId,
|
||||
endLine: lineNumber,
|
||||
})
|
||||
}
|
||||
|
||||
protected async saveDocument(): Promise<void> {
|
||||
if (!this.activeDiffEditorId) {
|
||||
return
|
||||
}
|
||||
await getHostBridgeProvider().diffClient.saveDocument({ diffId: this.activeDiffEditorId })
|
||||
}
|
||||
|
||||
protected override async scrollEditorToLine(line: number): Promise<void> {
|
||||
console.log(`Called ExternalDiffViewProvider.scrollEditorToLine(${line}) stub`)
|
||||
}
|
||||
|
||||
override async scrollAnimation(startLine: number, endLine: number): Promise<void> {
|
||||
console.log(`Called ExternalDiffViewProvider.scrollAnimation(${startLine}, ${endLine}) stub`)
|
||||
}
|
||||
|
||||
protected override async getDocumentText(): Promise<string | undefined> {
|
||||
if (!this.activeDiffEditorId) {
|
||||
return undefined
|
||||
}
|
||||
return (await getHostBridgeProvider().diffClient.getDocumentText({ diffId: this.activeDiffEditorId })).content
|
||||
}
|
||||
|
||||
protected override async closeDiffView(): Promise<void> {
|
||||
if (!this.activeDiffEditorId) {
|
||||
return
|
||||
}
|
||||
await getHostBridgeProvider().diffClient.closeDiff({ diffId: this.activeDiffEditorId })
|
||||
this.activeDiffEditorId = undefined
|
||||
}
|
||||
|
||||
protected override async resetDiffView(): Promise<void> {
|
||||
this.activeDiffEditorId = undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,6 @@ import * as vscode from "vscode"
|
||||
import { URI } from "vscode-uri"
|
||||
import { WebviewProvider } from "@core/webview"
|
||||
|
||||
/*
|
||||
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
||||
https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/customSidebarViewProvider.ts
|
||||
*/
|
||||
|
||||
export class ExternalWebviewProvider extends WebviewProvider {
|
||||
// This hostname cannot be changed without updating the external webview handler.
|
||||
private RESOURCE_HOSTNAME: string = "internal.resources"
|
||||
|
||||
@@ -39,210 +39,9 @@ vscode.window = {
|
||||
console.log("Stubbed showSaveDialog:", options)
|
||||
return undefined
|
||||
},
|
||||
showTextDocument: async (uri, options) => {
|
||||
console.log("Stubbed showTextDocument:", uri, options)
|
||||
|
||||
// Extract file path from URI
|
||||
let filePath = uri.path || uri.fsPath || uri
|
||||
if (typeof filePath !== "string") {
|
||||
filePath = uri.toString()
|
||||
}
|
||||
|
||||
// Remove file:// prefix if present
|
||||
if (filePath.startsWith("file://")) {
|
||||
filePath = filePath.substring(7)
|
||||
}
|
||||
|
||||
// Create a function that always reads the current file content
|
||||
const getCurrentFileContent = async () => {
|
||||
try {
|
||||
const content = await fs.promises.readFile(filePath, "utf8")
|
||||
console.log(`getCurrentFileContent: Read file ${filePath} (${content.length} chars)`)
|
||||
return content
|
||||
} catch (error) {
|
||||
console.log(`getCurrentFileContent: Could not read file ${filePath}:`, error.message)
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// Try to read the initial file content
|
||||
let fileContent = await getCurrentFileContent()
|
||||
let lineCount = fileContent.split("\n").length
|
||||
|
||||
// Check if we already have an active editor for this file path
|
||||
const existingEditor = vscode.window._documentEditors && vscode.window._documentEditors[filePath]
|
||||
if (existingEditor) {
|
||||
console.log(`showTextDocument: Updating existing editor for ${filePath}`)
|
||||
// Update the existing editor's content
|
||||
fileContent = await getCurrentFileContent()
|
||||
lineCount = fileContent.split("\n").length
|
||||
|
||||
// Update the document's getText method to return current content
|
||||
existingEditor.document.getText = (range) => {
|
||||
// Always read fresh content for getText calls
|
||||
const currentContent = require("fs").readFileSync(filePath, "utf8")
|
||||
if (!range) {
|
||||
return currentContent
|
||||
}
|
||||
// Handle range-based getText with current content
|
||||
const lines = currentContent.split("\n")
|
||||
const startLine = Math.max(0, range.start.line)
|
||||
const endLine = Math.min(lines.length - 1, range.end.line)
|
||||
|
||||
if (startLine === endLine) {
|
||||
// Single line
|
||||
const line = lines[startLine] || ""
|
||||
const startChar = Math.max(0, range.start.character)
|
||||
const endChar = Math.min(line.length, range.end.character)
|
||||
return line.substring(startChar, endChar)
|
||||
} else {
|
||||
// Multiple lines
|
||||
const result = []
|
||||
for (let i = startLine; i <= endLine; i++) {
|
||||
const line = lines[i] || ""
|
||||
if (i === startLine) {
|
||||
result.push(line.substring(range.start.character))
|
||||
} else if (i === endLine) {
|
||||
result.push(line.substring(0, range.end.character))
|
||||
} else {
|
||||
result.push(line)
|
||||
}
|
||||
}
|
||||
return result.join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Update other properties
|
||||
existingEditor.document.lineCount = lineCount
|
||||
existingEditor.document.fileName = filePath
|
||||
|
||||
// Update the active text editor reference
|
||||
vscode.window.activeTextEditor = existingEditor
|
||||
|
||||
return existingEditor
|
||||
}
|
||||
|
||||
// Create a new mock text editor that always reads current file content
|
||||
const mockEditor = {
|
||||
document: {
|
||||
uri: uri,
|
||||
fileName: filePath,
|
||||
isDirty: false,
|
||||
lineCount: lineCount,
|
||||
getText: (range) => {
|
||||
// Always read fresh content for getText calls
|
||||
try {
|
||||
const currentContent = require("fs").readFileSync(filePath, "utf8")
|
||||
console.log(`document.getText: Read fresh content (${currentContent.length} chars)`)
|
||||
if (!range) {
|
||||
return currentContent
|
||||
}
|
||||
// Handle range-based getText with current content
|
||||
const lines = currentContent.split("\n")
|
||||
const startLine = Math.max(0, range.start.line)
|
||||
const endLine = Math.min(lines.length - 1, range.end.line)
|
||||
|
||||
if (startLine === endLine) {
|
||||
// Single line
|
||||
const line = lines[startLine] || ""
|
||||
const startChar = Math.max(0, range.start.character)
|
||||
const endChar = Math.min(line.length, range.end.character)
|
||||
return line.substring(startChar, endChar)
|
||||
} else {
|
||||
// Multiple lines
|
||||
const result = []
|
||||
for (let i = startLine; i <= endLine; i++) {
|
||||
const line = lines[i] || ""
|
||||
if (i === startLine) {
|
||||
result.push(line.substring(range.start.character))
|
||||
} else if (i === endLine) {
|
||||
result.push(line.substring(0, range.end.character))
|
||||
} else {
|
||||
result.push(line)
|
||||
}
|
||||
}
|
||||
return result.join("\n")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error reading file in getText: ${error.message}`)
|
||||
return ""
|
||||
}
|
||||
},
|
||||
save: async () => {
|
||||
console.log("Called mock textDocument.save")
|
||||
return true
|
||||
},
|
||||
positionAt: (offset) => {
|
||||
try {
|
||||
const currentContent = require("fs").readFileSync(filePath, "utf8")
|
||||
const lines = currentContent.split("\n")
|
||||
let currentOffset = 0
|
||||
for (let line = 0; line < lines.length; line++) {
|
||||
const lineLength = lines[line].length + 1 // +1 for newline
|
||||
if (currentOffset + lineLength > offset) {
|
||||
return { line: line, character: offset - currentOffset }
|
||||
}
|
||||
currentOffset += lineLength
|
||||
}
|
||||
return { line: lines.length - 1, character: lines[lines.length - 1]?.length || 0 }
|
||||
} catch (error) {
|
||||
return { line: 0, character: 0 }
|
||||
}
|
||||
},
|
||||
offsetAt: (position) => {
|
||||
try {
|
||||
const currentContent = require("fs").readFileSync(filePath, "utf8")
|
||||
const lines = currentContent.split("\n")
|
||||
let offset = 0
|
||||
for (let i = 0; i < position.line && i < lines.length; i++) {
|
||||
offset += lines[i].length + 1 // +1 for newline
|
||||
}
|
||||
offset += Math.min(position.character, lines[position.line]?.length || 0)
|
||||
return offset
|
||||
} catch (error) {
|
||||
return 0
|
||||
}
|
||||
},
|
||||
},
|
||||
selection: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
|
||||
selections: [],
|
||||
visibleRanges: [{ start: { line: 0, character: 0 }, end: { line: 0, character: 0 } }],
|
||||
options: {},
|
||||
viewColumn: 1,
|
||||
edit: async (callback) => {
|
||||
console.log("Called mock textEditor.edit")
|
||||
return true
|
||||
},
|
||||
insertSnippet: async () => true,
|
||||
setDecorations: () => {},
|
||||
revealRange: () => {},
|
||||
show: () => {},
|
||||
hide: () => {},
|
||||
}
|
||||
|
||||
// Store the editor by file path for future reference
|
||||
if (!vscode.window._documentEditors) {
|
||||
vscode.window._documentEditors = {}
|
||||
}
|
||||
vscode.window._documentEditors[filePath] = mockEditor
|
||||
|
||||
// Update the active text editor
|
||||
vscode.window.activeTextEditor = mockEditor
|
||||
|
||||
// Trigger onDidChangeActiveTextEditor listeners
|
||||
if (vscode.window._activeTextEditorListeners) {
|
||||
setTimeout(() => {
|
||||
vscode.window._activeTextEditorListeners.forEach((listener) => {
|
||||
try {
|
||||
listener(mockEditor)
|
||||
} catch (error) {
|
||||
console.error("Error calling onDidChangeActiveTextEditor listener:", error)
|
||||
}
|
||||
})
|
||||
}, 10) // Small delay to simulate async behavior
|
||||
}
|
||||
|
||||
return mockEditor
|
||||
showTextDocument: async (...args) => {
|
||||
console.log("Stubbed showTextDocument:", ...args)
|
||||
return {}
|
||||
},
|
||||
createOutputChannel: (name) => {
|
||||
console.log("Stubbed createOutputChannel:", name)
|
||||
@@ -290,23 +89,10 @@ vscode.window = {
|
||||
activeTextEditor: undefined,
|
||||
visibleTextEditors: [],
|
||||
tabGroups: {
|
||||
all: [
|
||||
{
|
||||
tabs: [],
|
||||
isActive: true,
|
||||
viewColumn: 1,
|
||||
},
|
||||
],
|
||||
activeTabGroup: {
|
||||
tabs: [],
|
||||
isActive: true,
|
||||
viewColumn: 1,
|
||||
},
|
||||
close: async (tab) => {
|
||||
console.log("Stubbed tabGroups.close:", tab)
|
||||
return true
|
||||
},
|
||||
onDidChangeTabs: createStub("vscode.window.tabGroups.onDidChangeTabs"),
|
||||
all: [],
|
||||
close: async () => {},
|
||||
onDidChangeTabs: createStub("vscode.env.tabGroups.onDidChangeTabs"),
|
||||
activeTabGroup: { tabs: [] },
|
||||
},
|
||||
withProgress: async (_options, task) => {
|
||||
console.log("Stubbed withProgress")
|
||||
@@ -314,56 +100,14 @@ vscode.window = {
|
||||
},
|
||||
registerUriHandler: () => ({ dispose: () => {} }),
|
||||
registerWebviewViewProvider: () => ({ dispose: () => {} }),
|
||||
onDidChangeActiveTextEditor: (listener) => {
|
||||
console.log("Called vscode.window.onDidChangeActiveTextEditor")
|
||||
// Store the listener so we can call it when showTextDocument is called
|
||||
vscode.window._activeTextEditorListeners = vscode.window._activeTextEditorListeners || []
|
||||
vscode.window._activeTextEditorListeners.push(listener)
|
||||
return {
|
||||
dispose: () => {
|
||||
console.log("Disposed onDidChangeActiveTextEditor listener")
|
||||
const index = vscode.window._activeTextEditorListeners.indexOf(listener)
|
||||
if (index > -1) {
|
||||
vscode.window._activeTextEditorListeners.splice(index, 1)
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
onDidChangeActiveTextEditor: () => ({ dispose: () => {} }),
|
||||
createTextEditorDecorationType: () => ({ dispose: () => {} }),
|
||||
createWebviewPanel: (...args) => {
|
||||
console.log("Stubbed createWebviewPanel:", ...args)
|
||||
return {
|
||||
webview: {},
|
||||
reveal: () => {},
|
||||
dispose: () => {},
|
||||
}
|
||||
createWebviewPanel: (..._args) => {
|
||||
throw new Error("WebviewPanel is not supported in standalone app.")
|
||||
},
|
||||
onDidChangeTerminalState: (listener) => {
|
||||
console.log("Called vscode.window.onDidChangeTerminalState")
|
||||
return {
|
||||
dispose: () => {
|
||||
console.log("Disposed onDidChangeTerminalState listener")
|
||||
},
|
||||
}
|
||||
},
|
||||
onDidChangeTextEditorVisibleRanges: (listener) => {
|
||||
console.log("Called vscode.window.onDidChangeTextEditorVisibleRanges")
|
||||
return {
|
||||
dispose: () => {
|
||||
console.log("Disposed onDidChangeTextEditorVisibleRanges listener")
|
||||
},
|
||||
}
|
||||
},
|
||||
terminals: [],
|
||||
activeTerminal: null,
|
||||
}
|
||||
|
||||
// Initialize env object if it doesn't exist, then extend it
|
||||
if (!vscode.env) {
|
||||
vscode.env = {}
|
||||
}
|
||||
|
||||
Object.assign(vscode.env, {
|
||||
vscode.env = {
|
||||
uriScheme: "vscode",
|
||||
appName: "Visual Studio Code",
|
||||
appRoot: "/tmp/vscode/appRoot",
|
||||
@@ -373,26 +117,17 @@ Object.assign(vscode.env, {
|
||||
sessionId: "stub-session-id",
|
||||
shell: "/bin/bash",
|
||||
|
||||
// Add the stub functions that were missing
|
||||
clipboard: createStub("vscode.env.clipboard"),
|
||||
openExternal: createStub("vscode.env.openExternal"),
|
||||
getQueryParameter: createStub("vscode.env.getQueryParameter"),
|
||||
onDidChangeTelemetryEnabled: createStub("vscode.env.onDidChangeTelemetryEnabled"),
|
||||
isTelemetryEnabled: createStub("vscode.env.isTelemetryEnabled"),
|
||||
telemetryConfiguration: createStub("vscode.env.telemetryConfiguration"),
|
||||
onDidChangeTelemetryConfiguration: createStub("vscode.env.onDidChangeTelemetryConfiguration"),
|
||||
createTelemetryLogger: createStub("vscode.env.createTelemetryLogger"),
|
||||
})
|
||||
|
||||
// Override the openExternal function with actual implementation
|
||||
vscode.env.openExternal = async (uri) => {
|
||||
const url = typeof uri === "string" ? uri : (uri.toString?.() ?? "")
|
||||
console.log("Opening browser:", url)
|
||||
await open(url)
|
||||
return true
|
||||
}
|
||||
|
||||
// Extend Uri object with improved implementations
|
||||
Object.assign(vscode.Uri, {
|
||||
vscode.Uri = {
|
||||
parse: (uriString) => {
|
||||
const url = new URL(uriString)
|
||||
return {
|
||||
@@ -437,387 +172,16 @@ Object.assign(vscode.Uri, {
|
||||
const joined = segments.map((s) => (typeof s === "string" ? s : s.path)).join("/")
|
||||
return vscode.Uri.file("/" + joined.replace(/\/+/g, "/"))
|
||||
},
|
||||
})
|
||||
|
||||
// Extend workspace object with file system operations
|
||||
Object.assign(vscode.workspace, {
|
||||
fs: {
|
||||
readFile: async function (uri) {
|
||||
console.log(`Called vscode.workspace.fs.readFile with uri:`, uri)
|
||||
try {
|
||||
// Extract file path from URI
|
||||
let filePath = uri.path || uri.fsPath || uri
|
||||
if (typeof filePath !== "string") {
|
||||
filePath = uri.toString()
|
||||
}
|
||||
|
||||
// Remove file:// prefix if present
|
||||
if (filePath.startsWith("file://")) {
|
||||
filePath = filePath.substring(7)
|
||||
}
|
||||
|
||||
console.log(`Reading file: ${filePath}`)
|
||||
const content = await fs.promises.readFile(filePath, "utf8")
|
||||
console.log(
|
||||
`File content read (${content.length} chars):`,
|
||||
content.substring(0, 100) + (content.length > 100 ? "..." : ""),
|
||||
)
|
||||
return new Uint8Array(Buffer.from(content, "utf8"))
|
||||
} catch (error) {
|
||||
console.error(`Error reading file:`, error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
writeFile: async function (uri, content) {
|
||||
console.log(`Called vscode.workspace.fs.writeFile with uri:`, uri)
|
||||
try {
|
||||
// Extract file path from URI
|
||||
let filePath = uri.path || uri.fsPath || uri
|
||||
if (typeof filePath !== "string") {
|
||||
filePath = uri.toString()
|
||||
}
|
||||
|
||||
// Remove file:// prefix if present
|
||||
if (filePath.startsWith("file://")) {
|
||||
filePath = filePath.substring(7)
|
||||
}
|
||||
|
||||
console.log(`Writing file: ${filePath}`)
|
||||
|
||||
// Ensure directory exists
|
||||
const dir = path.dirname(filePath)
|
||||
await fs.promises.mkdir(dir, { recursive: true })
|
||||
|
||||
// Write the file
|
||||
await fs.promises.writeFile(filePath, content)
|
||||
} catch (error) {
|
||||
console.error(`Error writing file:`, error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
rootPath: process.cwd(),
|
||||
name: path.basename(process.cwd()),
|
||||
|
||||
// Add other workspace methods as stubs
|
||||
getConfiguration: () => ({
|
||||
get: () => undefined,
|
||||
update: () => Promise.resolve(),
|
||||
has: () => false,
|
||||
}),
|
||||
createFileSystemWatcher: () => ({
|
||||
onDidChange: () => ({ dispose: () => {} }),
|
||||
onDidCreate: () => ({ dispose: () => {} }),
|
||||
onDidDelete: () => ({ dispose: () => {} }),
|
||||
dispose: () => {},
|
||||
}),
|
||||
onDidChangeConfiguration: () => ({ dispose: () => {} }),
|
||||
onDidCreateFiles: createStub("vscode.workspace.onDidCreateFiles"),
|
||||
onDidDeleteFiles: createStub("vscode.workspace.onDidDeleteFiles"),
|
||||
onDidRenameFiles: createStub("vscode.workspace.onDidRenameFiles"),
|
||||
onWillCreateFiles: createStub("vscode.workspace.onWillCreateFiles"),
|
||||
onWillDeleteFiles: createStub("vscode.workspace.onWillDeleteFiles"),
|
||||
onWillRenameFiles: createStub("vscode.workspace.onWillRenameFiles"),
|
||||
textDocuments: {
|
||||
find: (predicate) => {
|
||||
console.log("Called vscode.workspace.textDocuments.find")
|
||||
// Return a mock text document that behaves like VSCode expects
|
||||
return {
|
||||
uri: { fsPath: "/tmp/mock-document" },
|
||||
fileName: "/tmp/mock-document",
|
||||
isDirty: false,
|
||||
save: async () => {
|
||||
console.log("Called mock textDocument.save")
|
||||
return true
|
||||
},
|
||||
getText: () => "",
|
||||
lineCount: 0,
|
||||
}
|
||||
},
|
||||
forEach: (callback) => {
|
||||
console.log("Called vscode.workspace.textDocuments.forEach")
|
||||
// No documents to iterate over in standalone mode
|
||||
},
|
||||
length: 0,
|
||||
[Symbol.iterator]: function* () {
|
||||
// Empty iterator for standalone mode
|
||||
},
|
||||
},
|
||||
|
||||
// Add the crucial applyEdit method
|
||||
applyEdit: async (workspaceEdit) => {
|
||||
console.log("Called vscode.workspace.applyEdit", workspaceEdit)
|
||||
|
||||
// For standalone mode, we'll simulate applying the edit by actually writing to files
|
||||
try {
|
||||
// WorkspaceEdit can contain multiple types of edits
|
||||
if (workspaceEdit._edits) {
|
||||
for (const edit of workspaceEdit._edits) {
|
||||
if (edit._type === 1) {
|
||||
// TextEdit
|
||||
const uri = edit._uri
|
||||
const edits = edit._edits
|
||||
|
||||
let filePath = uri.path || uri.fsPath
|
||||
if (filePath.startsWith("file://")) {
|
||||
filePath = filePath.substring(7)
|
||||
}
|
||||
|
||||
console.log(`Applying text edits to: ${filePath}`)
|
||||
|
||||
// Read current content if file exists
|
||||
let currentContent = ""
|
||||
try {
|
||||
currentContent = await fs.promises.readFile(filePath, "utf8")
|
||||
} catch (e) {
|
||||
// File doesn't exist, start with empty content
|
||||
console.log(`File ${filePath} doesn't exist, starting with empty content`)
|
||||
}
|
||||
|
||||
// Apply edits in reverse order (from end to beginning) to maintain positions
|
||||
const sortedEdits = edits.sort((a, b) => {
|
||||
const aStart = a.range.start.line * 1000000 + a.range.start.character
|
||||
const bStart = b.range.start.line * 1000000 + b.range.start.character
|
||||
return bStart - aStart
|
||||
})
|
||||
|
||||
let lines = currentContent.split("\n")
|
||||
|
||||
for (const edit of sortedEdits) {
|
||||
const startLine = edit.range.start.line
|
||||
const startChar = edit.range.start.character
|
||||
const endLine = edit.range.end.line
|
||||
const endChar = edit.range.end.character
|
||||
const newText = edit.newText
|
||||
|
||||
console.log(`Applying edit: ${startLine}:${startChar} - ${endLine}:${endChar} -> "${newText}"`)
|
||||
|
||||
// Handle the edit
|
||||
if (startLine === endLine) {
|
||||
// Single line edit
|
||||
const line = lines[startLine] || ""
|
||||
lines[startLine] = line.substring(0, startChar) + newText + line.substring(endChar)
|
||||
} else {
|
||||
// Multi-line edit
|
||||
const firstLine = lines[startLine] || ""
|
||||
const lastLine = lines[endLine] || ""
|
||||
const newFirstLine = firstLine.substring(0, startChar) + newText + lastLine.substring(endChar)
|
||||
|
||||
// Replace the range with the new content
|
||||
lines.splice(startLine, endLine - startLine + 1, newFirstLine)
|
||||
}
|
||||
}
|
||||
|
||||
const newContent = lines.join("\n")
|
||||
|
||||
// Ensure directory exists
|
||||
const dir = path.dirname(filePath)
|
||||
await fs.promises.mkdir(dir, { recursive: true })
|
||||
|
||||
// Write the updated content
|
||||
await fs.promises.writeFile(filePath, newContent, "utf8")
|
||||
console.log(`Successfully applied edits to: ${filePath}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error("Error applying workspace edit:", error)
|
||||
return false
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Fix CodeActionKind to have static properties instead of being a class
|
||||
vscode.CodeActionKind = {
|
||||
Empty: "",
|
||||
QuickFix: "quickfix",
|
||||
Refactor: "refactor",
|
||||
RefactorExtract: "refactor.extract",
|
||||
RefactorInline: "refactor.inline",
|
||||
RefactorRewrite: "refactor.rewrite",
|
||||
Source: "source",
|
||||
SourceOrganizeImports: "source.organizeImports",
|
||||
SourceFixAll: "source.fixAll",
|
||||
}
|
||||
|
||||
// Add missing commands implementation
|
||||
if (!vscode.commands) {
|
||||
vscode.commands = {}
|
||||
vscode.env.openExternal = async (uri) => {
|
||||
const url = typeof uri === "string" ? uri : (uri.toString?.() ?? "")
|
||||
console.log("Opening browser:", url)
|
||||
await open(url)
|
||||
return true
|
||||
}
|
||||
|
||||
Object.assign(vscode.commands, {
|
||||
executeCommand: async (command, ...args) => {
|
||||
console.log(`Called vscode.commands.executeCommand: ${command}`, args)
|
||||
|
||||
// Handle the vscode.diff command specifically
|
||||
if (command === "vscode.diff") {
|
||||
const [originalUri, modifiedUri, title, options] = args
|
||||
console.log("Opening diff view:", { originalUri, modifiedUri, title })
|
||||
|
||||
// For standalone mode, just open the modified file directly
|
||||
// since we can't show a proper diff view
|
||||
const editor = await vscode.window.showTextDocument(modifiedUri, {
|
||||
preserveFocus: options?.preserveFocus || false,
|
||||
preview: false,
|
||||
})
|
||||
|
||||
// Ensure the onDidChangeActiveTextEditor event fires with a slight delay
|
||||
// This is crucial for DiffViewProvider.openDiffEditor() to work properly
|
||||
setTimeout(() => {
|
||||
if (vscode.window._activeTextEditorListeners) {
|
||||
vscode.window._activeTextEditorListeners.forEach((listener) => {
|
||||
try {
|
||||
listener(editor)
|
||||
} catch (error) {
|
||||
console.error("Error calling onDidChangeActiveTextEditor listener in vscode.diff:", error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}, 50) // Slightly longer delay to ensure proper event ordering
|
||||
|
||||
return editor
|
||||
}
|
||||
|
||||
// For other commands, just return a resolved promise
|
||||
return Promise.resolve()
|
||||
},
|
||||
registerCommand: (command, callback) => {
|
||||
console.log(`Registered command: ${command}`)
|
||||
return { dispose: () => {} }
|
||||
},
|
||||
getCommands: async () => {
|
||||
return []
|
||||
},
|
||||
})
|
||||
|
||||
// Add missing TabInput classes
|
||||
vscode.TabInputText = class TabInputText {
|
||||
constructor(uri) {
|
||||
this.uri = uri
|
||||
}
|
||||
}
|
||||
|
||||
vscode.TabInputTextDiff = class TabInputTextDiff {
|
||||
constructor(original, modified) {
|
||||
this.original = original
|
||||
this.modified = modified
|
||||
}
|
||||
}
|
||||
|
||||
// Add missing WorkspaceEdit and related classes
|
||||
vscode.WorkspaceEdit = class WorkspaceEdit {
|
||||
constructor() {
|
||||
this._edits = []
|
||||
}
|
||||
|
||||
replace(uri, range, newText) {
|
||||
console.log("WorkspaceEdit.replace:", uri, range, newText)
|
||||
this._edits.push({
|
||||
_type: 1, // TextEdit
|
||||
_uri: uri,
|
||||
_edits: [
|
||||
{
|
||||
range: range,
|
||||
newText: newText,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
insert(uri, position, newText) {
|
||||
console.log("WorkspaceEdit.insert:", uri, position, newText)
|
||||
this.replace(uri, new vscode.Range(position, position), newText)
|
||||
}
|
||||
|
||||
delete(uri, range) {
|
||||
console.log("WorkspaceEdit.delete:", uri, range)
|
||||
this.replace(uri, range, "")
|
||||
}
|
||||
}
|
||||
|
||||
vscode.Range = class Range {
|
||||
constructor(startLine, startCharacter, endLine, endCharacter) {
|
||||
if (typeof startLine === "object") {
|
||||
// Called with Position objects
|
||||
this.start = startLine
|
||||
this.end = startCharacter
|
||||
} else {
|
||||
// Called with line/character numbers
|
||||
this.start = new vscode.Position(startLine, startCharacter)
|
||||
this.end = new vscode.Position(endLine, endCharacter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vscode.Position = class Position {
|
||||
constructor(line, character) {
|
||||
this.line = line
|
||||
this.character = character
|
||||
}
|
||||
}
|
||||
|
||||
vscode.Selection = class Selection extends vscode.Range {
|
||||
constructor(anchorLine, anchorCharacter, activeLine, activeCharacter) {
|
||||
if (typeof anchorLine === "object") {
|
||||
// Called with Position objects
|
||||
super(anchorLine, anchorCharacter)
|
||||
this.anchor = anchorLine
|
||||
this.active = anchorCharacter
|
||||
} else {
|
||||
// Called with line/character numbers
|
||||
super(anchorLine, anchorCharacter, activeLine, activeCharacter)
|
||||
this.anchor = new vscode.Position(anchorLine, anchorCharacter)
|
||||
this.active = new vscode.Position(activeLine, activeCharacter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add TextEditorRevealType enum
|
||||
vscode.TextEditorRevealType = {
|
||||
Default: 0,
|
||||
InCenter: 1,
|
||||
InCenterIfOutsideViewport: 2,
|
||||
AtTop: 3,
|
||||
}
|
||||
|
||||
// Add missing languages API
|
||||
if (!vscode.languages) {
|
||||
vscode.languages = {}
|
||||
}
|
||||
|
||||
Object.assign(vscode.languages, {
|
||||
getDiagnostics: (uri) => {
|
||||
console.log("Called vscode.languages.getDiagnostics")
|
||||
// Return empty diagnostics for standalone mode
|
||||
if (uri) {
|
||||
return []
|
||||
} else {
|
||||
// Return all diagnostics as empty array
|
||||
return []
|
||||
}
|
||||
},
|
||||
registerCodeActionsProvider: () => ({ dispose: () => {} }),
|
||||
createDiagnosticCollection: () => ({
|
||||
set: () => {},
|
||||
delete: () => {},
|
||||
clear: () => {},
|
||||
dispose: () => {},
|
||||
}),
|
||||
})
|
||||
|
||||
// Export the terminal manager globally for Cline core to use
|
||||
global.standaloneTerminalManager = globalTerminalManager
|
||||
|
||||
// Override the TerminalManager to use our standalone implementation
|
||||
if (typeof global !== "undefined") {
|
||||
// Replace the TerminalManager class with our standalone implementation
|
||||
global.StandaloneTerminalManagerClass = require("./enhanced-terminal").StandaloneTerminalManager
|
||||
}
|
||||
|
||||
module.exports = vscode
|
||||
|
||||
console.log("Finished loading stub impls...")
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { VSCodeBadge, VSCodeButton, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
|
||||
import { VSCodeBadge, 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 { useSize } from "react-use"
|
||||
|
||||
import CreditLimitError from "@/components/chat/CreditLimitError"
|
||||
import { OptionsButtons } from "@/components/chat/OptionsButtons"
|
||||
import TaskFeedbackButtons from "@/components/chat/TaskFeedbackButtons"
|
||||
import { CheckmarkControl } from "@/components/common/CheckmarkControl"
|
||||
@@ -36,8 +35,8 @@ import NewTaskPreview from "./NewTaskPreview"
|
||||
import ReportBugPreview from "./ReportBugPreview"
|
||||
import UserMessage from "./UserMessage"
|
||||
import QuoteButton from "./QuoteButton"
|
||||
import { useClineAuth } from "@/context/ClineAuthContext"
|
||||
import { CLINE_ACCOUNT_AUTH_ERROR_MESSAGE } from "@shared/ClineAccount"
|
||||
import ErrorRow from "./ErrorRow"
|
||||
import { ErrorBlockTitle } from "./ErrorBlockTitle"
|
||||
|
||||
const normalColor = "var(--vscode-foreground)"
|
||||
const errorColor = "var(--vscode-errorForeground)"
|
||||
@@ -104,42 +103,6 @@ const Markdown = memo(({ markdown }: { markdown?: string }) => {
|
||||
)
|
||||
})
|
||||
|
||||
const RetryMessage = memo(
|
||||
({ seconds, attempt, retryOperations }: { retryOperations: number; attempt: number; seconds?: number }) => {
|
||||
const [remainingSeconds, setRemainingSeconds] = useState(seconds || 0)
|
||||
|
||||
useEffect(() => {
|
||||
if (seconds && seconds > 0) {
|
||||
setRemainingSeconds(seconds)
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setRemainingSeconds((prev) => {
|
||||
if (prev <= 1) {
|
||||
clearInterval(interval)
|
||||
return 0
|
||||
}
|
||||
return prev - 1
|
||||
})
|
||||
}, 1000)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}
|
||||
}, [seconds])
|
||||
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
color: normalColor,
|
||||
fontWeight: "bold",
|
||||
}}>
|
||||
{`API Request (Retrying failed attempt ${attempt}/${retryOperations}`}
|
||||
{remainingSeconds > 0 && ` in ${remainingSeconds} seconds`}
|
||||
)...
|
||||
</span>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
const ChatRow = memo(
|
||||
(props: ChatRowProps) => {
|
||||
const { isLast, onHeightChange, message, lastModifiedMessage, inputValue } = props
|
||||
@@ -186,7 +149,6 @@ export const ChatRowContent = memo(
|
||||
sendMessageFromChatRow,
|
||||
onSetQuote,
|
||||
}: ChatRowContentProps) => {
|
||||
const { handleSignIn, clineUser } = useClineAuth()
|
||||
const { mcpServers, mcpMarketplaceCatalog, onRelinquishControl, apiConfiguration } = useExtensionState()
|
||||
const [seeNewChangesDisabled, setSeeNewChangesDisabled] = useState(false)
|
||||
const [quoteButtonState, setQuoteButtonState] = useState<QuoteButtonState>({
|
||||
@@ -201,7 +163,7 @@ export const ChatRowContent = memo(
|
||||
const info: ClineApiReqInfo = JSON.parse(message.text)
|
||||
return [info.cost, info.cancelReason, info.streamingFailedMessage, info.retryStatus]
|
||||
}
|
||||
return [undefined, undefined, undefined, undefined]
|
||||
return [undefined, undefined, undefined, undefined, undefined]
|
||||
}, [message.text, message.say])
|
||||
|
||||
// when resuming task last won't be api_req_failed but a resume_task message so api_req_started will show loading spinner. that's why we just remove the last api_req_started that failed without streaming anything
|
||||
@@ -381,73 +343,12 @@ export const ChatRowContent = memo(
|
||||
<span style={{ color: successColor, fontWeight: "bold" }}>Task Completed</span>,
|
||||
]
|
||||
case "api_req_started":
|
||||
const getIconSpan = (iconName: string, color: string) => (
|
||||
<div
|
||||
style={{
|
||||
width: 16,
|
||||
height: 16,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}>
|
||||
<span
|
||||
className={`codicon codicon-${iconName}`}
|
||||
style={{
|
||||
color,
|
||||
fontSize: 16,
|
||||
marginBottom: "-1.5px",
|
||||
}}></span>
|
||||
</div>
|
||||
)
|
||||
return [
|
||||
apiReqCancelReason != null ? (
|
||||
apiReqCancelReason === "user_cancelled" ? (
|
||||
getIconSpan("error", cancelledColor)
|
||||
) : (
|
||||
getIconSpan("error", errorColor)
|
||||
)
|
||||
) : cost != null ? (
|
||||
getIconSpan("check", successColor)
|
||||
) : apiRequestFailedMessage ? (
|
||||
getIconSpan("error", errorColor)
|
||||
) : (
|
||||
<ProgressIndicator />
|
||||
),
|
||||
(() => {
|
||||
if (apiReqCancelReason != null) {
|
||||
return apiReqCancelReason === "user_cancelled" ? (
|
||||
<span style={{ color: normalColor, fontWeight: "bold" }}>API Request Cancelled</span>
|
||||
) : (
|
||||
<span style={{ color: errorColor, fontWeight: "bold" }}>API Streaming Failed</span>
|
||||
)
|
||||
}
|
||||
|
||||
if (cost != null) {
|
||||
return <span style={{ color: normalColor, fontWeight: "bold" }}>API Request</span>
|
||||
}
|
||||
|
||||
if (apiRequestFailedMessage) {
|
||||
const errorData = parseErrorText(apiRequestFailedMessage)
|
||||
if (errorData?.code === "insufficient_credits") {
|
||||
return <span style={{ color: errorColor, fontWeight: "bold" }}>Credit Limit Reached</span>
|
||||
}
|
||||
return <span style={{ color: errorColor, fontWeight: "bold" }}>API Request Failed</span>
|
||||
}
|
||||
// New: Check for retryStatus to modify the title
|
||||
if (retryStatus && cost == null && !apiReqCancelReason) {
|
||||
const retryOperations = retryStatus.maxAttempts > 0 ? retryStatus.maxAttempts - 1 : 0
|
||||
return (
|
||||
<RetryMessage
|
||||
seconds={retryStatus.delaySec}
|
||||
attempt={retryStatus.attempt}
|
||||
retryOperations={retryOperations}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return <span style={{ color: normalColor, fontWeight: "bold" }}>API Request...</span>
|
||||
})(),
|
||||
]
|
||||
return ErrorBlockTitle({
|
||||
cost,
|
||||
apiReqCancelReason,
|
||||
apiRequestFailedMessage,
|
||||
retryStatus,
|
||||
})
|
||||
case "followup":
|
||||
return [
|
||||
<span
|
||||
@@ -946,92 +847,12 @@ export const ChatRowContent = memo(
|
||||
<span className={`codicon codicon-chevron-${isExpanded ? "up" : "down"}`}></span>
|
||||
</div>
|
||||
{((cost == null && apiRequestFailedMessage) || apiReqStreamingFailedMessage) && (
|
||||
<>
|
||||
{(() => {
|
||||
// Try to parse the error message as JSON for credit limit error
|
||||
const errorData = parseErrorText(
|
||||
apiRequestFailedMessage || apiReqStreamingFailedMessage,
|
||||
)
|
||||
if (errorData) {
|
||||
if (
|
||||
errorData.code === "insufficient_credits" &&
|
||||
typeof errorData.current_balance === "number"
|
||||
) {
|
||||
return (
|
||||
<CreditLimitError
|
||||
currentBalance={errorData.current_balance}
|
||||
totalSpent={errorData.total_spent}
|
||||
totalPromotions={errorData.total_promotions}
|
||||
message={errorData.message}
|
||||
buyCreditsUrl={errorData.buy_credits_url}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Check for rate limit errors (status code 429)
|
||||
const isRateLimitError =
|
||||
apiRequestFailedMessage?.includes("status code 429") ||
|
||||
apiRequestFailedMessage?.toLowerCase().includes("rate limit") ||
|
||||
apiRequestFailedMessage?.toLowerCase().includes("too many requests") ||
|
||||
apiRequestFailedMessage?.toLowerCase().includes("quota exceeded") ||
|
||||
apiRequestFailedMessage?.toLowerCase().includes("resource exhausted")
|
||||
|
||||
if (isRateLimitError) {
|
||||
return (
|
||||
<p
|
||||
style={{
|
||||
...pStyle,
|
||||
color: "var(--vscode-errorForeground)",
|
||||
}}>
|
||||
{apiRequestFailedMessage || apiReqStreamingFailedMessage}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
// Default error display
|
||||
return (
|
||||
<p
|
||||
style={{
|
||||
...pStyle,
|
||||
color: "var(--vscode-errorForeground)",
|
||||
}}>
|
||||
{apiRequestFailedMessage || apiReqStreamingFailedMessage}
|
||||
{apiRequestFailedMessage?.toLowerCase().includes("powershell") && (
|
||||
<>
|
||||
<br />
|
||||
<br />
|
||||
It seems like you're having Windows PowerShell issues, please see this{" "}
|
||||
<a
|
||||
href="https://github.com/cline/cline/wiki/TroubleShooting-%E2%80%90-%22PowerShell-is-not-recognized-as-an-internal-or-external-command%22"
|
||||
style={{
|
||||
color: "inherit",
|
||||
textDecoration: "underline",
|
||||
}}>
|
||||
troubleshooting guide
|
||||
</a>
|
||||
.
|
||||
</>
|
||||
)}
|
||||
{apiRequestFailedMessage?.includes(CLINE_ACCOUNT_AUTH_ERROR_MESSAGE) && (
|
||||
<>
|
||||
<br />
|
||||
<br />
|
||||
{clineUser ? (
|
||||
<span style={{ color: "var(--vscode-descriptionForeground)" }}>
|
||||
(Click "Retry" below)
|
||||
</span>
|
||||
) : (
|
||||
<VSCodeButton onClick={handleSignIn} className="w-full mb-4">
|
||||
Sign in to Cline
|
||||
</VSCodeButton>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
)
|
||||
})()}
|
||||
</>
|
||||
<ErrorRow
|
||||
message={message}
|
||||
errorType="error"
|
||||
apiRequestFailedMessage={apiRequestFailedMessage}
|
||||
apiReqStreamingFailedMessage={apiReqStreamingFailedMessage}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isExpanded && (
|
||||
@@ -1183,97 +1004,11 @@ export const ChatRowContent = memo(
|
||||
</div>
|
||||
)
|
||||
case "error":
|
||||
return (
|
||||
<>
|
||||
{title && (
|
||||
<div style={headerStyle}>
|
||||
{icon}
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
<p
|
||||
style={{
|
||||
...pStyle,
|
||||
color: "var(--vscode-errorForeground)",
|
||||
}}>
|
||||
{message.text}
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
return <ErrorRow message={message} errorType="error" />
|
||||
case "diff_error":
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
backgroundColor: "var(--vscode-textBlockQuote-background)",
|
||||
padding: 8,
|
||||
borderRadius: 3,
|
||||
fontSize: 12,
|
||||
color: "var(--vscode-foreground)",
|
||||
opacity: 0.8,
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
marginBottom: 4,
|
||||
}}>
|
||||
<i
|
||||
className="codicon codicon-warning"
|
||||
style={{
|
||||
marginRight: 8,
|
||||
fontSize: 14,
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}></i>
|
||||
<span style={{ fontWeight: 500 }}>Diff Edit Mismatch</span>
|
||||
</div>
|
||||
<div>The model used search patterns that don't match anything in the file. Retrying...</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
return <ErrorRow message={message} errorType="diff_error" />
|
||||
case "clineignore_error":
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
backgroundColor: "rgba(255, 191, 0, 0.1)",
|
||||
padding: 8,
|
||||
borderRadius: 3,
|
||||
fontSize: 12,
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
marginBottom: 4,
|
||||
}}>
|
||||
<i
|
||||
className="codicon codicon-error"
|
||||
style={{
|
||||
marginRight: 8,
|
||||
fontSize: 18,
|
||||
color: "#FFA500",
|
||||
}}></i>
|
||||
<span
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
color: "#FFA500",
|
||||
}}>
|
||||
Access Denied
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
Cline tried to access <code>{message.text}</code> which is blocked by the{" "}
|
||||
<code>.clineignore</code>
|
||||
file.
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
return <ErrorRow message={message} errorType="clineignore_error" />
|
||||
case "checkpoint_created":
|
||||
return (
|
||||
<>
|
||||
@@ -1433,37 +1168,9 @@ export const ChatRowContent = memo(
|
||||
case "ask":
|
||||
switch (message.ask) {
|
||||
case "mistake_limit_reached":
|
||||
return (
|
||||
<>
|
||||
<div style={headerStyle}>
|
||||
{icon}
|
||||
{title}
|
||||
</div>
|
||||
<p
|
||||
style={{
|
||||
...pStyle,
|
||||
color: "var(--vscode-errorForeground)",
|
||||
}}>
|
||||
{message.text}
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
return <ErrorRow message={message} errorType="mistake_limit_reached" />
|
||||
case "auto_approval_max_req_reached":
|
||||
return (
|
||||
<>
|
||||
<div style={headerStyle}>
|
||||
{icon}
|
||||
{title}
|
||||
</div>
|
||||
<p
|
||||
style={{
|
||||
...pStyle,
|
||||
color: "var(--vscode-errorForeground)",
|
||||
}}>
|
||||
{message.text}
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
return <ErrorRow message={message} errorType="auto_approval_max_req_reached" />
|
||||
case "completion_result":
|
||||
if (message.text) {
|
||||
const hasChanges = message.text.endsWith(COMPLETION_RESULT_CHANGES_FLAG) ?? false
|
||||
@@ -1686,20 +1393,3 @@ export const ChatRowContent = memo(
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
function parseErrorText(text: string | undefined) {
|
||||
if (!text) {
|
||||
return undefined
|
||||
}
|
||||
try {
|
||||
const startIndex = text.indexOf("{")
|
||||
const endIndex = text.lastIndexOf("}")
|
||||
if (startIndex !== -1 && endIndex !== -1) {
|
||||
const jsonStr = text.substring(startIndex, endIndex + 1)
|
||||
const errorObject = JSON.parse(jsonStr)
|
||||
return errorObject
|
||||
}
|
||||
} catch (e) {
|
||||
// Not JSON or missing required fields
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import React from "react"
|
||||
import { ClineError, ClineErrorType } from "../../../../src/services/error/ClineError"
|
||||
import { ProgressIndicator } from "./ChatRow"
|
||||
|
||||
const RetryMessage = React.memo(
|
||||
({ seconds, attempt, retryOperations }: { retryOperations: number; attempt: number; seconds?: number }) => {
|
||||
const [remainingSeconds, setRemainingSeconds] = React.useState(seconds || 0)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (seconds && seconds > 0) {
|
||||
setRemainingSeconds(seconds)
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setRemainingSeconds((prev) => {
|
||||
if (prev <= 1) {
|
||||
clearInterval(interval)
|
||||
return 0
|
||||
}
|
||||
return prev - 1
|
||||
})
|
||||
}, 1000)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}
|
||||
}, [seconds])
|
||||
|
||||
return (
|
||||
<span className="font-bold text-[var(--vscode-foreground)]">
|
||||
{`API Request (Retrying failed attempt ${attempt}/${retryOperations}`}
|
||||
{remainingSeconds > 0 && ` in ${remainingSeconds} seconds`}
|
||||
)...
|
||||
</span>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
interface ErrorBlockTitleProps {
|
||||
cost?: number
|
||||
apiReqCancelReason?: string
|
||||
apiRequestFailedMessage?: string
|
||||
retryStatus?: {
|
||||
attempt: number
|
||||
maxAttempts: number
|
||||
delaySec?: number
|
||||
errorSnippet?: string
|
||||
}
|
||||
}
|
||||
|
||||
export const ErrorBlockTitle = ({
|
||||
cost,
|
||||
apiReqCancelReason,
|
||||
apiRequestFailedMessage,
|
||||
retryStatus,
|
||||
}: ErrorBlockTitleProps): [React.ReactElement, React.ReactElement] => {
|
||||
const getIconSpan = (iconName: string, colorClass: string) => (
|
||||
<div className="w-4 h-4 flex items-center justify-center">
|
||||
<span className={`codicon codicon-${iconName} text-base -mb-0.5 ${colorClass}`}></span>
|
||||
</div>
|
||||
)
|
||||
|
||||
const icon =
|
||||
apiReqCancelReason != null ? (
|
||||
apiReqCancelReason === "user_cancelled" ? (
|
||||
getIconSpan("error", "text-[var(--vscode-descriptionForeground)]")
|
||||
) : (
|
||||
getIconSpan("error", "text-[var(--vscode-errorForeground)]")
|
||||
)
|
||||
) : cost != null ? (
|
||||
getIconSpan("check", "text-[var(--vscode-charts-green)]")
|
||||
) : apiRequestFailedMessage ? (
|
||||
getIconSpan("error", "text-[var(--vscode-errorForeground)]")
|
||||
) : (
|
||||
<ProgressIndicator />
|
||||
)
|
||||
|
||||
const title = (() => {
|
||||
// Default loading state
|
||||
const details = { title: "API Request...", classNames: ["font-bold"] }
|
||||
// Handle cancellation states first
|
||||
if (apiReqCancelReason === "user_cancelled") {
|
||||
details.title = "API Request Cancelled"
|
||||
details.classNames.push("text-[var(--vscode-foreground)]")
|
||||
} else if (apiReqCancelReason != null) {
|
||||
details.title = "API Streaming Failed"
|
||||
details.classNames.push("text-[var(--vscode-errorForeground)]")
|
||||
} else if (cost != null) {
|
||||
// Handle completed request
|
||||
details.title = "API Request"
|
||||
details.classNames.push("text-[var(--vscode-foreground)]")
|
||||
} else if (apiRequestFailedMessage) {
|
||||
// Handle failed request
|
||||
const clineError = ClineError.parse(apiRequestFailedMessage)
|
||||
const titleText = clineError?.isErrorType(ClineErrorType.Balance) ? "Credit Limit Reached" : "API Request Failed"
|
||||
details.title = titleText
|
||||
details.classNames.push("font-bold text-[var(--vscode-errorForeground)]")
|
||||
} else if (retryStatus) {
|
||||
// Handle retry state
|
||||
const retryOperations = Math.max(0, retryStatus.maxAttempts - 1)
|
||||
return <RetryMessage seconds={retryStatus.delaySec} attempt={retryStatus.attempt} retryOperations={retryOperations} />
|
||||
}
|
||||
|
||||
return <span className={details.classNames.join(" ")}>{details.title}</span>
|
||||
})()
|
||||
|
||||
return [icon, title]
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
import { render, screen } from "@testing-library/react"
|
||||
import { describe, it, expect, vi } from "vitest"
|
||||
import ErrorRow from "./ErrorRow"
|
||||
import { ClineMessage } from "@shared/ExtensionMessage"
|
||||
|
||||
// Mock the auth context
|
||||
const mockHandleSignIn = vi.fn()
|
||||
vi.mock("@/context/ClineAuthContext", () => ({
|
||||
useClineAuth: () => ({
|
||||
handleSignIn: mockHandleSignIn,
|
||||
clineUser: null,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock CreditLimitError component
|
||||
vi.mock("@/components/chat/CreditLimitError", () => ({
|
||||
default: ({ message }: { message: string }) => <div data-testid="credit-limit-error">{message}</div>,
|
||||
}))
|
||||
|
||||
// Mock ClineError
|
||||
vi.mock("../../../../src/services/error/ClineError", () => ({
|
||||
ClineError: {
|
||||
parse: vi.fn(),
|
||||
},
|
||||
ClineErrorType: {
|
||||
Balance: "balance",
|
||||
RateLimit: "rateLimit",
|
||||
Auth: "auth",
|
||||
},
|
||||
}))
|
||||
|
||||
describe("ErrorRow", () => {
|
||||
const mockMessage: ClineMessage = {
|
||||
ts: 123456789,
|
||||
type: "say",
|
||||
say: "error",
|
||||
text: "Test error message",
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it("renders basic error message", () => {
|
||||
render(<ErrorRow message={mockMessage} errorType="error" />)
|
||||
|
||||
expect(screen.getByText("Test error message")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders mistake limit reached error", () => {
|
||||
const mistakeMessage = { ...mockMessage, text: "Mistake limit reached" }
|
||||
render(<ErrorRow message={mistakeMessage} errorType="mistake_limit_reached" />)
|
||||
|
||||
expect(screen.getByText("Mistake limit reached")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders auto approval max requests error", () => {
|
||||
const maxReqMessage = { ...mockMessage, text: "Max requests reached" }
|
||||
render(<ErrorRow message={maxReqMessage} errorType="auto_approval_max_req_reached" />)
|
||||
|
||||
expect(screen.getByText("Max requests reached")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders diff error", () => {
|
||||
render(<ErrorRow message={mockMessage} errorType="diff_error" />)
|
||||
|
||||
expect(
|
||||
screen.getByText("The model used search patterns that don't match anything in the file. Retrying..."),
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders clineignore error", () => {
|
||||
const clineignoreMessage = { ...mockMessage, text: "/path/to/file.txt" }
|
||||
render(<ErrorRow message={clineignoreMessage} errorType="clineignore_error" />)
|
||||
|
||||
expect(screen.getByText(/Cline tried to access/)).toBeInTheDocument()
|
||||
expect(screen.getByText("/path/to/file.txt")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
describe("API error handling", () => {
|
||||
it("renders credit limit error when balance error is detected", async () => {
|
||||
const mockClineError = {
|
||||
message: "Insufficient credits",
|
||||
isErrorType: vi.fn((type) => type === "balance"),
|
||||
_error: {
|
||||
details: {
|
||||
current_balance: 0,
|
||||
total_spent: 10.5,
|
||||
total_promotions: 5.0,
|
||||
message: "You have run out of credit.",
|
||||
buy_credits_url: "https://app.cline.bot/dashboard",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const { ClineError } = await import("../../../../src/services/error/ClineError")
|
||||
vi.mocked(ClineError.parse).mockReturnValue(mockClineError as any)
|
||||
|
||||
render(<ErrorRow message={mockMessage} errorType="error" apiRequestFailedMessage="Insufficient credits error" />)
|
||||
|
||||
expect(screen.getByTestId("credit-limit-error")).toBeInTheDocument()
|
||||
expect(screen.getByText("You have run out of credit.")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders rate limit error with request ID", async () => {
|
||||
const mockClineError = {
|
||||
message: "Rate limit exceeded",
|
||||
isErrorType: vi.fn((type) => type === "rateLimit"),
|
||||
_error: {
|
||||
request_id: "req_123456",
|
||||
},
|
||||
}
|
||||
|
||||
const { ClineError } = await import("../../../../src/services/error/ClineError")
|
||||
vi.mocked(ClineError.parse).mockReturnValue(mockClineError as any)
|
||||
|
||||
render(<ErrorRow message={mockMessage} errorType="error" apiRequestFailedMessage="Rate limit exceeded" />)
|
||||
|
||||
expect(screen.getByText("Rate limit exceeded")).toBeInTheDocument()
|
||||
expect(screen.getByText("Request ID: req_123456")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders auth error with sign in button when user is not signed in", async () => {
|
||||
const mockClineError = {
|
||||
message: "Authentication failed",
|
||||
isErrorType: vi.fn((type) => type === "auth"),
|
||||
providerId: "cline",
|
||||
_error: {},
|
||||
}
|
||||
|
||||
const { ClineError } = await import("../../../../src/services/error/ClineError")
|
||||
vi.mocked(ClineError.parse).mockReturnValue(mockClineError as any)
|
||||
|
||||
render(<ErrorRow message={mockMessage} errorType="error" apiRequestFailedMessage="Authentication failed" />)
|
||||
|
||||
expect(screen.getByText("Authentication failed")).toBeInTheDocument()
|
||||
expect(screen.getByText("Sign in to Cline")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders PowerShell troubleshooting link when error mentions PowerShell", async () => {
|
||||
const mockClineError = {
|
||||
message: "PowerShell is not recognized as an internal or external command",
|
||||
isErrorType: vi.fn(() => false),
|
||||
_error: {},
|
||||
}
|
||||
|
||||
const { ClineError } = await import("../../../../src/services/error/ClineError")
|
||||
vi.mocked(ClineError.parse).mockReturnValue(mockClineError as any)
|
||||
|
||||
render(
|
||||
<ErrorRow
|
||||
message={mockMessage}
|
||||
errorType="error"
|
||||
apiRequestFailedMessage="PowerShell is not recognized as an internal or external command"
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText(/PowerShell is not recognized/)).toBeInTheDocument()
|
||||
expect(screen.getByText("troubleshooting guide")).toBeInTheDocument()
|
||||
expect(screen.getByRole("link", { name: "troubleshooting guide" })).toHaveAttribute(
|
||||
"href",
|
||||
"https://github.com/cline/cline/wiki/TroubleShooting-%E2%80%90-%22PowerShell-is-not-recognized-as-an-internal-or-external-command%22",
|
||||
)
|
||||
})
|
||||
|
||||
it("handles apiReqStreamingFailedMessage instead of apiRequestFailedMessage", async () => {
|
||||
const mockClineError = {
|
||||
message: "Streaming failed",
|
||||
isErrorType: vi.fn(() => false),
|
||||
_error: {},
|
||||
}
|
||||
|
||||
const { ClineError } = await import("../../../../src/services/error/ClineError")
|
||||
vi.mocked(ClineError.parse).mockReturnValue(mockClineError as any)
|
||||
|
||||
render(<ErrorRow message={mockMessage} errorType="error" apiReqStreamingFailedMessage="Streaming failed" />)
|
||||
|
||||
expect(screen.getByText("Streaming failed")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("falls back to regular error message when ClineError.parse returns null", async () => {
|
||||
const { ClineError } = await import("../../../../src/services/error/ClineError")
|
||||
vi.mocked(ClineError.parse).mockReturnValue(undefined)
|
||||
|
||||
render(<ErrorRow message={mockMessage} errorType="error" apiRequestFailedMessage="Some API error" />)
|
||||
|
||||
// When ClineError.parse returns null, clineErrorMessage is undefined, so it renders an empty paragraph
|
||||
// The fallback to message.text only happens when there's no apiRequestFailedMessage at all
|
||||
const paragraph = screen.getByRole("paragraph")
|
||||
expect(paragraph).toBeInTheDocument()
|
||||
expect(paragraph).toBeEmptyDOMElement()
|
||||
})
|
||||
|
||||
it("renders regular error message when no API error messages are provided", () => {
|
||||
render(<ErrorRow message={mockMessage} errorType="error" />)
|
||||
|
||||
expect(screen.getByText("Test error message")).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,130 @@
|
||||
import { memo } from "react"
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import { ClineMessage } from "@shared/ExtensionMessage"
|
||||
import { ClineError, ClineErrorType } from "../../../../src/services/error/ClineError"
|
||||
import CreditLimitError from "@/components/chat/CreditLimitError"
|
||||
import { useClineAuth } from "@/context/ClineAuthContext"
|
||||
|
||||
const errorColor = "var(--vscode-errorForeground)"
|
||||
|
||||
interface ErrorRowProps {
|
||||
message: ClineMessage
|
||||
errorType: "error" | "mistake_limit_reached" | "auto_approval_max_req_reached" | "diff_error" | "clineignore_error"
|
||||
apiRequestFailedMessage?: string
|
||||
apiReqStreamingFailedMessage?: string
|
||||
}
|
||||
|
||||
const ErrorRow = memo(({ message, errorType, apiRequestFailedMessage, apiReqStreamingFailedMessage }: ErrorRowProps) => {
|
||||
const { handleSignIn, clineUser } = useClineAuth()
|
||||
|
||||
const renderErrorContent = () => {
|
||||
switch (errorType) {
|
||||
case "error":
|
||||
case "mistake_limit_reached":
|
||||
case "auto_approval_max_req_reached":
|
||||
// Handle API request errors with special error parsing
|
||||
if (apiRequestFailedMessage || apiReqStreamingFailedMessage) {
|
||||
const clineError = ClineError.parse(apiRequestFailedMessage || apiReqStreamingFailedMessage)
|
||||
const clineErrorMessage = clineError?.message
|
||||
const requestId = clineError?._error?.request_id
|
||||
const isClineProvider = clineError?.providerId === "cline"
|
||||
|
||||
if (clineError) {
|
||||
if (clineError.isErrorType(ClineErrorType.Balance)) {
|
||||
const errorDetails = clineError._error?.details
|
||||
return (
|
||||
<CreditLimitError
|
||||
currentBalance={errorDetails?.current_balance}
|
||||
totalSpent={errorDetails?.total_spent}
|
||||
totalPromotions={errorDetails?.total_promotions}
|
||||
message={errorDetails?.message}
|
||||
buyCreditsUrl={errorDetails?.buy_credits_url}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (clineError?.isErrorType(ClineErrorType.RateLimit)) {
|
||||
return (
|
||||
<p className="m-0 whitespace-pre-wrap text-[var(--vscode-errorForeground)] wrap-anywhere">
|
||||
{clineErrorMessage}
|
||||
{requestId && <div>Request ID: {requestId}</div>}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
// Default error display
|
||||
return (
|
||||
<p className="m-0 whitespace-pre-wrap text-[var(--vscode-errorForeground)] wrap-anywhere">
|
||||
{clineErrorMessage}
|
||||
{requestId && <div>Request ID: {requestId}</div>}
|
||||
{clineErrorMessage?.toLowerCase()?.includes("powershell") && (
|
||||
<>
|
||||
<br />
|
||||
<br />
|
||||
It seems like you're having Windows PowerShell issues, please see this{" "}
|
||||
<a
|
||||
href="https://github.com/cline/cline/wiki/TroubleShooting-%E2%80%90-%22PowerShell-is-not-recognized-as-an-internal-or-external-command%22"
|
||||
className="underline text-inherit">
|
||||
troubleshooting guide
|
||||
</a>
|
||||
.
|
||||
</>
|
||||
)}
|
||||
{clineError?.isErrorType(ClineErrorType.Auth) && (
|
||||
<>
|
||||
<br />
|
||||
<br />
|
||||
{/* The user is signed in or not using cline provider */}
|
||||
{clineUser && !isClineProvider ? (
|
||||
<span className="mb-4 text-[var(--vscode-descriptionForeground)]">
|
||||
(Click "Retry" below)
|
||||
</span>
|
||||
) : (
|
||||
<VSCodeButton onClick={handleSignIn} className="w-full mb-4">
|
||||
Sign in to Cline
|
||||
</VSCodeButton>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
// Regular error message
|
||||
return (
|
||||
<p className="m-0 whitespace-pre-wrap text-[var(--vscode-errorForeground)] wrap-anywhere">{message.text}</p>
|
||||
)
|
||||
|
||||
case "diff_error":
|
||||
return (
|
||||
<div className="flex flex-col p-2 rounded text-xs opacity-80 bg-[var(--vscode-textBlockQuote-background)] text-[var(--vscode-foreground)]">
|
||||
<div>The model used search patterns that don't match anything in the file. Retrying...</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
case "clineignore_error":
|
||||
return (
|
||||
<div className="flex flex-col p-2 rounded text-xs bg-[var(--vscode-textBlockQuote-background)] text-[var(--vscode-foreground)] opacity-80">
|
||||
<div>
|
||||
Cline tried to access <code>{message.text}</code> which is blocked by the <code>.clineignore</code>
|
||||
file.
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// For diff_error and clineignore_error, we don't show the header separately
|
||||
if (errorType === "diff_error" || errorType === "clineignore_error") {
|
||||
return <>{renderErrorContent()}</>
|
||||
}
|
||||
|
||||
// For other error types, show header + content
|
||||
return <>{renderErrorContent()}</>
|
||||
})
|
||||
|
||||
export default ErrorRow
|
||||
@@ -0,0 +1,51 @@
|
||||
import { describe, it, expect } from "vitest"
|
||||
import { ErrorBlockTitle } from "../ErrorBlockTitle"
|
||||
|
||||
describe("ErrorBlockTitle", () => {
|
||||
it("should return icon and title for API request cancelled", () => {
|
||||
const [icon, title] = ErrorBlockTitle({
|
||||
apiReqCancelReason: "user_cancelled",
|
||||
})
|
||||
|
||||
expect(icon).toBeDefined()
|
||||
expect(title).toBeDefined()
|
||||
})
|
||||
|
||||
it("should return icon and title for completed API request", () => {
|
||||
const [icon, title] = ErrorBlockTitle({
|
||||
cost: 0.001,
|
||||
})
|
||||
|
||||
expect(icon).toBeDefined()
|
||||
expect(title).toBeDefined()
|
||||
})
|
||||
|
||||
it("should return icon and title for failed API request", () => {
|
||||
const [icon, title] = ErrorBlockTitle({
|
||||
apiRequestFailedMessage: "Request failed",
|
||||
})
|
||||
|
||||
expect(icon).toBeDefined()
|
||||
expect(title).toBeDefined()
|
||||
})
|
||||
|
||||
it("should return icon and title for retry status", () => {
|
||||
const [icon, title] = ErrorBlockTitle({
|
||||
retryStatus: {
|
||||
attempt: 2,
|
||||
maxAttempts: 3,
|
||||
delaySec: 5,
|
||||
},
|
||||
})
|
||||
|
||||
expect(icon).toBeDefined()
|
||||
expect(title).toBeDefined()
|
||||
})
|
||||
|
||||
it("should return icon and title for default API request", () => {
|
||||
const [icon, title] = ErrorBlockTitle({})
|
||||
|
||||
expect(icon).toBeDefined()
|
||||
expect(title).toBeDefined()
|
||||
})
|
||||
})
|
||||
@@ -30,6 +30,7 @@ import { ClaudeCodeProvider } from "./providers/ClaudeCodeProvider"
|
||||
import { SapAiCoreProvider } from "./providers/SapAiCoreProvider"
|
||||
import { BedrockProvider } from "./providers/BedrockProvider"
|
||||
import { MoonshotProvider } from "./providers/MoonshotProvider"
|
||||
import { HuggingFaceProvider } from "./providers/HuggingFaceProvider"
|
||||
import { NebiusProvider } from "./providers/NebiusProvider"
|
||||
import { LiteLlmProvider } from "./providers/LiteLlmProvider"
|
||||
import { VSCodeLmProvider } from "./providers/VSCodeLmProvider"
|
||||
@@ -159,6 +160,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
|
||||
<VSCodeOption value="ollama">Ollama</VSCodeOption>
|
||||
<VSCodeOption value="litellm">LiteLLM</VSCodeOption>
|
||||
<VSCodeOption value="moonshot">Moonshot AI</VSCodeOption>
|
||||
<VSCodeOption value="huggingface">Hugging Face</VSCodeOption>
|
||||
<VSCodeOption value="nebius">Nebius AI Studio</VSCodeOption>
|
||||
<VSCodeOption value="asksage">AskSage</VSCodeOption>
|
||||
<VSCodeOption value="xai">xAI</VSCodeOption>
|
||||
@@ -261,6 +263,10 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
|
||||
<MoonshotProvider showModelOptions={showModelOptions} isPopup={isPopup} />
|
||||
)}
|
||||
|
||||
{apiConfiguration && selectedProvider === "huggingface" && (
|
||||
<HuggingFaceProvider showModelOptions={showModelOptions} isPopup={isPopup} />
|
||||
)}
|
||||
|
||||
{apiConfiguration && selectedProvider === "nebius" && (
|
||||
<NebiusProvider showModelOptions={showModelOptions} isPopup={isPopup} />
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
import { EmptyRequest } from "@shared/proto/common"
|
||||
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
import Fuse from "fuse.js"
|
||||
import React, { KeyboardEvent, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { useMount } from "react-use"
|
||||
import { huggingFaceDefaultModelId, huggingFaceModels } from "@shared/api"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { ModelsServiceClient } from "../../services/grpc-client"
|
||||
import { highlight } from "../history/HistoryView"
|
||||
import { ModelInfoView } from "./common/ModelInfoView"
|
||||
import { normalizeApiConfiguration } from "./utils/providerUtils"
|
||||
import { useApiConfigurationHandlers } from "./utils/useApiConfigurationHandlers"
|
||||
|
||||
export interface HuggingFaceModelPickerProps {
|
||||
isPopup?: boolean
|
||||
}
|
||||
|
||||
const HuggingFaceModelPicker: React.FC<HuggingFaceModelPickerProps> = ({ isPopup }) => {
|
||||
const { apiConfiguration, huggingFaceModels: dynamicModels, setHuggingFaceModels } = useExtensionState()
|
||||
const { handleFieldsChange } = useApiConfigurationHandlers()
|
||||
const [searchTerm, setSearchTerm] = useState(apiConfiguration?.huggingFaceModelId || huggingFaceDefaultModelId)
|
||||
const [isDropdownVisible, setIsDropdownVisible] = useState(false)
|
||||
const [selectedIndex, setSelectedIndex] = useState(-1)
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
const itemRefs = useRef<(HTMLDivElement | null)[]>([])
|
||||
const dropdownListRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const handleModelChange = (newModelId: string) => {
|
||||
const allModels = { ...huggingFaceModels, ...dynamicModels }
|
||||
handleFieldsChange({
|
||||
huggingFaceModelId: newModelId,
|
||||
huggingFaceModelInfo: allModels[newModelId as keyof typeof allModels],
|
||||
})
|
||||
setSearchTerm(newModelId)
|
||||
}
|
||||
|
||||
const { selectedModelId, selectedModelInfo } = useMemo(() => {
|
||||
return normalizeApiConfiguration(apiConfiguration)
|
||||
}, [apiConfiguration])
|
||||
|
||||
useMount(() => {
|
||||
ModelsServiceClient.refreshHuggingFaceModels(EmptyRequest.create({}))
|
||||
.then((response) => {
|
||||
setHuggingFaceModels({
|
||||
[huggingFaceDefaultModelId]: huggingFaceModels[huggingFaceDefaultModelId],
|
||||
...response.models,
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to refresh Hugging Face models:", err)
|
||||
})
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
setIsDropdownVisible(false)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside)
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const allModels = useMemo(() => {
|
||||
return { ...huggingFaceModels, ...dynamicModels }
|
||||
}, [dynamicModels])
|
||||
|
||||
const modelIds = useMemo(() => {
|
||||
return Object.keys(allModels).sort((a, b) => a.localeCompare(b))
|
||||
}, [allModels])
|
||||
|
||||
const searchableItems = useMemo(() => {
|
||||
return modelIds.map((id) => ({
|
||||
id,
|
||||
html: id,
|
||||
}))
|
||||
}, [modelIds])
|
||||
|
||||
const fuse = useMemo(() => {
|
||||
return new Fuse(searchableItems, {
|
||||
keys: ["html"],
|
||||
threshold: 0.6,
|
||||
shouldSort: true,
|
||||
isCaseSensitive: false,
|
||||
ignoreLocation: false,
|
||||
includeMatches: true,
|
||||
minMatchCharLength: 1,
|
||||
})
|
||||
}, [searchableItems])
|
||||
|
||||
const modelSearchResults = useMemo(() => {
|
||||
let results: { id: string; html: string }[] = searchTerm
|
||||
? highlight(fuse.search(searchTerm), "model-item-highlight")
|
||||
: searchableItems
|
||||
return results
|
||||
}, [searchTerm, fuse, searchableItems])
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLElement>) => {
|
||||
if (!isDropdownVisible) return
|
||||
|
||||
switch (e.key) {
|
||||
case "ArrowDown":
|
||||
e.preventDefault()
|
||||
setSelectedIndex((prev) => (prev < modelSearchResults.length - 1 ? prev + 1 : 0))
|
||||
break
|
||||
case "ArrowUp":
|
||||
e.preventDefault()
|
||||
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : modelSearchResults.length - 1))
|
||||
break
|
||||
case "Enter":
|
||||
e.preventDefault()
|
||||
if (selectedIndex >= 0 && selectedIndex < modelSearchResults.length) {
|
||||
const selectedModelId = modelSearchResults[selectedIndex].id
|
||||
handleModelChange(selectedModelId)
|
||||
setIsDropdownVisible(false)
|
||||
}
|
||||
break
|
||||
case "Escape":
|
||||
e.preventDefault()
|
||||
setIsDropdownVisible(false)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedIndex >= 0 && itemRefs.current[selectedIndex] && dropdownListRef.current) {
|
||||
const selectedItem = itemRefs.current[selectedIndex]
|
||||
const dropdown = dropdownListRef.current
|
||||
const itemOffsetTop = selectedItem.offsetTop
|
||||
const itemHeight = selectedItem.offsetHeight
|
||||
const dropdownScrollTop = dropdown.scrollTop
|
||||
const dropdownHeight = dropdown.offsetHeight
|
||||
|
||||
if (itemOffsetTop < dropdownScrollTop) {
|
||||
dropdown.scrollTop = itemOffsetTop
|
||||
} else if (itemOffsetTop + itemHeight > dropdownScrollTop + dropdownHeight) {
|
||||
dropdown.scrollTop = itemOffsetTop + itemHeight - dropdownHeight
|
||||
}
|
||||
}
|
||||
}, [selectedIndex])
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="flex flex-col">
|
||||
<label htmlFor="hf-model-search">
|
||||
<span className="font-medium">Model</span>
|
||||
</label>
|
||||
|
||||
<div ref={dropdownRef} className="relative w-full">
|
||||
<VSCodeTextField
|
||||
id="hf-model-search"
|
||||
placeholder="Search models..."
|
||||
value={searchTerm}
|
||||
onInput={(e: any) => {
|
||||
setSearchTerm(e.target.value)
|
||||
setIsDropdownVisible(true)
|
||||
setSelectedIndex(-1)
|
||||
}}
|
||||
onFocus={() => setIsDropdownVisible(true)}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="w-full relative z-[1000]"
|
||||
/>
|
||||
{isDropdownVisible && (
|
||||
<div
|
||||
ref={dropdownListRef}
|
||||
className={`absolute top-[calc(100%-3px)] left-0 w-[calc(100%-2px)] ${
|
||||
isPopup ? "max-h-[90px]" : "max-h-[200px]"
|
||||
} overflow-y-auto bg-[var(--vscode-dropdown-background)] border border-[var(--vscode-list-activeSelectionBackground)] z-[999] rounded-b-[3px]`}>
|
||||
{modelSearchResults.map((result, index) => (
|
||||
<div
|
||||
key={result.id}
|
||||
ref={(el: HTMLDivElement | null) => (itemRefs.current[index] = el)}
|
||||
className={`p-[5px_10px] cursor-pointer break-all whitespace-normal ${
|
||||
index === selectedIndex ? "bg-[var(--vscode-list-activeSelectionBackground)]" : ""
|
||||
} hover:bg-[var(--vscode-list-activeSelectionBackground)]`}
|
||||
onMouseEnter={() => setSelectedIndex(index)}
|
||||
onClick={() => {
|
||||
handleModelChange(result.id)
|
||||
setIsDropdownVisible(false)
|
||||
}}>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: result.html }}
|
||||
className="[&_.model-item-highlight]:bg-[var(--vscode-editor-findMatchHighlightBackground)] [&_.model-item-highlight]:text-inherit"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { HuggingFaceModelPicker }
|
||||
@@ -0,0 +1,58 @@
|
||||
import { huggingFaceModels } from "@shared/api"
|
||||
import { DebouncedTextField } from "../common/DebouncedTextField"
|
||||
import { ModelSelector } from "../common/ModelSelector"
|
||||
import { ModelInfoView } from "../common/ModelInfoView"
|
||||
import { normalizeApiConfiguration } from "../utils/providerUtils"
|
||||
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { HuggingFaceModelPicker } from "../HuggingFaceModelPicker"
|
||||
|
||||
/**
|
||||
* Props for the HuggingFaceProvider component
|
||||
*/
|
||||
interface HuggingFaceProviderProps {
|
||||
showModelOptions: boolean
|
||||
isPopup?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* The Hugging Face provider configuration component
|
||||
*/
|
||||
export const HuggingFaceProvider = ({ showModelOptions, isPopup }: HuggingFaceProviderProps) => {
|
||||
const { apiConfiguration } = useExtensionState()
|
||||
const { handleFieldChange } = useApiConfigurationHandlers()
|
||||
|
||||
// Get the normalized configuration
|
||||
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DebouncedTextField
|
||||
initialValue={apiConfiguration?.huggingFaceApiKey || ""}
|
||||
onChange={(value) => handleFieldChange("huggingFaceApiKey", value)}
|
||||
style={{ width: "100%" }}
|
||||
type="password"
|
||||
placeholder="Enter API Key...">
|
||||
<span style={{ fontWeight: 500 }}>Hugging Face API Key</span>
|
||||
</DebouncedTextField>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: "5px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
This key is stored locally and only used to make API requests from this extension. We don’t show pricing here
|
||||
because it depends on your Hugging Face provider settings and isn’t consistently available via their API{" "}
|
||||
<a href="https://huggingface.co/settings/tokens" target="_blank" rel="noopener noreferrer">
|
||||
Get your API key here
|
||||
</a>
|
||||
</p>
|
||||
|
||||
{showModelOptions && (
|
||||
<>
|
||||
<HuggingFaceModelPicker isPopup={isPopup} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -36,6 +36,8 @@ import {
|
||||
liteLlmModelInfoSaneDefaults,
|
||||
moonshotModels,
|
||||
moonshotDefaultModelId,
|
||||
huggingFaceModels,
|
||||
huggingFaceDefaultModelId,
|
||||
nebiusModels,
|
||||
nebiusDefaultModelId,
|
||||
cerebrasModels,
|
||||
@@ -174,6 +176,12 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration):
|
||||
return getProviderData(xaiModels, xaiDefaultModelId)
|
||||
case "moonshot":
|
||||
return getProviderData(moonshotModels, moonshotDefaultModelId)
|
||||
case "huggingface":
|
||||
return {
|
||||
selectedProvider: provider,
|
||||
selectedModelId: apiConfiguration?.huggingFaceModelId || huggingFaceDefaultModelId,
|
||||
selectedModelInfo: apiConfiguration?.huggingFaceModelInfo || huggingFaceModels[huggingFaceDefaultModelId],
|
||||
}
|
||||
case "nebius":
|
||||
return getProviderData(nebiusModels, nebiusDefaultModelId)
|
||||
case "sambanova":
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react"
|
||||
import { useState, useEffect } from "react"
|
||||
import { useDebounceEffect } from "@/utils/useDebounceEffect"
|
||||
|
||||
/**
|
||||
@@ -18,6 +18,11 @@ export function useDebouncedInput<T>(
|
||||
// Local state to prevent jumpy input - initialize once
|
||||
const [localValue, setLocalValue] = useState(initialValue)
|
||||
|
||||
// Update local value when initialValue changes (e.g., when component remounts with new data)
|
||||
useEffect(() => {
|
||||
setLocalValue(initialValue)
|
||||
}, [initialValue])
|
||||
|
||||
// Debounced backend save - saves after user stops changing value
|
||||
useDebounceEffect(
|
||||
() => {
|
||||
|
||||
@@ -1,21 +1,12 @@
|
||||
export type Environment = "production" | "staging" | "local"
|
||||
|
||||
const CURRENT_ENVIRONMENT: Environment = "production"
|
||||
// Use the injected global variable from vite.config.ts
|
||||
declare const __APP_BASE_URL__: string
|
||||
|
||||
interface EnvironmentConfig {
|
||||
appBaseUrl: string
|
||||
}
|
||||
|
||||
const configs: Record<Environment, EnvironmentConfig> = {
|
||||
production: {
|
||||
appBaseUrl: "https://app.cline.bot",
|
||||
},
|
||||
staging: {
|
||||
appBaseUrl: "https://staging-app.cline.bot",
|
||||
},
|
||||
local: {
|
||||
appBaseUrl: "http://localhost:3000",
|
||||
},
|
||||
export const clineEnvConfig: EnvironmentConfig = {
|
||||
appBaseUrl: typeof __APP_BASE_URL__ !== "undefined" ? __APP_BASE_URL__ : "https://app.cline.bot",
|
||||
}
|
||||
|
||||
export const clineEnvConfig = configs[CURRENT_ENVIRONMENT]
|
||||
|
||||
@@ -26,6 +26,8 @@ import {
|
||||
requestyDefaultModelInfo,
|
||||
groqDefaultModelId,
|
||||
groqModels,
|
||||
huggingFaceDefaultModelId,
|
||||
huggingFaceModels,
|
||||
} from "../../../src/shared/api"
|
||||
import { McpMarketplaceCatalog, McpServer, McpViewTab } from "../../../src/shared/mcp"
|
||||
import { convertTextMateToHljs } from "../utils/textMateToHljs"
|
||||
@@ -41,6 +43,7 @@ interface ExtensionStateContextType extends ExtensionState {
|
||||
openAiModels: string[]
|
||||
requestyModels: Record<string, ModelInfo>
|
||||
groqModels: Record<string, ModelInfo>
|
||||
huggingFaceModels: Record<string, ModelInfo>
|
||||
mcpServers: McpServer[]
|
||||
mcpMarketplaceCatalog: McpMarketplaceCatalog
|
||||
filePaths: string[]
|
||||
@@ -62,6 +65,7 @@ interface ExtensionStateContextType extends ExtensionState {
|
||||
setMcpServers: (value: McpServer[]) => void
|
||||
setRequestyModels: (value: Record<string, ModelInfo>) => void
|
||||
setGroqModels: (value: Record<string, ModelInfo>) => void
|
||||
setHuggingFaceModels: (value: Record<string, ModelInfo>) => void
|
||||
setGlobalClineRulesToggles: (toggles: Record<string, boolean>) => void
|
||||
setLocalClineRulesToggles: (toggles: Record<string, boolean>) => void
|
||||
setLocalCursorRulesToggles: (toggles: Record<string, boolean>) => void
|
||||
@@ -212,6 +216,7 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
const [groqModelsState, setGroqModels] = useState<Record<string, ModelInfo>>({
|
||||
[groqDefaultModelId]: groqModels[groqDefaultModelId],
|
||||
})
|
||||
const [huggingFaceModels, setHuggingFaceModels] = useState<Record<string, ModelInfo>>({})
|
||||
const [mcpServers, setMcpServers] = useState<McpServer[]>([])
|
||||
const [mcpMarketplaceCatalog, setMcpMarketplaceCatalog] = useState<McpMarketplaceCatalog>({ items: [] })
|
||||
|
||||
@@ -638,6 +643,7 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
openAiModels,
|
||||
requestyModels,
|
||||
groqModels: groqModelsState,
|
||||
huggingFaceModels,
|
||||
mcpServers,
|
||||
mcpMarketplaceCatalog,
|
||||
filePaths,
|
||||
@@ -678,6 +684,7 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
setMcpServers: (mcpServers: McpServer[]) => setMcpServers(mcpServers),
|
||||
setRequestyModels: (models: Record<string, ModelInfo>) => setRequestyModels(models),
|
||||
setGroqModels: (models: Record<string, ModelInfo>) => setGroqModels(models),
|
||||
setHuggingFaceModels: (models: Record<string, ModelInfo>) => setHuggingFaceModels(models),
|
||||
setMcpMarketplaceCatalog: (catalog: McpMarketplaceCatalog) => setMcpMarketplaceCatalog(catalog),
|
||||
setShowMcp,
|
||||
closeMcpView,
|
||||
|
||||
@@ -67,7 +67,15 @@ export default defineConfig({
|
||||
NODE_ENV: JSON.stringify(process.env.IS_DEV ? "development" : "production"),
|
||||
IS_DEV: JSON.stringify(process.env.IS_DEV),
|
||||
IS_TEST: JSON.stringify(process.env.IS_TEST),
|
||||
CLINE_ENVIRONMENT: JSON.stringify(process.env.CLINE_ENVIRONMENT ?? "production"),
|
||||
},
|
||||
__APP_BASE_URL__: JSON.stringify(
|
||||
process.env.CLINE_ENVIRONMENT === "local"
|
||||
? "http://localhost:3000"
|
||||
: process.env.CLINE_ENVIRONMENT === "staging"
|
||||
? "https://staging-app.cline.bot"
|
||||
: "https://app.cline.bot",
|
||||
),
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
Reference in New Issue
Block a user