mirror of
https://github.com/cline/cline.git
synced 2026-09-11 16:42:40 +08:00
Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30c03ba18b | ||
|
|
ae01f0f1ba | ||
|
|
85ba2d3816 | ||
|
|
3e5abd5e72 | ||
|
|
1ba5873454 | ||
|
|
1bdaf8ef6f | ||
|
|
7f6038c74e | ||
|
|
2fd9635b97 | ||
|
|
568b834338 | ||
|
|
381e9b9d1f | ||
|
|
d86861629d | ||
|
|
7fb10ba053 | ||
|
|
b7ca95ed57 | ||
|
|
6bd8726dd6 | ||
|
|
347d4f48da | ||
|
|
baa5aaa0a7 | ||
|
|
16f066dcbf | ||
|
|
59f42c7a81 | ||
|
|
3a86938a56 | ||
|
|
042bf359a9 | ||
|
|
17200740a8 | ||
|
|
c38f443ec4 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Add Kimi-K2 as the trending model in the Cline Provider
|
||||
@@ -0,0 +1,75 @@
|
||||
# Git Diff Analysis Workflow
|
||||
|
||||
## Objective
|
||||
Analyze the current branch's changes against main to provide informed insights and context for development decisions.
|
||||
|
||||
## Step 1: Gather Git Information
|
||||
<important>Do not return any text or conversation other than what is necessary to run these commands</important>
|
||||
|
||||
**First, check the expected output size:**
|
||||
```shell
|
||||
(git branch --show-current && echo "=== STATUS ===" && git status --porcelain | cat && echo "=== COMMIT MESSAGES ===" && git log main..HEAD --oneline | cat && echo "=== CHANGED FILES ===" && git diff main --name-only | cat && echo "=== FULL DIFF ===" && git diff main | cat) | wc -l
|
||||
```
|
||||
|
||||
**If the expected line count is greater than 500 lines, use the file-based approach:**
|
||||
```shell
|
||||
git branch --show-current > cline-git-analysis.temp && echo "=== STATUS ===" >> cline-git-analysis.temp && git status --porcelain >> cline-git-analysis.temp && echo "=== COMMIT MESSAGES ===" >> cline-git-analysis.temp && git log main..HEAD --oneline >> cline-git-analysis.temp && echo "=== CHANGED FILES ===" >> cline-git-analysis.temp && git diff main --name-only >> cline-git-analysis.temp && echo "=== FULL DIFF ===" >> cline-git-analysis.temp && git diff main >> cline-git-analysis.temp
|
||||
```
|
||||
|
||||
Then, read the file using the read_file tool. After you have read the file but before you proceed with subsequent steps, delete it:
|
||||
```shell
|
||||
rm cline-git-analysis.temp
|
||||
```
|
||||
|
||||
**If the expected line count is 500 lines or fewer, use the direct approach:**
|
||||
```shell
|
||||
git branch --show-current && echo "=== STATUS ===" && git status --porcelain | cat && echo "=== COMMIT MESSAGES ===" && git log main..HEAD --oneline | cat && echo "=== CHANGED FILES ===" && git diff main --name-only | cat && echo "=== FULL DIFF ===" && git diff main | cat
|
||||
```
|
||||
|
||||
<important>If using the direct approach, pipe outputs through `cat` to avoid interactive terminals. If the user's shell is not bash/zsh, adjust the command and chaining
|
||||
syntax accordingly.</important>
|
||||
|
||||
## Step 2: Silent, Structured Analysis Phase
|
||||
- Analyze all git output without providing commentary or narration
|
||||
- Read the full diff to understand the scope and nature of changes
|
||||
- Identify patterns, architectural modifications, or potential impacts
|
||||
- Use `read_file` to examine any related files providing additional context on the changes you have observed
|
||||
|
||||
## Step 3: Context Gathering
|
||||
- Analyze related code without providing commentary or narration
|
||||
- Read relevant related source files if needed for complete understanding
|
||||
- Check dependencies, imports, or cross-references spanning the changes
|
||||
- Understand the broader codebase context around modifications
|
||||
- This additional context gathering should include related backend code, as well as related ui/frontend code
|
||||
- You will typically need to analyze at least several files, potentially many, in order to fully complete this step
|
||||
- You should not continue reading additional context if you have exhausted more than 60% of your available context window
|
||||
- If you have exhausted less than 40% of your context window, you should continue reviewing additional context
|
||||
|
||||
## Step 4: Ready for User Interaction
|
||||
**Only after completing the full analysis:**
|
||||
- Engage with the user based on comprehensive understanding
|
||||
- Provide insights about specific modifications and their impacts
|
||||
- If you are certain they exist, note potential breaking changes or compatibility issues
|
||||
- Answer questions with informed context from the complete change set and context gathering
|
||||
- If the user has not provided a question, or the question is insufficient to provide a quality response, ask brief (one sentence) clarifying questions.
|
||||
- Only offer recommendations if they are applicable to the user's request and relevant to the changes that you have observed
|
||||
|
||||
## Key Rules
|
||||
- **No prose or conversation during git research phase**
|
||||
- **No prose or conversation during context gathering phase**
|
||||
- **Complete all analysis before any user interaction**
|
||||
- **Use gathered information for all subsequent questions and insights**
|
||||
- **Focus on understanding the complete picture before discussing**
|
||||
|
||||
## Optional: Additional Analysis Commands
|
||||
For deeper investigation when needed:
|
||||
|
||||
```shell
|
||||
# Detailed commit history with author info
|
||||
git log main..HEAD --format="%h %s (%an)" | cat
|
||||
|
||||
# Change statistics
|
||||
git diff main --stat | cat
|
||||
|
||||
# Specific file type changes
|
||||
git diff main --name-only | grep -E '\.(ts|js|tsx|jsx|py|md)$' | cat
|
||||
@@ -1,5 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## [3.18.14]
|
||||
|
||||
- Fix bug where Cline account users logged in with invalid token would not be shown as logged out in webview presentation layer
|
||||
|
||||
## [3.18.13]
|
||||
|
||||
- Fix authentication issue where Cline accounts users would keep getting logged out or seeing 'Unexpected API response' errors
|
||||
|
||||
## [3.18.12]
|
||||
|
||||
- Fix flaky organization switching behavior in Cline provider that caused UI inconsistencies and double loading
|
||||
|
||||
Generated
+17
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-dev",
|
||||
"version": "3.18.12",
|
||||
"version": "3.18.14",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-dev",
|
||||
"version": "3.18.12",
|
||||
"version": "3.18.14",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.37.0",
|
||||
@@ -51,6 +51,7 @@
|
||||
"image-size": "^2.0.2",
|
||||
"isbinaryfile": "^5.0.2",
|
||||
"jschardet": "^3.1.4",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"mammoth": "^1.8.0",
|
||||
"monaco-vscode-textmate-theme-converter": "^0.1.7",
|
||||
"nice-grpc": "^2.1.12",
|
||||
@@ -15785,6 +15786,15 @@
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/jwt-decode": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
|
||||
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/katex": {
|
||||
"version": "0.16.22",
|
||||
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.22.tgz",
|
||||
@@ -35228,6 +35238,11 @@
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"jwt-decode": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
|
||||
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA=="
|
||||
},
|
||||
"katex": {
|
||||
"version": "0.16.22",
|
||||
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.22.tgz",
|
||||
|
||||
+2
-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.18.12",
|
||||
"version": "3.18.14",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
@@ -449,6 +449,7 @@
|
||||
"image-size": "^2.0.2",
|
||||
"isbinaryfile": "^5.0.2",
|
||||
"jschardet": "^3.1.4",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"mammoth": "^1.8.0",
|
||||
"monaco-vscode-textmate-theme-converter": "^0.1.7",
|
||||
"nice-grpc": "^2.1.12",
|
||||
|
||||
@@ -27,5 +27,6 @@ export const hostServiceNameMap = {
|
||||
workspace: "host.WorkspaceService",
|
||||
env: "host.EnvService",
|
||||
window: "host.WindowService",
|
||||
diff: "host.DiffService",
|
||||
// Add new host services here
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package host;
|
||||
option java_package = "bot.cline.host.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "common.proto";
|
||||
|
||||
// Provides methods for diff views.
|
||||
service DiffService {
|
||||
// Open the diff view/editor.
|
||||
rpc openDiff(OpenDiffRequest) returns (OpenDiffResponse);
|
||||
}
|
||||
|
||||
message OpenDiffRequest {
|
||||
optional cline.Metadata metadata = 1;
|
||||
// The absolute path of the document being edited.
|
||||
optional string path = 2;
|
||||
// The new content for the file.
|
||||
optional string content = 3;
|
||||
}
|
||||
|
||||
message OpenDiffResponse {
|
||||
// TODO(sfortune) the host needs to return a unique id for the diff editor.
|
||||
}
|
||||
@@ -11,6 +11,7 @@ service WindowService {
|
||||
// Opens a text document in the editor and returns editor information.
|
||||
rpc showTextDocument(ShowTextDocumentRequest) returns (TextEditorInfo);
|
||||
rpc showOpenDialogue(ShowOpenDialogueRequest) returns (SelectedResources);
|
||||
rpc showMessage(ShowMessageRequest) returns (SelectedResponse);
|
||||
}
|
||||
|
||||
message ShowTextDocumentRequest {
|
||||
@@ -46,3 +47,27 @@ message ShowOpenDialogueFilterOption {
|
||||
message SelectedResources {
|
||||
repeated string paths = 1;
|
||||
}
|
||||
|
||||
enum ShowMessageType {
|
||||
ERROR = 0;
|
||||
INFORMATION = 1;
|
||||
WARNING = 2;
|
||||
}
|
||||
|
||||
message ShowMessageRequest {
|
||||
cline.Metadata metadata = 1;
|
||||
ShowMessageType type = 2;
|
||||
string message = 3;
|
||||
optional ShowMessageRequestOptions options = 4;
|
||||
}
|
||||
|
||||
message ShowMessageRequestOptions {
|
||||
repeated string items = 1;
|
||||
optional bool modal = 2;
|
||||
optional string detail = 3;
|
||||
|
||||
}
|
||||
|
||||
message SelectedResponse {
|
||||
optional string selected_option = 1;
|
||||
}
|
||||
@@ -9,6 +9,8 @@ import { OpenRouterErrorResponse } from "./types"
|
||||
import { withRetry } from "../retry"
|
||||
import { AuthService } from "@/services/auth/AuthService"
|
||||
import OpenAI from "openai"
|
||||
import { version as extensionVersion } from "../../../package.json"
|
||||
import { shouldSkipReasoningForModel } from "@utils/model-utils"
|
||||
|
||||
interface ClineHandlerOptions {
|
||||
taskId?: string
|
||||
@@ -51,6 +53,7 @@ export class ClineHandler implements ApiHandler {
|
||||
"HTTP-Referer": "https://cline.bot",
|
||||
"X-Title": "Cline",
|
||||
"X-Task-ID": this.options.taskId || "",
|
||||
"X-Cline-Version": extensionVersion,
|
||||
},
|
||||
})
|
||||
} catch (error: any) {
|
||||
@@ -125,7 +128,8 @@ export class ClineHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
// Reasoning tokens are returned separately from the content
|
||||
if ("reasoning" in delta && delta.reasoning) {
|
||||
// Skip reasoning content for Grok 4 models since it only displays "thinking" without providing useful information
|
||||
if ("reasoning" in delta && delta.reasoning && !shouldSkipReasoningForModel(this.options.openRouterModelId)) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
// @ts-ignore-next-line
|
||||
@@ -180,7 +184,7 @@ export class ClineHandler implements ApiHandler {
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code === "ERR_BAD_REQUEST" || error.status === 401) {
|
||||
throw new Error("Unauthorized: Please sign in to Cline before trying again.")
|
||||
throw new Error("Unauthorized: Please sign in to Cline before trying again.") // match with webview-ui/src/components/chat/ChatRow.tsx
|
||||
} else if (error.code === "insufficient_credits" || error.status === 402) {
|
||||
throw new Error(error.error ? JSON.stringify(error.error) : "Insufficient credits or unknown error.")
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { withRetry } from "../retry"
|
||||
import { createOpenRouterStream } from "../transform/openrouter-stream"
|
||||
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
|
||||
import { OpenRouterErrorResponse } from "./types"
|
||||
import { shouldSkipReasoningForModel } from "@utils/model-utils"
|
||||
|
||||
interface OpenRouterHandlerOptions {
|
||||
openRouterApiKey?: string
|
||||
@@ -112,7 +113,8 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
// Reasoning tokens are returned separately from the content
|
||||
if ("reasoning" in delta && delta.reasoning) {
|
||||
// Skip reasoning content for Grok 4 models since it only displays "thinking" without providing useful information
|
||||
if ("reasoning" in delta && delta.reasoning && !shouldSkipReasoningForModel(this.options.openRouterModelId)) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
// @ts-ignore-next-line
|
||||
|
||||
@@ -6,6 +6,7 @@ import { convertToOpenAiMessages } from "@api/transform/openai-format"
|
||||
import { ApiStream } from "@api/transform/stream"
|
||||
import { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
|
||||
import { withRetry } from "../retry"
|
||||
import { shouldSkipReasoningForModel } from "@utils/model-utils"
|
||||
|
||||
interface XAIHandlerOptions {
|
||||
xaiApiKey?: string
|
||||
@@ -70,10 +71,13 @@ export class XAIHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
// @ts-ignore-next-line
|
||||
reasoning: delta.reasoning_content,
|
||||
// Skip reasoning content for Grok 4 models since it only displays "thinking" without providing useful information
|
||||
if (!shouldSkipReasoningForModel(modelId)) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
// @ts-ignore-next-line
|
||||
reasoning: delta.reasoning_content,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import * as path from "path"
|
||||
import { FileContextTracker } from "./FileContextTracker"
|
||||
import * as diskModule from "@core/storage/disk"
|
||||
import type { TaskMetadata, FileMetadataEntry } from "./ContextTrackerTypes"
|
||||
import type { WebviewProviderCreator } from "@/hosts/host-providers"
|
||||
import type { DiffViewProviderCreator, WebviewProviderCreator } from "@/hosts/host-providers"
|
||||
import * as hostProviders from "@hosts/host-providers"
|
||||
import { vscodeHostBridgeClient } from "@/hosts/vscode/client/host-grpc-client"
|
||||
|
||||
@@ -53,7 +53,11 @@ describe("FileContextTracker", () => {
|
||||
mockTaskMetadata = { files_in_context: [], model_usage: [] }
|
||||
getTaskMetadataStub = sandbox.stub(diskModule, "getTaskMetadata").resolves(mockTaskMetadata)
|
||||
saveTaskMetadataStub = sandbox.stub(diskModule, "saveTaskMetadata").resolves()
|
||||
hostProviders.initializeHostProviders(((_) => {}) as WebviewProviderCreator, vscodeHostBridgeClient)
|
||||
hostProviders.initializeHostProviders(
|
||||
((_) => {}) as WebviewProviderCreator,
|
||||
(() => {}) as DiffViewProviderCreator,
|
||||
vscodeHostBridgeClient,
|
||||
)
|
||||
|
||||
// Create tracker instance
|
||||
taskId = "test-task-id"
|
||||
|
||||
@@ -3,11 +3,12 @@ import { RuleFileRequest, RuleFile } from "@shared/proto/file"
|
||||
import { FileMethodHandler } from "./index"
|
||||
import { refreshClineRulesToggles } from "@core/context/instructions/user-instructions/cline-rules"
|
||||
import { createRuleFile as createRuleFileImpl } from "@core/context/instructions/user-instructions/rule-helpers"
|
||||
import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
import { handleFileServiceRequest } from "./index"
|
||||
import { refreshWorkflowToggles } from "@/core/context/instructions/user-instructions/workflows"
|
||||
import { getCwd, getDesktopDir } from "@/utils/path"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
|
||||
/**
|
||||
* Creates a rule file in either global or workspace rules directory
|
||||
@@ -42,7 +43,13 @@ export const createRuleFile: FileMethodHandler = async (controller: Controller,
|
||||
const fileTypeName = request.type === "workflow" ? "workflow" : "rule"
|
||||
|
||||
if (fileExists) {
|
||||
vscode.window.showWarningMessage(`${fileTypeName} file "${request.filename}" already exists.`)
|
||||
const message = `${fileTypeName} file "${request.filename}" already exists.`
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.WARNING,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
// Still open it for editing
|
||||
await handleFileServiceRequest(controller, "openFile", { value: filePath })
|
||||
} else {
|
||||
@@ -55,8 +62,12 @@ export const createRuleFile: FileMethodHandler = async (controller: Controller,
|
||||
|
||||
await handleFileServiceRequest(controller, "openFile", { value: filePath })
|
||||
|
||||
vscode.window.showInformationMessage(
|
||||
`Created new ${request.isGlobal ? "global" : "workspace"} ${fileTypeName} file: ${request.filename}`,
|
||||
const message = `Created new ${request.isGlobal ? "global" : "workspace"} ${fileTypeName} file: ${request.filename}`
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { deleteRuleFile as deleteRuleFileImpl } from "@core/context/instructions/user-instructions/rule-helpers"
|
||||
import { RuleFile, RuleFileRequest } from "@shared/proto/file"
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { Controller } from ".."
|
||||
import { FileMethodHandler } from "./index"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
|
||||
/**
|
||||
* Deletes a rule file from either global or workspace rules directory
|
||||
@@ -44,7 +45,13 @@ export const deleteRuleFile: FileMethodHandler = async (controller: Controller,
|
||||
|
||||
const fileTypeName = request.type === "workflow" ? "workflow" : "rule"
|
||||
|
||||
vscode.window.showInformationMessage(`${fileTypeName} file "${fileName}" deleted successfully`)
|
||||
const message = `${fileTypeName} file "${fileName}" deleted successfully`
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
|
||||
return RuleFile.create({
|
||||
filePath: request.rulePath,
|
||||
|
||||
@@ -27,20 +27,15 @@ import pWaitFor from "p-wait-for"
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { ensureMcpServersDirectoryExists, ensureSettingsDirectoryExists, GlobalFileNames } from "../storage/disk"
|
||||
import {
|
||||
getAllExtensionState,
|
||||
getGlobalState,
|
||||
getWorkspaceState,
|
||||
storeSecret,
|
||||
updateGlobalState,
|
||||
updateWorkspaceState,
|
||||
} from "../storage/state"
|
||||
import { getAllExtensionState, getGlobalState, getWorkspaceState, storeSecret, updateGlobalState } from "../storage/state"
|
||||
import { Task } from "../task"
|
||||
import { handleGrpcRequest, handleGrpcRequestCancel } from "./grpc-handler"
|
||||
import { sendStateUpdate } from "./state/subscribeToState"
|
||||
import { sendAddToInputEvent } from "./ui/subscribeToAddToInput"
|
||||
import { sendMcpMarketplaceCatalogEvent } from "./mcp/subscribeToMcpMarketplaceCatalog"
|
||||
import { AuthService } from "@/services/auth/AuthService"
|
||||
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
|
||||
/*
|
||||
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
||||
@@ -79,7 +74,7 @@ export class Controller {
|
||||
)
|
||||
this.accountService = ClineAccountService.getInstance()
|
||||
this.authService = AuthService.getInstance(context)
|
||||
this.authService.restoreAuthToken()
|
||||
this.authService.restoreRefreshTokenAndRetrieveAuthInfo()
|
||||
|
||||
// Clean up legacy checkpoints
|
||||
cleanupLegacyCheckpoints(this.context.globalStorageUri.fsPath, this.outputChannel).catch((error) => {
|
||||
@@ -118,9 +113,19 @@ export class Controller {
|
||||
await updateGlobalState(this.context, "userInfo", undefined)
|
||||
await updateGlobalState(this.context, "apiProvider", "openrouter")
|
||||
await this.postStateToWebview()
|
||||
vscode.window.showInformationMessage("Successfully logged out of Cline")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Successfully logged out of Cline",
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage("Logout failed")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Logout failed",
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -484,7 +489,12 @@ export class Controller {
|
||||
await this.postStateToWebview()
|
||||
} catch (error) {
|
||||
console.error("Failed to handle auth callback:", error)
|
||||
vscode.window.showErrorMessage("Failed to log in to Cline")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
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
|
||||
}
|
||||
@@ -519,7 +529,12 @@ 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"
|
||||
vscode.window.showErrorMessage(errorMessage)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: errorMessage,
|
||||
}),
|
||||
)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
@@ -603,7 +618,12 @@ 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"
|
||||
vscode.window.showErrorMessage(errorMessage)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: errorMessage,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -967,14 +987,24 @@ export class Controller {
|
||||
// Check if there's a workspace folder open
|
||||
const cwd = await getCwd()
|
||||
if (!cwd) {
|
||||
vscode.window.showErrorMessage("No workspace folder open")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
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") {
|
||||
vscode.window.showInformationMessage("No changes in workspace for commit message")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "No changes in workspace for commit message",
|
||||
}),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1041,25 +1071,59 @@ Commit message:`
|
||||
if (api && api.repositories.length > 0) {
|
||||
const repo = api.repositories[0]
|
||||
repo.inputBox.value = commitMessage
|
||||
vscode.window.showInformationMessage("Commit message generated and applied")
|
||||
const message = "Commit message generated and applied"
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
vscode.window.showErrorMessage("No Git repositories found")
|
||||
const message = "No Git repositories found"
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
vscode.window.showErrorMessage("Git extension not found")
|
||||
const message = "Git extension not found"
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
vscode.window.showErrorMessage("Failed to generate commit message")
|
||||
const message = "Failed to generate commit message"
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
}
|
||||
} catch (innerError) {
|
||||
const innerErrorMessage = innerError instanceof Error ? innerError.message : String(innerError)
|
||||
vscode.window.showErrorMessage(`Failed to generate commit message: ${innerErrorMessage}`)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Failed to generate commit message: ${innerErrorMessage}`,
|
||||
}),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
vscode.window.showErrorMessage(`Failed to generate commit message: ${errorMessage}`)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Failed to generate commit message: ${errorMessage}`,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@ import { Controller } from ".."
|
||||
import { Empty } from "../../../shared/proto/common"
|
||||
import { ResetStateRequest } from "../../../shared/proto/state"
|
||||
import { resetGlobalState, resetWorkspaceState } from "../../../core/storage/state"
|
||||
import * as vscode from "vscode"
|
||||
import { sendChatButtonClickedEvent } from "../ui/subscribeToChatButtonClicked"
|
||||
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
|
||||
/**
|
||||
* Resets the extension state to its defaults
|
||||
@@ -14,10 +15,20 @@ import { sendChatButtonClickedEvent } from "../ui/subscribeToChatButtonClicked"
|
||||
export async function resetState(controller: Controller, request: ResetStateRequest): Promise<Empty> {
|
||||
try {
|
||||
if (request.global) {
|
||||
vscode.window.showInformationMessage("Resetting global state...")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Resetting global state...",
|
||||
}),
|
||||
)
|
||||
await resetGlobalState(controller.context)
|
||||
} else {
|
||||
vscode.window.showInformationMessage("Resetting workspace state...")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Resetting workspace state...",
|
||||
}),
|
||||
)
|
||||
await resetWorkspaceState(controller.context)
|
||||
}
|
||||
|
||||
@@ -26,7 +37,12 @@ export async function resetState(controller: Controller, request: ResetStateRequ
|
||||
controller.task = undefined
|
||||
}
|
||||
|
||||
vscode.window.showInformationMessage("State reset")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "State reset",
|
||||
}),
|
||||
)
|
||||
await controller.postStateToWebview()
|
||||
|
||||
await sendChatButtonClickedEvent(controller.id)
|
||||
@@ -34,7 +50,12 @@ export async function resetState(controller: Controller, request: ResetStateRequ
|
||||
return Empty.create()
|
||||
} catch (error) {
|
||||
console.error("Error resetting state:", error)
|
||||
vscode.window.showErrorMessage(`Failed to reset state: ${error instanceof Error ? error.message : String(error)}`)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Failed to reset state: ${error instanceof Error ? error.message : String(error)}`,
|
||||
}),
|
||||
)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import * as vscode from "vscode"
|
||||
import { Controller } from "../index"
|
||||
import * as proto from "@/shared/proto"
|
||||
import { updateGlobalState } from "../../storage/state"
|
||||
import { TerminalInfo } from "@/integrations/terminal/TerminalRegistry"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
|
||||
export async function updateDefaultTerminalProfile(
|
||||
controller: Controller,
|
||||
@@ -25,16 +26,25 @@ export async function updateDefaultTerminalProfile(
|
||||
|
||||
// Show information message if terminals were closed
|
||||
if (closedCount > 0) {
|
||||
vscode.window.showInformationMessage(
|
||||
`Closed ${closedCount} ${closedCount === 1 ? "terminal" : "terminals"} with different profile.`,
|
||||
const message = `Closed ${closedCount} ${closedCount === 1 ? "terminal" : "terminals"} with different profile.`
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// Show warning if there are busy terminals that couldn't be closed
|
||||
if (busyTerminals.length > 0) {
|
||||
vscode.window.showWarningMessage(
|
||||
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.`,
|
||||
`Close ${busyTerminals.length === 1 ? "it" : "them"} to use the new profile for all commands.`
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.WARNING,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ import { Controller } from ".."
|
||||
import { DeleteAllTaskHistoryCount } from "../../../shared/proto/task"
|
||||
import { getGlobalState, updateGlobalState } from "../../storage/state"
|
||||
import { fileExistsAtPath } from "../../../utils/fs"
|
||||
import vscode from "vscode"
|
||||
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
|
||||
/**
|
||||
* Deletes all task history, with an option to preserve favorites
|
||||
@@ -21,12 +22,18 @@ export async function deleteAllTaskHistory(controller: Controller): Promise<Dele
|
||||
const taskHistory = ((await getGlobalState(controller.context, "taskHistory")) as any[]) || []
|
||||
const totalTasks = taskHistory.length
|
||||
|
||||
const userChoice = await vscode.window.showWarningMessage(
|
||||
"What would you like to delete?",
|
||||
{ modal: true },
|
||||
"Delete All Except Favorites",
|
||||
"Delete Everything",
|
||||
)
|
||||
const userChoice = (
|
||||
await getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.WARNING,
|
||||
message: "What would you like to delete?",
|
||||
options: {
|
||||
modal: true,
|
||||
items: ["Delete All Except Favorites", "Delete Everything"],
|
||||
},
|
||||
}),
|
||||
)
|
||||
)?.selectedOption
|
||||
|
||||
// Default VS Code Cancel button returns `undefined` - don't delete anything
|
||||
if (userChoice === undefined) {
|
||||
@@ -59,11 +66,18 @@ export async function deleteAllTaskHistory(controller: Controller): Promise<Dele
|
||||
})
|
||||
} else {
|
||||
// No favorited tasks found - show warning and ask user what to do
|
||||
const answer = await vscode.window.showWarningMessage(
|
||||
"No favorited tasks found. Would you like to delete all tasks anyway?",
|
||||
{ modal: true },
|
||||
"Delete All Tasks",
|
||||
)
|
||||
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
|
||||
|
||||
// User cancelled - don't delete anything
|
||||
if (answer === undefined) {
|
||||
@@ -91,8 +105,11 @@ export async function deleteAllTaskHistory(controller: Controller): Promise<Dele
|
||||
await fs.rm(checkpointsDirPath, { recursive: true, force: true })
|
||||
}
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(
|
||||
`Encountered error while deleting task history, there may be some files left behind. Error: ${error instanceof Error ? error.message : String(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)}`,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import vscode from "vscode"
|
||||
import { Controller } from ".."
|
||||
import { Empty, StringArrayRequest, BooleanRequest } from "../../../shared/proto/common"
|
||||
import { Empty, StringArrayRequest } from "../../../shared/proto/common"
|
||||
import { TaskMethodHandler } from "./index"
|
||||
import { fileExistsAtPath } from "../../../utils/fs"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
|
||||
/**
|
||||
* Deletes tasks with the specified IDs
|
||||
@@ -27,7 +28,13 @@ 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 vscode.window.showWarningMessage(message, { modal: true }, "Delete")
|
||||
const userChoice = await getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.WARNING,
|
||||
message,
|
||||
options: { modal: true, items: ["Delete"] },
|
||||
}),
|
||||
)
|
||||
|
||||
if (userChoice === undefined) {
|
||||
return Empty.create()
|
||||
|
||||
@@ -13,6 +13,8 @@ import { getWorkingState } from "@utils/git"
|
||||
import { FileContextTracker } from "../context/context-tracking/FileContextTracker"
|
||||
import { getCwd } from "@/utils/path"
|
||||
import { openExternal } from "@utils/env"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
|
||||
export async function openMention(mention?: string): Promise<void> {
|
||||
if (!mention) {
|
||||
@@ -76,7 +78,12 @@ export async function parseMentions(
|
||||
await urlContentFetcher.launchBrowser()
|
||||
} catch (error) {
|
||||
launchBrowserError = error
|
||||
vscode.window.showErrorMessage(`Error fetching content for ${urlMention}: ${error.message}`)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Error fetching content for ${urlMention}: ${error.message}`,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +100,12 @@ export async function parseMentions(
|
||||
const markdown = await urlContentFetcher.urlToMarkdown(mention)
|
||||
result = markdown
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(`Error fetching content for ${mention}: ${error.message}`)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Error fetching content for ${mention}: ${error.message}`,
|
||||
}),
|
||||
)
|
||||
result = `Error fetching content: ${error.message}`
|
||||
}
|
||||
}
|
||||
@@ -120,7 +132,7 @@ export async function parseMentions(
|
||||
}
|
||||
} else if (mention === "problems") {
|
||||
try {
|
||||
const problems = getWorkspaceProblems(cwd)
|
||||
const problems = await getWorkspaceProblems()
|
||||
parsedText += `\n\n<workspace_diagnostics>\n${problems}\n</workspace_diagnostics>`
|
||||
} catch (error) {
|
||||
parsedText += `\n\n<workspace_diagnostics>\nError fetching diagnostics: ${error.message}\n</workspace_diagnostics>`
|
||||
@@ -216,13 +228,9 @@ async function getFileOrFolderContent(mentionPath: string, cwd: string): Promise
|
||||
}
|
||||
}
|
||||
|
||||
function getWorkspaceProblems(cwd: string): string {
|
||||
async function getWorkspaceProblems(): Promise<string> {
|
||||
const diagnostics = vscode.languages.getDiagnostics()
|
||||
const result = diagnosticsToProblemsString(
|
||||
diagnostics,
|
||||
[vscode.DiagnosticSeverity.Error, vscode.DiagnosticSeverity.Warning],
|
||||
cwd,
|
||||
)
|
||||
const result = diagnosticsToProblemsString(diagnostics, [vscode.DiagnosticSeverity.Error, vscode.DiagnosticSeverity.Warning])
|
||||
if (!result) {
|
||||
return "No errors or warnings detected."
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ import { MessageStateHandler } from "./message-state"
|
||||
import { TaskState } from "./TaskState"
|
||||
import { ToolExecutor } from "./ToolExecutor"
|
||||
import { formatErrorWithStatusCode, updateApiReqMsg } from "./utils"
|
||||
import { createDiffViewProvider, getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
|
||||
export const USE_EXPERIMENTAL_CLAUDE4_FEATURES = false
|
||||
|
||||
@@ -174,7 +175,7 @@ export class Task {
|
||||
this.urlContentFetcher = new UrlContentFetcher(context)
|
||||
this.browserSession = new BrowserSession(context, browserSettings)
|
||||
this.contextManager = new ContextManager()
|
||||
this.diffViewProvider = new DiffViewProvider(cwd)
|
||||
this.diffViewProvider = createDiffViewProvider()
|
||||
this.autoApprovalSettings = autoApprovalSettings
|
||||
this.browserSettings = browserSettings
|
||||
this.chatSettings = chatSettings
|
||||
@@ -1723,7 +1724,7 @@ export class Task {
|
||||
await this.messageStateHandler.updateClineMessage(lastApiReqStartedIndex, {
|
||||
text: JSON.stringify({
|
||||
...currentApiReqInfo, // Spread the modified info (with retryStatus removed)
|
||||
cancelReason: "retries_exhausted", // Indicate that automatic retries failed
|
||||
// cancelReason: "retries_exhausted", // Indicate that automatic retries failed
|
||||
streamingFailedMessage: errorMessage,
|
||||
} satisfies ClineApiReqInfo),
|
||||
})
|
||||
|
||||
@@ -10,6 +10,8 @@ import path from "node:path"
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
import { Uri } from "vscode"
|
||||
import { ExtensionMessage } from "@/shared/ExtensionMessage"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
|
||||
export abstract class WebviewProvider {
|
||||
public static readonly sideBarId = "claude-dev.SidebarProvider" // used in package.json as the view's id. This value cannot be changed due to how vscode caches views based on their id, and updating the id would break existing instances of the extension.
|
||||
@@ -260,8 +262,12 @@ export abstract class WebviewProvider {
|
||||
try {
|
||||
await axios.get(`http://${localServerUrl}`)
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(
|
||||
"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(
|
||||
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.",
|
||||
}),
|
||||
)
|
||||
|
||||
return this.getHtmlContent()
|
||||
|
||||
@@ -4,6 +4,8 @@ import * as path from "path"
|
||||
import { Controller } from "@core/controller"
|
||||
import { HistoryItem } from "@shared/HistoryItem"
|
||||
import { ClineMessage } from "@shared/ExtensionMessage"
|
||||
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
|
||||
/**
|
||||
* Registers development-only commands for task manipulation.
|
||||
@@ -96,7 +98,13 @@ export function registerTaskCommands(context: vscode.ExtensionContext, controlle
|
||||
// Update the UI to show the new tasks
|
||||
await controller.postStateToWebview()
|
||||
|
||||
vscode.window.showInformationMessage(`Created ${tasksCount} test tasks`)
|
||||
const message = `Created ${tasksCount} test tasks`
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
},
|
||||
)
|
||||
}),
|
||||
|
||||
+52
-13
@@ -37,7 +37,9 @@ import { VscodeWebviewProvider } from "./core/webview/VscodeWebviewProvider"
|
||||
import { ExtensionContext } from "vscode"
|
||||
import { AuthService } from "./services/auth/AuthService"
|
||||
import { writeTextToClipboard, readTextFromClipboard } from "@/utils/env"
|
||||
|
||||
import { VscodeDiffViewProvider } from "./integrations/editor/VscodeDiffViewProvider"
|
||||
import { getHostBridgeProvider } from "@hosts/host-providers"
|
||||
import { ShowMessageRequest, ShowMessageType } from "./shared/proto/host/window"
|
||||
/*
|
||||
Built using https://github.com/microsoft/vscode-webview-ui-toolkit
|
||||
|
||||
@@ -104,7 +106,12 @@ 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))
|
||||
vscode.window.showInformationMessage(message)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
// Record that we've shown the popup for this version.
|
||||
await context.globalState.update("clineLastPopupNotificationVersion", currentVersion)
|
||||
}
|
||||
@@ -310,11 +317,14 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
// Ask user to confirm on state mismatch. This enables signins initiated from
|
||||
// outside the extension (e.g. Cline web) to be handled correctly.
|
||||
if (authService.authNonce !== state) {
|
||||
const userConfirmation = await vscode.window.showWarningMessage(
|
||||
`Store token returned from ${uri.path}`,
|
||||
"Store",
|
||||
"Cancel",
|
||||
)
|
||||
const userConfirmation = (
|
||||
await getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Invalid auth state",
|
||||
}),
|
||||
)
|
||||
)?.selectedOption
|
||||
if (userConfirmation === "Cancel") {
|
||||
console.log("User declined to continue with auth callback due to state mismatch")
|
||||
return
|
||||
@@ -426,7 +436,12 @@ 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)
|
||||
vscode.window.showErrorMessage("Failed to get terminal contents")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Failed to get terminal contents",
|
||||
}),
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
@@ -562,7 +577,12 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
}
|
||||
const selectedText = editor.document.getText(range)
|
||||
if (!selectedText.trim()) {
|
||||
vscode.window.showInformationMessage("Please select some code to explain.")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Please select some code to explain.",
|
||||
}),
|
||||
)
|
||||
return
|
||||
}
|
||||
const filePath = editor.document.uri.fsPath
|
||||
@@ -584,7 +604,12 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
}
|
||||
const selectedText = editor.document.getText(range)
|
||||
if (!selectedText.trim()) {
|
||||
vscode.window.showInformationMessage("Please select some code to improve.")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Please select some code to improve.",
|
||||
}),
|
||||
)
|
||||
return
|
||||
}
|
||||
const filePath = editor.document.uri.fsPath
|
||||
@@ -649,8 +674,11 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
sendFocusChatInputEvent(clientId)
|
||||
} else {
|
||||
console.error("FocusChatInput: Could not find or activate a Cline webview to focus.")
|
||||
vscode.window.showErrorMessage(
|
||||
"Could not activate Cline view. Please try opening it manually from the Activity Bar.",
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
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, true)
|
||||
@@ -685,6 +713,14 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
}),
|
||||
)
|
||||
|
||||
context.subscriptions.push(
|
||||
context.secrets.onDidChange((event) => {
|
||||
if (event.key === "clineAccountId") {
|
||||
AuthService.getInstance(context)?.restoreRefreshTokenAndRetrieveAuthInfo()
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
return createClineAPI(outputChannel, sidebarWebview.controller)
|
||||
}
|
||||
|
||||
@@ -694,7 +730,10 @@ function maybeSetupHostProviders(context: ExtensionContext) {
|
||||
const createWebview = function (type: WebviewProviderType) {
|
||||
return new VscodeWebviewProvider(context, outputChannel, type)
|
||||
}
|
||||
hostProviders.initializeHostProviders(createWebview, vscodeHostBridgeClient)
|
||||
const createDiffView = function () {
|
||||
return new VscodeDiffViewProvider()
|
||||
}
|
||||
hostProviders.initializeHostProviders(createWebview, createDiffView, vscodeHostBridgeClient)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
WorkspaceServiceClientInterface,
|
||||
EnvServiceClientInterface,
|
||||
WindowServiceClientInterface,
|
||||
DiffServiceClientInterface,
|
||||
} from "@generated/hosts/host-bridge-client-types"
|
||||
|
||||
/**
|
||||
@@ -13,6 +14,7 @@ export interface HostBridgeClientProvider {
|
||||
workspaceClient: WorkspaceServiceClientInterface
|
||||
envClient: EnvServiceClientInterface
|
||||
windowClient: WindowServiceClientInterface
|
||||
diffClient: DiffServiceClientInterface
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
import { WebviewProvider } from "@core/webview"
|
||||
import { HostBridgeClientProvider } from "./host-provider-types"
|
||||
import { WebviewProviderType } from "@/shared/webview/types"
|
||||
import { DiffViewProvider } from "@/integrations/editor/DiffViewProvider"
|
||||
|
||||
/**
|
||||
* A function that creates WebviewProvider instances
|
||||
*/
|
||||
export type WebviewProviderCreator = (providerType: WebviewProviderType) => WebviewProvider
|
||||
|
||||
export type DiffViewProviderCreator = () => DiffViewProvider
|
||||
|
||||
let _webviewProviderCreator: WebviewProviderCreator | undefined
|
||||
let _diffViewProviderCreator: DiffViewProviderCreator | undefined
|
||||
let _hostBridgeProvider: HostBridgeClientProvider | undefined
|
||||
|
||||
export var isSetup: boolean = false
|
||||
|
||||
export function initializeHostProviders(
|
||||
webviewProviderCreator: WebviewProviderCreator,
|
||||
diffViewProviderCreator: DiffViewProviderCreator,
|
||||
hostBridgeProvider: HostBridgeClientProvider,
|
||||
) {
|
||||
_webviewProviderCreator = webviewProviderCreator
|
||||
_diffViewProviderCreator = diffViewProviderCreator
|
||||
_hostBridgeProvider = hostBridgeProvider
|
||||
isSetup = true
|
||||
}
|
||||
@@ -28,6 +34,13 @@ export function createWebviewProvider(providerType: WebviewProviderType): Webvie
|
||||
return _webviewProviderCreator(providerType)
|
||||
}
|
||||
|
||||
export function createDiffViewProvider(): DiffViewProvider {
|
||||
if (!_diffViewProviderCreator) {
|
||||
throw Error("Host providers not initialized")
|
||||
}
|
||||
return _diffViewProviderCreator()
|
||||
}
|
||||
|
||||
export function getHostBridgeProvider(): HostBridgeClientProvider {
|
||||
if (!_hostBridgeProvider) {
|
||||
throw Error("Host providers not initialized")
|
||||
|
||||
@@ -7,4 +7,5 @@ export const vscodeHostBridgeClient: HostBridgeClientProvider = {
|
||||
workspaceClient: createGrpcClient(host.WorkspaceServiceDefinition),
|
||||
envClient: createGrpcClient(host.EnvServiceDefinition),
|
||||
windowClient: createGrpcClient(host.WindowServiceDefinition),
|
||||
diffClient: createGrpcClient(host.DiffServiceDefinition),
|
||||
}
|
||||
|
||||
@@ -0,0 +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.")
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { window } from "vscode"
|
||||
import { SelectedResponse, ShowMessageRequest, ShowMessageType } from "@/shared/proto/index.host"
|
||||
|
||||
export async function showMessage(request: ShowMessageRequest): Promise<SelectedResponse | undefined> {
|
||||
const { message, type, options } = request
|
||||
const { modal, detail, items } = options || {}
|
||||
const option = items ? { modal, items } : { modal, detail }
|
||||
|
||||
let selectedOption: string | undefined = undefined
|
||||
|
||||
switch (type) {
|
||||
case ShowMessageType.ERROR:
|
||||
selectedOption = await window.showErrorMessage(message, option)
|
||||
break
|
||||
case ShowMessageType.WARNING:
|
||||
selectedOption = await window.showWarningMessage(message, option)
|
||||
break
|
||||
default:
|
||||
selectedOption = await window.showInformationMessage(message, option)
|
||||
break
|
||||
}
|
||||
|
||||
return SelectedResponse.create({ selectedOption })
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
import deepEqual from "fast-deep-equal"
|
||||
import { getCwd } from "@/utils/path"
|
||||
|
||||
export function getNewDiagnostics(
|
||||
oldDiagnostics: [vscode.Uri, vscode.Diagnostic[]][],
|
||||
@@ -70,11 +71,11 @@ export function getNewDiagnostics(
|
||||
// // - New error in file3 (1:1)
|
||||
|
||||
// will return empty string if no problems with the given severity are found
|
||||
export function diagnosticsToProblemsString(
|
||||
export async function diagnosticsToProblemsString(
|
||||
diagnostics: [vscode.Uri, vscode.Diagnostic[]][],
|
||||
severities: vscode.DiagnosticSeverity[],
|
||||
cwd: string,
|
||||
): string {
|
||||
): Promise<string> {
|
||||
const cwd = await getCwd()
|
||||
let result = ""
|
||||
for (const [uri, fileDiagnostics] of diagnostics) {
|
||||
const problems = fileDiagnostics.filter((d) => severities.includes(d.severity))
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
import * as fs from "fs/promises"
|
||||
import { createDirectoriesForFile } from "@utils/fs"
|
||||
import { arePathsEqual } from "@utils/path"
|
||||
import { arePathsEqual, getCwd } from "@utils/path"
|
||||
import { formatResponse } from "@core/prompts/responses"
|
||||
import { DecorationController } from "./DecorationController"
|
||||
import * as diff from "diff"
|
||||
@@ -10,46 +10,45 @@ 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, TextEditorInfo } from "@/shared/proto/host/window"
|
||||
import { ShowTextDocumentRequest, ShowTextDocumentOptions } from "@/shared/proto/host/window"
|
||||
|
||||
export const DIFF_VIEW_URI_SCHEME = "cline-diff"
|
||||
|
||||
export class DiffViewProvider {
|
||||
export abstract class DiffViewProvider {
|
||||
editType?: "create" | "modify"
|
||||
isEditing = false
|
||||
originalContent: string | undefined
|
||||
private createdDirs: string[] = []
|
||||
private documentWasOpen = false
|
||||
private relPath?: string
|
||||
private newContent?: string
|
||||
private activeDiffEditor?: vscode.TextEditor
|
||||
private fadedOverlayController?: DecorationController
|
||||
private activeLineController?: DecorationController
|
||||
protected documentWasOpen = false
|
||||
protected relPath?: string
|
||||
protected absolutePath?: string
|
||||
protected fileEncoding: string = "utf8"
|
||||
private streamedLines: string[] = []
|
||||
private preDiagnostics: [vscode.Uri, vscode.Diagnostic[]][] = []
|
||||
private fileEncoding: string = "utf8"
|
||||
private scrollListener?: vscode.Disposable
|
||||
private newContent?: string
|
||||
|
||||
constructor(private cwd: string) {}
|
||||
protected activeDiffEditor?: vscode.TextEditor
|
||||
protected fadedOverlayController?: DecorationController
|
||||
protected activeLineController?: DecorationController
|
||||
protected preDiagnostics: [vscode.Uri, vscode.Diagnostic[]][] = []
|
||||
|
||||
async open(relPath: string): Promise<void> {
|
||||
this.relPath = relPath
|
||||
const fileExists = this.editType === "modify"
|
||||
const absolutePath = path.resolve(this.cwd, relPath)
|
||||
constructor() {}
|
||||
|
||||
public async open(relPath: string): Promise<void> {
|
||||
this.isEditing = true
|
||||
this.relPath = relPath
|
||||
this.absolutePath = path.resolve(await getCwd(), relPath)
|
||||
const fileExists = this.editType === "modify"
|
||||
|
||||
// if the file is already open, ensure it's not dirty before getting its contents
|
||||
if (fileExists) {
|
||||
const existingDocument = vscode.workspace.textDocuments.find((doc) => arePathsEqual(doc.uri.fsPath, absolutePath))
|
||||
const existingDocument = vscode.workspace.textDocuments.find((doc) =>
|
||||
arePathsEqual(doc.uri.fsPath, this.absolutePath),
|
||||
)
|
||||
if (existingDocument && existingDocument.isDirty) {
|
||||
await existingDocument.save()
|
||||
}
|
||||
}
|
||||
|
||||
// get diagnostics before editing the file, we'll compare to diagnostics after editing to see if cline needs to fix anything
|
||||
this.preDiagnostics = vscode.languages.getDiagnostics()
|
||||
|
||||
if (fileExists) {
|
||||
const fileBuffer = await fs.readFile(absolutePath)
|
||||
const fileBuffer = await fs.readFile(this.absolutePath)
|
||||
this.fileEncoding = await detectEncoding(fileBuffer)
|
||||
this.originalContent = iconv.decode(fileBuffer, this.fileEncoding)
|
||||
} else {
|
||||
@@ -57,33 +56,30 @@ export class DiffViewProvider {
|
||||
this.fileEncoding = "utf8"
|
||||
}
|
||||
// for new files, create any necessary directories and keep track of new directories to delete if the user denies the operation
|
||||
this.createdDirs = await createDirectoriesForFile(absolutePath)
|
||||
this.createdDirs = await createDirectoriesForFile(this.absolutePath)
|
||||
// make sure the file exists before we open it
|
||||
if (!fileExists) {
|
||||
await fs.writeFile(absolutePath, "")
|
||||
await fs.writeFile(this.absolutePath, "")
|
||||
}
|
||||
// if the file was already open, close it (must happen after showing the diff view since if it's the only tab the column will close)
|
||||
this.documentWasOpen = false
|
||||
// close the tab if it's open (it's already saved above)
|
||||
const tabs = vscode.window.tabGroups.all
|
||||
.map((tg) => tg.tabs)
|
||||
.flat()
|
||||
.filter((tab) => tab.input instanceof vscode.TabInputText && arePathsEqual(tab.input.uri.fsPath, absolutePath))
|
||||
for (const tab of tabs) {
|
||||
if (!tab.isDirty) {
|
||||
await vscode.window.tabGroups.close(tab)
|
||||
}
|
||||
this.documentWasOpen = true
|
||||
}
|
||||
this.activeDiffEditor = await this.openDiffEditor()
|
||||
this.fadedOverlayController = new DecorationController("fadedOverlay", this.activeDiffEditor)
|
||||
this.activeLineController = new DecorationController("activeLine", this.activeDiffEditor)
|
||||
// Apply faded overlay to all lines initially
|
||||
this.fadedOverlayController.addLines(0, this.activeDiffEditor.document.lineCount)
|
||||
await this.openDiffEditor()
|
||||
this.scrollEditorToLine(0) // will this crash for new files?
|
||||
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.
|
||||
*
|
||||
* @returns A promise that resolves when the diff editor is open and ready
|
||||
*/
|
||||
protected abstract openDiffEditor(): Promise<void>
|
||||
|
||||
async update(
|
||||
accumulatedContent: string,
|
||||
isFinal: boolean,
|
||||
@@ -125,15 +121,9 @@ export class DiffViewProvider {
|
||||
|
||||
// Replace all content up to the current line with accumulated lines
|
||||
// This is necessary (as compared to inserting one line at a time) to handle cases where html tags on previous lines are auto closed for example
|
||||
const edit = new vscode.WorkspaceEdit()
|
||||
const rangeToReplace = new vscode.Range(0, 0, currentLine + 1, 0)
|
||||
const contentToReplace = accumulatedLines.slice(0, currentLine + 1).join("\n") + "\n"
|
||||
edit.replace(document.uri, rangeToReplace, contentToReplace)
|
||||
await vscode.workspace.applyEdit(edit)
|
||||
|
||||
// Update decorations for the entire changed section
|
||||
this.activeLineController.setActiveLine(currentLine)
|
||||
this.fadedOverlayController.updateOverlayAfterLine(currentLine, document.lineCount)
|
||||
const rangeToReplace = { startLine: 0, endLine: currentLine + 1 }
|
||||
await this.replaceText(contentToReplace, rangeToReplace, currentLine)
|
||||
|
||||
// Scroll to the actual change location if provided.
|
||||
if (changeLocation) {
|
||||
@@ -190,6 +180,24 @@ export class DiffViewProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces text in the diff editor with the specified content.
|
||||
*
|
||||
* This abstract method must be implemented by subclasses to handle the actual
|
||||
* text replacement in their specific diff editor implementation. It's called
|
||||
* during the streaming update process to progressively show changes.
|
||||
*
|
||||
* @param content The new content to insert into the document
|
||||
* @param rangeToReplace An object specifying the line range to replace
|
||||
* @param currentLine The current line number being edited, used for scroll positioning
|
||||
* @returns A promise that resolves when the text replacement is complete
|
||||
*/
|
||||
abstract replaceText(
|
||||
content: string,
|
||||
rangeToReplace: { startLine: number; endLine: number },
|
||||
currentLine: number,
|
||||
): Promise<void>
|
||||
|
||||
async saveChanges(): Promise<{
|
||||
newProblemsMessage: string | undefined
|
||||
userEdits: string | undefined
|
||||
@@ -204,7 +212,6 @@ export class DiffViewProvider {
|
||||
finalContent: undefined,
|
||||
}
|
||||
}
|
||||
const absolutePath = path.resolve(this.cwd, this.relPath)
|
||||
const updatedDocument = this.activeDiffEditor.document
|
||||
|
||||
// get the contents before save operation which may do auto-formatting
|
||||
@@ -219,7 +226,7 @@ export class DiffViewProvider {
|
||||
|
||||
await getHostBridgeProvider().windowClient.showTextDocument(
|
||||
ShowTextDocumentRequest.create({
|
||||
path: absolutePath,
|
||||
path: this.absolutePath,
|
||||
options: ShowTextDocumentOptions.create({
|
||||
preview: false,
|
||||
preserveFocus: true,
|
||||
@@ -246,13 +253,9 @@ export class DiffViewProvider {
|
||||
initial fix is usually correct and it may just take time for linters to catch up.
|
||||
*/
|
||||
const postDiagnostics = vscode.languages.getDiagnostics()
|
||||
const newProblems = diagnosticsToProblemsString(
|
||||
getNewDiagnostics(this.preDiagnostics, postDiagnostics),
|
||||
[
|
||||
vscode.DiagnosticSeverity.Error, // only including errors since warnings can be distracting (if user wants to fix warnings they can use the @problems mention)
|
||||
],
|
||||
this.cwd,
|
||||
) // will be empty string if no errors
|
||||
const newProblems = await diagnosticsToProblemsString(getNewDiagnostics(this.preDiagnostics, postDiagnostics), [
|
||||
vscode.DiagnosticSeverity.Error, // only including errors since warnings can be distracting (if user wants to fix warnings they can use the @problems mention)
|
||||
]) // will be empty string if no errors
|
||||
const newProblemsMessage =
|
||||
newProblems.length > 0 ? `\n\nNew problems detected after saving the file:\n${newProblems}` : ""
|
||||
|
||||
@@ -292,24 +295,23 @@ export class DiffViewProvider {
|
||||
}
|
||||
|
||||
async revertChanges(): Promise<void> {
|
||||
if (!this.relPath || !this.activeDiffEditor) {
|
||||
if (!this.absolutePath || !this.activeDiffEditor) {
|
||||
return
|
||||
}
|
||||
const fileExists = this.editType === "modify"
|
||||
const updatedDocument = this.activeDiffEditor.document
|
||||
const absolutePath = path.resolve(this.cwd, this.relPath)
|
||||
if (!fileExists) {
|
||||
if (updatedDocument.isDirty) {
|
||||
await updatedDocument.save()
|
||||
}
|
||||
await this.closeAllDiffViews()
|
||||
await fs.unlink(absolutePath)
|
||||
await fs.unlink(this.absolutePath)
|
||||
// Remove only the directories we created, in reverse order
|
||||
for (let i = this.createdDirs.length - 1; i >= 0; i--) {
|
||||
await fs.rmdir(this.createdDirs[i])
|
||||
console.log(`Directory ${this.createdDirs[i]} has been deleted.`)
|
||||
}
|
||||
console.log(`File ${absolutePath} has been deleted.`)
|
||||
console.log(`File ${this.absolutePath} has been deleted.`)
|
||||
} else {
|
||||
// revert document
|
||||
const edit = new vscode.WorkspaceEdit()
|
||||
@@ -321,11 +323,11 @@ export class DiffViewProvider {
|
||||
// Apply the edit and save, since contents shouldn't have changed this won't show in local history unless of course the user made changes and saved during the edit
|
||||
await vscode.workspace.applyEdit(edit)
|
||||
await updatedDocument.save()
|
||||
console.log(`File ${absolutePath} has been reverted to its original content.`)
|
||||
console.log(`File ${this.absolutePath} has been reverted to its original content.`)
|
||||
if (this.documentWasOpen) {
|
||||
await getHostBridgeProvider().windowClient.showTextDocument(
|
||||
ShowTextDocumentRequest.create({
|
||||
path: absolutePath,
|
||||
path: this.absolutePath,
|
||||
options: ShowTextDocumentOptions.create({
|
||||
preview: false,
|
||||
preserveFocus: true,
|
||||
@@ -352,65 +354,6 @@ export class DiffViewProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private async openDiffEditor(): Promise<vscode.TextEditor> {
|
||||
if (!this.relPath) {
|
||||
throw new Error("No file path set")
|
||||
}
|
||||
const uri = vscode.Uri.file(path.resolve(this.cwd, this.relPath))
|
||||
// If this diff editor is already open (ie if a previous write file was interrupted) then we should activate that instead of opening a new diff
|
||||
const diffTab = vscode.window.tabGroups.all
|
||||
.flatMap((group) => group.tabs)
|
||||
.find(
|
||||
(tab) =>
|
||||
tab.input instanceof vscode.TabInputTextDiff &&
|
||||
tab.input?.original?.scheme === DIFF_VIEW_URI_SCHEME &&
|
||||
arePathsEqual(tab.input.modified.fsPath, uri.fsPath),
|
||||
)
|
||||
if (diffTab && diffTab.input instanceof vscode.TabInputTextDiff) {
|
||||
const editorInfo = await getHostBridgeProvider().windowClient.showTextDocument(
|
||||
ShowTextDocumentRequest.create({
|
||||
path: diffTab.input.modified.fsPath,
|
||||
options: ShowTextDocumentOptions.create({
|
||||
preserveFocus: true,
|
||||
}),
|
||||
}),
|
||||
)
|
||||
// Find the editor that matches the returned path
|
||||
const editor = vscode.window.visibleTextEditors.find((e) => e.document.uri.fsPath === editorInfo.documentPath)
|
||||
if (!editor) {
|
||||
throw new Error("Failed to find opened text editor")
|
||||
}
|
||||
return editor
|
||||
}
|
||||
// Open new diff editor
|
||||
return new Promise<vscode.TextEditor>((resolve, reject) => {
|
||||
const fileName = path.basename(uri.fsPath)
|
||||
const fileExists = this.editType === "modify"
|
||||
const disposable = vscode.window.onDidChangeActiveTextEditor((editor) => {
|
||||
if (editor && arePathsEqual(editor.document.uri.fsPath, uri.fsPath)) {
|
||||
disposable.dispose()
|
||||
resolve(editor)
|
||||
}
|
||||
})
|
||||
vscode.commands.executeCommand(
|
||||
"vscode.diff",
|
||||
vscode.Uri.parse(`${DIFF_VIEW_URI_SCHEME}:${fileName}`).with({
|
||||
query: Buffer.from(this.originalContent ?? "").toString("base64"),
|
||||
}),
|
||||
uri,
|
||||
`${fileName}: ${fileExists ? "Original ↔ Cline's Changes" : "New File"} (Editable)`,
|
||||
{
|
||||
preserveFocus: true,
|
||||
},
|
||||
)
|
||||
// This may happen on very slow machines ie project idx
|
||||
setTimeout(() => {
|
||||
disposable.dispose()
|
||||
reject(new Error("Failed to open diff editor, please try again..."))
|
||||
}, 10_000)
|
||||
})
|
||||
}
|
||||
|
||||
private scrollEditorToLine(line: number) {
|
||||
if (this.activeDiffEditor) {
|
||||
const scrollLine = line + 4
|
||||
@@ -455,11 +398,5 @@ export class DiffViewProvider {
|
||||
this.activeLineController = undefined
|
||||
this.streamedLines = []
|
||||
this.preDiagnostics = []
|
||||
|
||||
// Clean up the scroll listener
|
||||
if (this.scrollListener) {
|
||||
this.scrollListener.dispose()
|
||||
this.scrollListener = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { arePathsEqual } from "@/utils/path"
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { DecorationController } from "./DecorationController"
|
||||
import { DIFF_VIEW_URI_SCHEME, DiffViewProvider } from "./DiffViewProvider"
|
||||
|
||||
export class VscodeDiffViewProvider extends DiffViewProvider {
|
||||
override async openDiffEditor(): Promise<void> {
|
||||
if (!this.absolutePath) {
|
||||
throw new Error("No file path set")
|
||||
}
|
||||
// get diagnostics before editing the file, we'll compare to diagnostics after editing to see if cline needs to fix anything
|
||||
this.preDiagnostics = vscode.languages.getDiagnostics()
|
||||
|
||||
// if the file was already open, close it (must happen after showing the diff view since if it's the only tab the column will close)
|
||||
this.documentWasOpen = false
|
||||
// close the tab if it's open (it's already been saved)
|
||||
const tabs = vscode.window.tabGroups.all
|
||||
.map((tg) => tg.tabs)
|
||||
.flat()
|
||||
.filter((tab) => tab.input instanceof vscode.TabInputText && arePathsEqual(tab.input.uri.fsPath, this.absolutePath))
|
||||
for (const tab of tabs) {
|
||||
if (!tab.isDirty) {
|
||||
await vscode.window.tabGroups.close(tab)
|
||||
}
|
||||
this.documentWasOpen = true
|
||||
}
|
||||
|
||||
const uri = vscode.Uri.file(this.absolutePath)
|
||||
// If this diff editor is already open (ie if a previous write file was interrupted) then we should activate that instead of opening a new diff
|
||||
const diffTab = vscode.window.tabGroups.all
|
||||
.flatMap((group) => group.tabs)
|
||||
.find(
|
||||
(tab) =>
|
||||
tab.input instanceof vscode.TabInputTextDiff &&
|
||||
tab.input?.original?.scheme === DIFF_VIEW_URI_SCHEME &&
|
||||
arePathsEqual(tab.input.modified.fsPath, uri.fsPath),
|
||||
)
|
||||
|
||||
if (diffTab && diffTab.input instanceof vscode.TabInputTextDiff) {
|
||||
// Use already open diff editor.
|
||||
this.activeDiffEditor = await vscode.window.showTextDocument(diffTab.input.modified, {
|
||||
preserveFocus: true,
|
||||
})
|
||||
} else {
|
||||
// Open new diff editor.
|
||||
this.activeDiffEditor = await new Promise<vscode.TextEditor>((resolve, reject) => {
|
||||
const fileName = path.basename(uri.fsPath)
|
||||
const fileExists = this.editType === "modify"
|
||||
const disposable = vscode.window.onDidChangeActiveTextEditor((editor) => {
|
||||
if (editor && arePathsEqual(editor.document.uri.fsPath, uri.fsPath)) {
|
||||
disposable.dispose()
|
||||
resolve(editor)
|
||||
}
|
||||
})
|
||||
vscode.commands.executeCommand(
|
||||
"vscode.diff",
|
||||
vscode.Uri.parse(`${DIFF_VIEW_URI_SCHEME}:${fileName}`).with({
|
||||
query: Buffer.from(this.originalContent ?? "").toString("base64"),
|
||||
}),
|
||||
uri,
|
||||
`${fileName}: ${fileExists ? "Original ↔ Cline's Changes" : "New File"} (Editable)`,
|
||||
{
|
||||
preserveFocus: true,
|
||||
},
|
||||
)
|
||||
// This may happen on very slow machines ie project idx
|
||||
setTimeout(() => {
|
||||
disposable.dispose()
|
||||
reject(new Error("Failed to open diff editor, please try again..."))
|
||||
}, 10_000)
|
||||
})
|
||||
}
|
||||
|
||||
this.fadedOverlayController = new DecorationController("fadedOverlay", this.activeDiffEditor)
|
||||
this.activeLineController = new DecorationController("activeLine", this.activeDiffEditor)
|
||||
// Apply faded overlay to all lines initially
|
||||
this.fadedOverlayController.addLines(0, this.activeDiffEditor.document.lineCount)
|
||||
}
|
||||
|
||||
override async replaceText(
|
||||
content: string,
|
||||
rangeToReplace: { startLine: number; endLine: number },
|
||||
currentLine: number,
|
||||
): Promise<void> {
|
||||
const document = this.activeDiffEditor?.document
|
||||
if (!document) {
|
||||
throw new Error("User closed text editor, unable to edit file...")
|
||||
}
|
||||
|
||||
const edit = new vscode.WorkspaceEdit()
|
||||
const range = new vscode.Range(rangeToReplace.startLine, 0, rangeToReplace.endLine, 0)
|
||||
edit.replace(document.uri, range, content)
|
||||
await vscode.workspace.applyEdit(edit)
|
||||
|
||||
// Update decorations for the entire changed section
|
||||
this.activeLineController?.setActiveLine(currentLine)
|
||||
this.fadedOverlayController?.updateOverlayAfterLine(currentLine, document.lineCount)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
import * as vscode from "vscode"
|
||||
import { getWorkingState } from "@utils/git"
|
||||
import { writeTextToClipboard } from "@utils/env"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { ShowTextDocumentRequest } from "@/shared/proto/host/window"
|
||||
|
||||
import { ShowMessageType, ShowTextDocumentRequest, ShowMessageRequest } from "@/shared/proto/host/window"
|
||||
/**
|
||||
* Formats the git diff into a prompt for the AI
|
||||
* @param gitDiff The git diff to format
|
||||
@@ -61,7 +59,12 @@ export function extractCommitMessage(aiResponse: string): string {
|
||||
*/
|
||||
export async function copyCommitMessageToClipboard(message: string): Promise<void> {
|
||||
await writeTextToClipboard(message)
|
||||
vscode.window.showInformationMessage("Commit message copied to clipboard")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Commit message copied to clipboard",
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,13 +76,19 @@ export async function showCommitMessageOptions(message: string): Promise<void> {
|
||||
const applyAction = "Apply to Git Input"
|
||||
const editAction = "Edit Message"
|
||||
|
||||
const selectedAction = await vscode.window.showInformationMessage(
|
||||
"Commit message generated",
|
||||
{ modal: false, detail: message },
|
||||
copyAction,
|
||||
applyAction,
|
||||
editAction,
|
||||
)
|
||||
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
|
||||
|
||||
// Handle user dismissing the dialog (selectedAction is undefined)
|
||||
if (!selectedAction) {
|
||||
@@ -111,13 +120,28 @@ async function applyCommitMessageToGitInput(message: string): Promise<void> {
|
||||
if (api && api.repositories.length > 0) {
|
||||
const repo = api.repositories[0]
|
||||
repo.inputBox.value = message
|
||||
vscode.window.showInformationMessage("Commit message applied to Git input")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Commit message applied to Git input",
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
vscode.window.showErrorMessage("No Git repositories found")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "No Git repositories found",
|
||||
}),
|
||||
)
|
||||
await copyCommitMessageToClipboard(message)
|
||||
}
|
||||
} else {
|
||||
vscode.window.showErrorMessage("Git extension not found")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Git extension not found",
|
||||
}),
|
||||
)
|
||||
await copyCommitMessageToClipboard(message)
|
||||
}
|
||||
}
|
||||
@@ -137,5 +161,10 @@ async function editCommitMessage(message: string): Promise<void> {
|
||||
path: document.uri.fsPath,
|
||||
}),
|
||||
)
|
||||
vscode.window.showInformationMessage("Edit the commit message and copy when ready")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Edit the commit message and copy when ready",
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import os from "os"
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { ShowTextDocumentRequest, ShowTextDocumentOptions } from "@/shared/proto/host/window"
|
||||
import { ShowTextDocumentRequest, ShowTextDocumentOptions, ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { writeFile } from "@utils/fs"
|
||||
|
||||
export async function downloadTask(dateTs: number, conversationHistory: Anthropic.MessageParam[]) {
|
||||
@@ -48,8 +48,11 @@ export async function downloadTask(dateTs: number, conversationHistory: Anthropi
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(
|
||||
`Failed to save markdown file: ${error instanceof Error ? error.message : String(error)}`,
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Failed to save markdown file: ${error instanceof Error ? error.message : String(error)}`,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,18 @@ import * as os from "os"
|
||||
import * as vscode from "vscode"
|
||||
import { arePathsEqual } from "@utils/path"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { ShowTextDocumentRequest, ShowTextDocumentOptions } from "@/shared/proto/host/window"
|
||||
import { ShowTextDocumentRequest, ShowTextDocumentOptions, 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) {
|
||||
vscode.window.showErrorMessage("Invalid data URI format")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Invalid data URI format",
|
||||
}),
|
||||
)
|
||||
return
|
||||
}
|
||||
const [, format, base64Data] = matches
|
||||
@@ -19,7 +24,12 @@ export async function openImage(dataUri: string) {
|
||||
await writeFile(tempFilePath, new Uint8Array(imageBuffer))
|
||||
await vscode.commands.executeCommand("vscode.open", vscode.Uri.file(tempFilePath))
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(`Error opening image: ${error}`)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Error opening image: ${error}`,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +62,11 @@ export async function openFile(absolutePath: string) {
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(`Could not open file!`)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Could not open file!`,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import sizeOf from "image-size"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { ShowOpenDialogueRequest } from "@/shared/proto/host/window"
|
||||
import { ShowMessageRequest, ShowMessageType, ShowOpenDialogueRequest } from "@/shared/proto/host/window"
|
||||
|
||||
/**
|
||||
* Supports processing of images and other file types
|
||||
@@ -46,14 +46,22 @@ 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}`)
|
||||
vscode.window.showErrorMessage(
|
||||
`Image too large: ${path.basename(filePath)} was skipped (dimensions exceed 7500px).`,
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
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)
|
||||
vscode.window.showErrorMessage(`Could not read dimensions for ${path.basename(filePath)}, skipping.`)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Could not read dimensions for ${path.basename(filePath)}, skipping.`,
|
||||
}),
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -68,12 +76,22 @@ 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}`)
|
||||
vscode.window.showErrorMessage(`File too large: ${path.basename(filePath)} was skipped (size exceeds 20MB).`)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
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)
|
||||
vscode.window.showErrorMessage(`Could not check file size for ${path.basename(filePath)}, skipping.`)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Could not check file size for ${path.basename(filePath)}, skipping.`,
|
||||
}),
|
||||
)
|
||||
return null
|
||||
}
|
||||
return { type: "file", data: filePath }
|
||||
|
||||
@@ -274,8 +274,8 @@ export class ClineAccountService {
|
||||
console.error("Error switching account:", error)
|
||||
throw error
|
||||
} finally {
|
||||
// Request a new authentication token
|
||||
await this._authService.refreshAuth()
|
||||
// After user switches account, we will force a refresh of the id token by calling this function that restores the refresh token and retrieves new auth info
|
||||
await this._authService.restoreRefreshTokenAndRetrieveAuthInfo()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import vscode from "vscode"
|
||||
import crypto from "crypto"
|
||||
import { EmptyRequest, String } from "../../shared/proto/common"
|
||||
import { AuthState } from "../../shared/proto/account"
|
||||
import { AuthState, UserInfo } from "../../shared/proto/account"
|
||||
import { StreamingResponseHandler, getRequestRegistry } from "@/core/controller/grpc-handler"
|
||||
import { FirebaseAuthProvider } from "./providers/FirebaseAuthProvider"
|
||||
import { Controller } from "@/core/controller"
|
||||
@@ -22,14 +22,35 @@ const availableAuthProviders = {
|
||||
// Add other providers here as needed
|
||||
}
|
||||
|
||||
export interface ClineAuthInfo {
|
||||
idToken: string
|
||||
userInfo: ClineAccountUserInfo
|
||||
}
|
||||
|
||||
export interface ClineAccountUserInfo {
|
||||
createdAt: string
|
||||
displayName: string
|
||||
email: string
|
||||
id: string
|
||||
organizations: ClineAccountOrganization[]
|
||||
}
|
||||
|
||||
export interface ClineAccountOrganization {
|
||||
active: boolean
|
||||
memberId: string
|
||||
name: string
|
||||
organizationId: string
|
||||
roles: string[]
|
||||
}
|
||||
|
||||
// TODO: Add logic to handle multiple webviews getting auth updates.
|
||||
|
||||
export class AuthService {
|
||||
private static instance: AuthService | null = null
|
||||
private _config: ServiceConfig
|
||||
private _authenticated: boolean = false
|
||||
private _user: any = null
|
||||
private _provider: any = null
|
||||
private _clineAuthInfo: ClineAuthInfo | null = null
|
||||
private _provider: { provider: FirebaseAuthProvider } | null = null
|
||||
private readonly _authNonce = crypto.randomBytes(32).toString("hex")
|
||||
private _activeAuthStatusUpdateSubscriptions = new Set<[Controller, StreamingResponseHandler]>()
|
||||
private _context: vscode.ExtensionContext
|
||||
@@ -100,6 +121,7 @@ export class AuthService {
|
||||
})
|
||||
|
||||
this._setProvider(authProviders.find((authProvider) => authProvider.name === providerName).name)
|
||||
|
||||
this._context = context
|
||||
}
|
||||
|
||||
@@ -118,7 +140,7 @@ export class AuthService {
|
||||
}
|
||||
AuthService.instance = new AuthService(context, config || {}, authProvider)
|
||||
}
|
||||
if (context) {
|
||||
if (context !== undefined) {
|
||||
AuthService.instance.context = context
|
||||
}
|
||||
return AuthService.instance
|
||||
@@ -141,13 +163,19 @@ export class AuthService {
|
||||
}
|
||||
|
||||
async getAuthToken(): Promise<string | null> {
|
||||
if (!this._user) {
|
||||
if (!this._clineAuthInfo) {
|
||||
return null
|
||||
}
|
||||
|
||||
// TODO: This may need to be dependant on the auth provider
|
||||
// Return the ID token from the user object
|
||||
return this._provider.provider.getAuthToken(this._user)
|
||||
const idToken = this._clineAuthInfo.idToken
|
||||
const shouldRefreshIdToken = await this._provider?.provider.shouldRefreshIdToken(idToken)
|
||||
if (shouldRefreshIdToken) {
|
||||
// Retrieves the stored id token and refreshes it, then updates this._clineAuthInfo
|
||||
await this.restoreRefreshTokenAndRetrieveAuthInfo()
|
||||
if (!this._clineAuthInfo) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
return this._clineAuthInfo.idToken
|
||||
}
|
||||
|
||||
private _setProvider(providerName: string): void {
|
||||
@@ -160,9 +188,17 @@ export class AuthService {
|
||||
}
|
||||
|
||||
getInfo(): AuthState {
|
||||
let user = null
|
||||
if (this._user && this._authenticated) {
|
||||
user = this._provider.provider.convertUserData(this._user)
|
||||
// TODO: this logic should be cleaner, but this will determine the authentication state for the webview -- if a user object is returned then the webview assumes authenticated, otherwise it assumes logged out (we previously returned a UserInfo object with empty fields, and this represented a broken logged in state)
|
||||
let user: any = null
|
||||
if (this._clineAuthInfo && this._authenticated) {
|
||||
const userInfo = this._clineAuthInfo.userInfo
|
||||
user = UserInfo.create({
|
||||
// TODO: create proto for new user info type
|
||||
uid: userInfo?.id,
|
||||
displayName: userInfo?.displayName,
|
||||
email: userInfo?.email,
|
||||
photoUrl: undefined,
|
||||
})
|
||||
}
|
||||
|
||||
return AuthState.create({
|
||||
@@ -199,8 +235,7 @@ export class AuthService {
|
||||
}
|
||||
|
||||
try {
|
||||
await this._provider.provider.signOut()
|
||||
this._user = null
|
||||
this._clineAuthInfo = null
|
||||
this._authenticated = false
|
||||
this.sendAuthStatusUpdate()
|
||||
} catch (error) {
|
||||
@@ -215,12 +250,11 @@ export class AuthService {
|
||||
}
|
||||
|
||||
try {
|
||||
this._user = await this._provider.provider.signIn(this._context, token, provider)
|
||||
this._clineAuthInfo = await this._provider.provider.signIn(this._context, token, provider)
|
||||
this._authenticated = true
|
||||
|
||||
await this.sendAuthStatusUpdate()
|
||||
this.setupAutoRefreshAuth()
|
||||
return this._user
|
||||
// return this._clineAuthInfo
|
||||
} catch (error) {
|
||||
console.error("Error signing in with custom token:", error)
|
||||
throw error
|
||||
@@ -239,59 +273,29 @@ export class AuthService {
|
||||
* Restores the authentication token from the extension's storage.
|
||||
* This is typically called when the extension is activated.
|
||||
*/
|
||||
async restoreAuthToken(): Promise<void> {
|
||||
async restoreRefreshTokenAndRetrieveAuthInfo(): Promise<void> {
|
||||
if (!this._provider || !this._provider.provider) {
|
||||
throw new Error("Auth provider is not set")
|
||||
}
|
||||
|
||||
try {
|
||||
this._user = await this._provider.provider.restoreAuthCredential(this._context)
|
||||
if (this._user) {
|
||||
this._clineAuthInfo = await this._provider.provider.retrieveClineAuthInfo(this._context)
|
||||
if (this._clineAuthInfo) {
|
||||
this._authenticated = true
|
||||
await this.sendAuthStatusUpdate()
|
||||
this.setupAutoRefreshAuth()
|
||||
// Setup auto-refresh for the auth token
|
||||
} else {
|
||||
console.warn("No user found after restoring auth token")
|
||||
this._authenticated = false
|
||||
this._user = null
|
||||
this._clineAuthInfo = null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error restoring auth token:", error)
|
||||
this._authenticated = false
|
||||
this._user = null
|
||||
this._clineAuthInfo = null
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the authentication status and sends an update to all subscribers.
|
||||
*/
|
||||
async refreshAuth(): Promise<void> {
|
||||
if (!this._user) {
|
||||
console.warn("No user is authenticated, skipping auth refresh")
|
||||
return
|
||||
}
|
||||
|
||||
await this._provider.provider.refreshAuthToken()
|
||||
this.sendAuthStatusUpdate()
|
||||
}
|
||||
|
||||
private setupAutoRefreshAuth(): void {
|
||||
// Set timeoutDuration to refresh the auth token 5 minutes before it expires
|
||||
const timeoutDuration = Math.floor(this._user.stsTokenManager.expirationTime - 5 * 60000 - Date.now()) // Milliseconds until 5 minutes before expiration
|
||||
setTimeout(() => this._autoRefreshAuth(), timeoutDuration)
|
||||
}
|
||||
|
||||
private async _autoRefreshAuth(): Promise<void> {
|
||||
if (!this._user) {
|
||||
console.warn("No user is authenticated, skipping auth refresh")
|
||||
return
|
||||
}
|
||||
await this.refreshAuth()
|
||||
this.setupAutoRefreshAuth() // Reschedule the next auto-refresh
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to authStatusUpdate events
|
||||
* @param controller The controller instance
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
import { getSecret, storeSecret } from "@/core/storage/state"
|
||||
import { ErrorService } from "@/services/error/ErrorService"
|
||||
import axios from "axios"
|
||||
import { initializeApp } from "firebase/app"
|
||||
import {
|
||||
AuthCredential,
|
||||
GoogleAuthProvider,
|
||||
GithubAuthProvider,
|
||||
OAuthCredential,
|
||||
User,
|
||||
UserCredential,
|
||||
getAuth,
|
||||
signInWithCredential,
|
||||
signOut,
|
||||
} from "firebase/auth"
|
||||
import { GithubAuthProvider, GoogleAuthProvider, User, getAuth, signInWithCredential } from "firebase/auth"
|
||||
import { ExtensionContext } from "vscode"
|
||||
import { ClineAccountUserInfo, ClineAuthInfo } from "../AuthService"
|
||||
import { jwtDecode } from "jwt-decode"
|
||||
|
||||
export class FirebaseAuthProvider {
|
||||
private _config: any
|
||||
@@ -29,80 +22,16 @@ export class FirebaseAuthProvider {
|
||||
this._config = value
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the authentication token of the current user.
|
||||
* @returns {Promise<string | null>} A promise that resolves to the authentication token of the current user, or null if no user is signed in.
|
||||
*/
|
||||
async getAuthToken(): Promise<string | null> {
|
||||
const user = getAuth().currentUser
|
||||
const idToken = user ? await user.getIdToken() : null
|
||||
return idToken
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the refresh token of the current user.
|
||||
* @returns {Promise<string | null>} A promise that resolves to the refresh token of the current user, or null if no user is signed in.
|
||||
*/
|
||||
async getRefreshToken(): Promise<string | null> {
|
||||
const user = getAuth().currentUser
|
||||
const refreshToken = user ? user.refreshToken : null
|
||||
return refreshToken
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the authentication token of the current user.
|
||||
* @returns {Promise<string | null>} A promise that resolves to the refreshed authentication token of the current user, or null if no user is signed in.
|
||||
*/
|
||||
async refreshAuthToken(): Promise<string | null> {
|
||||
const user = getAuth().currentUser
|
||||
const idToken = user ? await user.getIdToken(true) : null
|
||||
return idToken
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts Firebase User object to a generic user object.
|
||||
* @param user - The Firebase User object.
|
||||
* @returns {User} A generic user object.
|
||||
*/
|
||||
convertUserData(user: User) {
|
||||
return {
|
||||
uid: user.uid,
|
||||
email: user.email,
|
||||
displayName: user.displayName,
|
||||
photoUrl: user.photoURL,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs out the current user from Firebase.
|
||||
* @returns {Promise<void>} A promise that resolves when the user is signed out.
|
||||
*/
|
||||
async signOut(): Promise<void> {
|
||||
signOut(getAuth(initializeApp(Object.assign({}, this._config))))
|
||||
.then(() => {
|
||||
console.log("User signed out successfully.")
|
||||
})
|
||||
.catch((error) => {
|
||||
ErrorService.logMessage("Firebase sign-out error", "error")
|
||||
ErrorService.logException(error)
|
||||
throw error
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the authentication token using a provided token.
|
||||
* @param token - The authentication token to store.
|
||||
* @returns {Promise<User>} A promise that resolves with the authenticated user.
|
||||
* @throws {Error} Throws an error if the storage fails.
|
||||
*/
|
||||
private async _storeAuthCredential(context: ExtensionContext, credential: AuthCredential): Promise<void> {
|
||||
try {
|
||||
await storeSecret(context, "clineAccountId", JSON.stringify(credential.toJSON()))
|
||||
} catch (error) {
|
||||
ErrorService.logMessage("Firebase store token error", "error")
|
||||
ErrorService.logException(error)
|
||||
throw error
|
||||
async shouldRefreshIdToken(existingIdToken: string): Promise<boolean> {
|
||||
const decodedToken = jwtDecode(existingIdToken)
|
||||
const exp = decodedToken.exp || 0 // 1752297633
|
||||
const expirationTime = exp * 1000
|
||||
const currentTime = Date.now()
|
||||
const fiveMinutesInMs = 5 * 60 * 1000
|
||||
if (currentTime > expirationTime - fiveMinutesInMs) {
|
||||
return true // id token is expired or about to be expired
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,31 +40,55 @@ export class FirebaseAuthProvider {
|
||||
* @returns {Promise<User>} A promise that resolves with the authenticated user.
|
||||
* @throws {Error} Throws an error if the restoration fails.
|
||||
*/
|
||||
async restoreAuthCredential(context: ExtensionContext): Promise<User | null> {
|
||||
const credentialJSON = await getSecret(context, "clineAccountId")
|
||||
if (!credentialJSON) {
|
||||
async retrieveClineAuthInfo(context: ExtensionContext): Promise<ClineAuthInfo | null> {
|
||||
const userRefreshToken = await getSecret(context, "clineAccountId")
|
||||
if (!userRefreshToken) {
|
||||
console.error("No stored authentication credential found.")
|
||||
return null
|
||||
}
|
||||
try {
|
||||
const credentialData: AuthCredential = OAuthCredential.fromJSON(credentialJSON) as AuthCredential
|
||||
const userCredential = await this._signInWithCredential(credentialData)
|
||||
return userCredential.user
|
||||
} catch (error) {
|
||||
ErrorService.logMessage("Firebase restore token error", "error")
|
||||
ErrorService.logException(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
// Exchange refresh token for new access token using Firebase's secure token endpoint
|
||||
// https://stackoverflow.com/questions/38233687/how-to-use-the-firebase-refreshtoken-to-reauthenticate/57119131#57119131
|
||||
const firebaseApiKey = this._config.apiKey
|
||||
const googleAccessTokenResponse = await axios.post(
|
||||
`https://securetoken.googleapis.com/v1/token?key=${firebaseApiKey}`,
|
||||
`grant_type=refresh_token&refresh_token=${encodeURIComponent(userRefreshToken)}`,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
async _signInWithCredential(credential: AuthCredential): Promise<UserCredential> {
|
||||
const firebaseConfig = Object.assign({}, this._config)
|
||||
const app = initializeApp(firebaseConfig)
|
||||
const auth = getAuth(app)
|
||||
try {
|
||||
return await signInWithCredential(auth, credential)
|
||||
// console.log("googleAccessTokenResponse", googleAccessTokenResponse)
|
||||
|
||||
// This returns an object with access_token, expires_in (3600), id_token (can be used as bearer token to authenticate requests, we'll use this in the future instead of firebase but need to be aware of how we use firebase sdk for e.g. user info like the profile image), project_id, refresh_token, token_type (always Bearer), and user_id
|
||||
const idToken = googleAccessTokenResponse.data.id_token
|
||||
// const idTokenExpirationDate = new Date(Date.now() + googleAccessTokenResponse.data.expires_in * 1000)
|
||||
|
||||
// Now retrieve the user info from the backend (this was an easy solution to keep providing user profile details like name and email, but we should move to using the fetchMe() function instead)
|
||||
// Fetch user info from Cline API
|
||||
// TODO: consolidate with fetchMe() instead of making the call directly here
|
||||
const userResponse = await axios.get("https://api.cline.bot/api/v1/users/me", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${idToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// Store user data
|
||||
const userInfo: ClineAccountUserInfo = userResponse.data.data
|
||||
|
||||
return { idToken, userInfo }
|
||||
|
||||
// let userObject = JSON.parse(credentialJSON)
|
||||
// let user = User.
|
||||
// userObject = User.constructor._fromJSON(auth, user2);
|
||||
// const credentialData: AuthCredential = OAuthCredential.fromJSON(credentialJSON) as AuthCredential
|
||||
// const userCredential = await this._signInWithCredential(context, credentialData)
|
||||
// return userCredential.user
|
||||
} catch (error) {
|
||||
ErrorService.logMessage("Firebase sign-in with credential error", "error")
|
||||
console.error("Firebase restore token error", error)
|
||||
ErrorService.logMessage("Firebase restore token error", "error")
|
||||
ErrorService.logException(error)
|
||||
throw error
|
||||
}
|
||||
@@ -146,10 +99,9 @@ export class FirebaseAuthProvider {
|
||||
* @returns {Promise<User>} A promise that resolves with the authenticated user.
|
||||
* @throws {Error} Throws an error if the sign-in fails.
|
||||
*/
|
||||
async signIn(context: ExtensionContext, token: string, provider: string): Promise<User> {
|
||||
async signIn(context: ExtensionContext, token: string, provider: string): Promise<ClineAuthInfo | null> {
|
||||
try {
|
||||
let credential
|
||||
let userCredential
|
||||
switch (provider) {
|
||||
case "google":
|
||||
credential = GoogleAuthProvider.credential(token)
|
||||
@@ -160,9 +112,25 @@ export class FirebaseAuthProvider {
|
||||
default:
|
||||
throw new Error(`Unsupported provider: ${provider}`)
|
||||
}
|
||||
this._storeAuthCredential(context, credential)
|
||||
userCredential = await this._signInWithCredential(credential)
|
||||
return userCredential.user
|
||||
// we've received the short-lived tokens from google/github, now we need to sign in to firebase with them
|
||||
const firebaseConfig = Object.assign({}, this._config)
|
||||
const app = initializeApp(firebaseConfig)
|
||||
const auth = getAuth(app)
|
||||
// this signs the user into firebase sdk internally
|
||||
const userCredential = (await signInWithCredential(auth, credential)).user
|
||||
// const userRefreshToken = await userCredential.getIdToken()
|
||||
|
||||
// store the long-lived refresh token in secret storage
|
||||
try {
|
||||
await storeSecret(context, "clineAccountId", userCredential.refreshToken)
|
||||
} catch (error) {
|
||||
ErrorService.logMessage("Firebase store token error", "error")
|
||||
ErrorService.logException(error)
|
||||
throw error
|
||||
}
|
||||
|
||||
// userCredential = await this._signInWithCredential(context, credential)
|
||||
return await this.retrieveClineAuthInfo(context)
|
||||
} catch (error) {
|
||||
ErrorService.logMessage("Firebase sign-in error", "error")
|
||||
ErrorService.logException(error)
|
||||
|
||||
+58
-19
@@ -20,10 +20,8 @@ import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { z } from "zod"
|
||||
import { FileChangeEvent_ChangeType, SubscribeToFileRequest } from "../../shared/proto/host/watch"
|
||||
import { Metadata } from "../../shared/proto/common"
|
||||
import {
|
||||
DEFAULT_MCP_TIMEOUT_SECONDS,
|
||||
McpMode,
|
||||
McpResource,
|
||||
McpResourceResponse,
|
||||
McpResourceTemplate,
|
||||
@@ -33,15 +31,14 @@ import {
|
||||
MIN_MCP_TIMEOUT_SECONDS,
|
||||
} from "@shared/mcp"
|
||||
import { fileExistsAtPath } from "@utils/fs"
|
||||
import { arePathsEqual } from "@utils/path"
|
||||
import { secondsToMs } from "@utils/time"
|
||||
import { GlobalFileNames } from "@core/storage/disk"
|
||||
import { ExtensionMessage } from "@shared/ExtensionMessage"
|
||||
import { DEFAULT_REQUEST_TIMEOUT_MS } from "./constants"
|
||||
import { Transport, McpConnection, McpTransportType, McpServerConfig } from "./types"
|
||||
import { McpConnection, McpServerConfig } from "./types"
|
||||
import { BaseConfigSchema, ServerConfigSchema, McpSettingsSchema } from "./schemas"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
|
||||
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
export class McpHub {
|
||||
getMcpServersPath: () => Promise<string>
|
||||
private getSettingsDirectoryPath: () => Promise<string>
|
||||
@@ -112,8 +109,11 @@ export class McpHub {
|
||||
try {
|
||||
config = JSON.parse(content)
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(
|
||||
"Invalid MCP settings format. Please ensure your settings follow the correct JSON format.",
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Invalid MCP settings format. Please ensure your settings follow the correct JSON format.",
|
||||
}),
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
@@ -121,7 +121,12 @@ export class McpHub {
|
||||
// Validate against schema
|
||||
const result = McpSettingsSchema.safeParse(config)
|
||||
if (!result.success) {
|
||||
vscode.window.showErrorMessage("Invalid MCP settings schema.")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Invalid MCP settings schema.",
|
||||
}),
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -153,7 +158,12 @@ export class McpHub {
|
||||
if (settings) {
|
||||
try {
|
||||
await this.updateServerConnections(settings.mcpServers)
|
||||
vscode.window.showInformationMessage("MCP servers updated")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "MCP servers updated",
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Failed to process MCP settings change:", error)
|
||||
}
|
||||
@@ -403,8 +413,11 @@ export class McpHub {
|
||||
console.log(`[MCP Fallback Notification] ${name}:`, JSON.stringify(notification, null, 2))
|
||||
|
||||
// Show in VS Code for visibility
|
||||
vscode.window.showInformationMessage(
|
||||
`MCP ${name}: ${notification.method || "unknown"} - ${JSON.stringify(notification.params || {})}`,
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: `MCP ${name}: ${notification.method || "unknown"} - ${JSON.stringify(notification.params || {})}`,
|
||||
}),
|
||||
)
|
||||
}
|
||||
console.log(`[MCP Debug] Successfully set fallback notification handler for ${name}`)
|
||||
@@ -658,7 +671,12 @@ export class McpHub {
|
||||
const connection = this.connections.find((conn) => conn.server.name === serverName)
|
||||
const config = connection?.server.config
|
||||
if (config) {
|
||||
vscode.window.showInformationMessage(`Restarting ${serverName} MCP server...`)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: `Restarting ${serverName} MCP server...`,
|
||||
}),
|
||||
)
|
||||
connection.server.status = "connecting"
|
||||
connection.server.error = ""
|
||||
await this.notifyWebviewOfServerChanges()
|
||||
@@ -667,10 +685,20 @@ export class McpHub {
|
||||
await this.deleteConnection(serverName)
|
||||
// Try to connect again using existing config
|
||||
await this.connectToServer(serverName, JSON.parse(config), "internal")
|
||||
vscode.window.showInformationMessage(`${serverName} MCP server connected`)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: `${serverName} MCP server connected`,
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error(`Failed to restart connection for ${serverName}:`, error)
|
||||
vscode.window.showErrorMessage(`Failed to connect to ${serverName} MCP server`)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Failed to connect to ${serverName} MCP server`,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -756,8 +784,11 @@ export class McpHub {
|
||||
if (error instanceof Error) {
|
||||
console.error("Error details:", error.message, error.stack)
|
||||
}
|
||||
vscode.window.showErrorMessage(
|
||||
`Failed to update server state: ${error instanceof Error ? error.message : String(error)}`,
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Failed to update server state: ${error instanceof Error ? error.message : String(error)}`,
|
||||
}),
|
||||
)
|
||||
throw error
|
||||
}
|
||||
@@ -915,7 +946,12 @@ export class McpHub {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to update autoApprove settings:", error)
|
||||
vscode.window.showErrorMessage("Failed to update autoApprove settings")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Failed to update autoApprove settings",
|
||||
}),
|
||||
)
|
||||
throw error // Re-throw to ensure the error is properly handled
|
||||
}
|
||||
}
|
||||
@@ -1033,8 +1069,11 @@ export class McpHub {
|
||||
if (error instanceof Error) {
|
||||
console.error("Error details:", error.message, error.stack)
|
||||
}
|
||||
vscode.window.showErrorMessage(
|
||||
`Failed to update server timeout: ${error instanceof Error ? error.message : String(error)}`,
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Failed to update server timeout: ${error instanceof Error ? error.message : String(error)}`,
|
||||
}),
|
||||
)
|
||||
throw error
|
||||
}
|
||||
|
||||
+2
-1
@@ -2094,7 +2094,8 @@ export const xaiModels = {
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 3.0, // will have different pricing for long context vs short context
|
||||
outputPrice: 6.0,
|
||||
cacheReadsPrice: 0.75,
|
||||
outputPrice: 15.0,
|
||||
},
|
||||
"grok-3-beta": {
|
||||
maxTokens: 8192,
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { DiffViewProvider } from "@/integrations/editor/DiffViewProvider"
|
||||
|
||||
export class ExternalDiffViewProvider extends DiffViewProvider {
|
||||
override async openDiffEditor(): Promise<void> {
|
||||
if (!this.absolutePath) {
|
||||
return
|
||||
}
|
||||
getHostBridgeProvider().diffClient.openDiff({ path: this.absolutePath, content: this.originalContent ?? "" })
|
||||
}
|
||||
override replaceText(
|
||||
content: string,
|
||||
rangeToReplace: { startLine: number; endLine: number },
|
||||
currentLine: number,
|
||||
): Promise<void> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,14 @@ import {
|
||||
WorkspaceServiceClientImpl,
|
||||
EnvServiceClientImpl,
|
||||
WindowServiceClientImpl,
|
||||
DiffServiceClientImpl,
|
||||
} from "@generated/standalone/host-bridge-clients"
|
||||
import {
|
||||
WatchServiceClientInterface,
|
||||
WorkspaceServiceClientInterface,
|
||||
EnvServiceClientInterface,
|
||||
WindowServiceClientInterface,
|
||||
DiffServiceClientInterface,
|
||||
} from "@generated/hosts/host-bridge-client-types"
|
||||
import { HostBridgeClientProvider } from "@/hosts/host-provider-types"
|
||||
|
||||
@@ -23,6 +25,7 @@ export class ExternalHostBridgeClientManager implements HostBridgeClientProvider
|
||||
workspaceClient: WorkspaceServiceClientInterface
|
||||
envClient: EnvServiceClientInterface
|
||||
windowClient: WindowServiceClientInterface
|
||||
diffClient: DiffServiceClientInterface
|
||||
|
||||
constructor() {
|
||||
const address = process.env.HOST_BRIDGE_ADDRESS || "localhost:50052"
|
||||
@@ -32,6 +35,7 @@ export class ExternalHostBridgeClientManager implements HostBridgeClientProvider
|
||||
this.workspaceClient = new WorkspaceServiceClientImpl(this.channel)
|
||||
this.envClient = new EnvServiceClientImpl(this.channel)
|
||||
this.windowClient = new WindowServiceClientImpl(this.channel)
|
||||
this.diffClient = new DiffServiceClientImpl(this.channel)
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
|
||||
@@ -13,11 +13,12 @@ import { ExternalHostBridgeClientManager } from "./host-bridge-client-manager"
|
||||
import { ExternalWebviewProvider } from "./ExternalWebviewProvider"
|
||||
import { WebviewProviderType } from "@/shared/webview/types"
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
import { ExternalDiffViewProvider } from "./ExternalDiffviewProvider"
|
||||
|
||||
async function main() {
|
||||
log("Starting standalone service...")
|
||||
|
||||
hostProviders.initializeHostProviders(createWebview, new ExternalHostBridgeClientManager())
|
||||
hostProviders.initializeHostProviders(createWebview, createDiffView, new ExternalHostBridgeClientManager())
|
||||
activate(extensionContext)
|
||||
const controller = new Controller(extensionContext, outputChannel, postMessage, uuidv4())
|
||||
startProtobusService(controller)
|
||||
@@ -60,9 +61,12 @@ function getProtobusServiceNames(packageDefinition: { [x: string]: any }): strin
|
||||
return protobusServiceNames
|
||||
}
|
||||
|
||||
const createWebview = () => {
|
||||
function createWebview() {
|
||||
return new ExternalWebviewProvider(extensionContext, outputChannel, WebviewProviderType.SIDEBAR)
|
||||
}
|
||||
function createDiffView() {
|
||||
return new ExternalDiffViewProvider()
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a Promise-based handler function to make it compatible with gRPC's callback-based API.
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { describe, it } from "mocha"
|
||||
import "should"
|
||||
import { shouldSkipReasoningForModel } from "../model-utils"
|
||||
|
||||
describe("shouldSkipReasoningForModel", () => {
|
||||
it("should return true for grok-4 models", () => {
|
||||
shouldSkipReasoningForModel("grok-4").should.equal(true)
|
||||
shouldSkipReasoningForModel("x-ai/grok-4").should.equal(true)
|
||||
shouldSkipReasoningForModel("openrouter/grok-4-turbo").should.equal(true)
|
||||
shouldSkipReasoningForModel("some-provider/grok-4-mini").should.equal(true)
|
||||
})
|
||||
|
||||
it("should return false for non-grok-4 models", () => {
|
||||
shouldSkipReasoningForModel("grok-3").should.equal(false)
|
||||
shouldSkipReasoningForModel("grok-2").should.equal(false)
|
||||
shouldSkipReasoningForModel("claude-3-sonnet").should.equal(false)
|
||||
shouldSkipReasoningForModel("gpt-4").should.equal(false)
|
||||
shouldSkipReasoningForModel("gemini-pro").should.equal(false)
|
||||
})
|
||||
|
||||
it("should return false for undefined or empty model IDs", () => {
|
||||
shouldSkipReasoningForModel(undefined).should.equal(false)
|
||||
shouldSkipReasoningForModel("").should.equal(false)
|
||||
})
|
||||
|
||||
it("should be case sensitive", () => {
|
||||
shouldSkipReasoningForModel("GROK-4").should.equal(false)
|
||||
shouldSkipReasoningForModel("Grok-4").should.equal(false)
|
||||
})
|
||||
})
|
||||
@@ -13,3 +13,14 @@ export function isGemini2dot5ModelFamily(api: ApiHandler): boolean {
|
||||
const modelId = model.id
|
||||
return modelId.includes("gemini-2.5")
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if reasoning content should be skipped for a given model
|
||||
* Currently skips reasoning for Grok-4 models since they only display "thinking" without useful information
|
||||
*/
|
||||
export function shouldSkipReasoningForModel(modelId?: string): boolean {
|
||||
if (!modelId) {
|
||||
return false
|
||||
}
|
||||
return modelId.includes("grok-4")
|
||||
}
|
||||
|
||||
Generated
+10
-19
@@ -9,6 +9,7 @@
|
||||
"version": "0.3.0",
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "^0.27.4",
|
||||
"@fontsource/azeret-mono": "^5.2.9",
|
||||
"@heroui/react": "^2.8.0-beta.2",
|
||||
"@vscode/webview-ui-toolkit": "^1.4.0",
|
||||
"debounce": "^2.1.1",
|
||||
@@ -23,7 +24,6 @@
|
||||
"posthog-js": "^1.224.0",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
"react": "^18.3.1",
|
||||
"react-countup": "^6.5.3",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-remark": "^2.1.0",
|
||||
"react-textarea-autosize": "^8.5.7",
|
||||
@@ -1261,6 +1261,15 @@
|
||||
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@fontsource/azeret-mono": {
|
||||
"version": "5.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/azeret-mono/-/azeret-mono-5.2.9.tgz",
|
||||
"integrity": "sha512-1qnbVspQPI38qhSTSidWU4bjG5ynWCfkMwfPxahqxejJO/u4yT1FbPqG73s4fDmQSuDQYoA8jfTpoQiod7+fuA==",
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/ecma402-abstract": {
|
||||
"version": "2.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.4.tgz",
|
||||
@@ -8327,12 +8336,6 @@
|
||||
"layout-base": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/countup.js": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/countup.js/-/countup.js-2.8.0.tgz",
|
||||
"integrity": "sha512-f7xEhX0awl4NOElHulrl4XRfKoNH3rB+qfNSZZyjSZhaAoUk6elvhH+MNxMmlmuUJ2/QNTWPSA7U4mNtIAKljQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/create-error-class": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz",
|
||||
@@ -13711,18 +13714,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-countup": {
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmjs.org/react-countup/-/react-countup-6.5.3.tgz",
|
||||
"integrity": "sha512-udnqVQitxC7QWADSPDOxVWULkLvKUWrDapn5i53HE4DPRVgs+Y5rr4bo25qEl8jSh+0l2cToJgGMx+clxPM3+w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"countup.js": "^2.8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-devtools": {
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/react-devtools/-/react-devtools-6.1.2.tgz",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "^0.27.4",
|
||||
"@fontsource/azeret-mono": "^5.2.9",
|
||||
"@heroui/react": "^2.8.0-beta.2",
|
||||
"@vscode/webview-ui-toolkit": "^1.4.0",
|
||||
"debounce": "^2.1.1",
|
||||
@@ -30,7 +31,6 @@
|
||||
"posthog-js": "^1.224.0",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
"react": "^18.3.1",
|
||||
"react-countup": "^6.5.3",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-remark": "^2.1.0",
|
||||
"react-textarea-autosize": "^8.5.7",
|
||||
|
||||
@@ -1,16 +1,73 @@
|
||||
import { VSCodeButton, VSCodeDivider, VSCodeLink, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
|
||||
import { memo, useCallback, useEffect, useState } from "react"
|
||||
import { BadgeCent } from "lucide-react"
|
||||
import { memo, useCallback, useEffect, useState, useRef } from "react"
|
||||
import { useClineAuth } from "@/context/ClineAuthContext"
|
||||
import VSCodeButtonLink from "../common/VSCodeButtonLink"
|
||||
import ClineLogoWhite from "../../assets/ClineLogoWhite"
|
||||
import CountUp from "react-countup"
|
||||
import CreditsHistoryTable from "./CreditsHistoryTable"
|
||||
import { UsageTransaction, PaymentTransaction } from "@shared/ClineAccount"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { AccountServiceClient } from "@/services/grpc-client"
|
||||
import { EmptyRequest } from "@shared/proto/common"
|
||||
import { UserOrganization, UserOrganizationUpdateRequest } from "@shared/proto/account"
|
||||
import { formatCreditsBalance } from "@/utils/format"
|
||||
|
||||
// Custom hook for animated credit display with styled decimals
|
||||
const useAnimatedCredits = (targetValue: number, duration: number = 660) => {
|
||||
const [currentValue, setCurrentValue] = useState(0)
|
||||
const animationRef = useRef<number>()
|
||||
const startTimeRef = useRef<number>()
|
||||
|
||||
useEffect(() => {
|
||||
const animate = (timestamp: number) => {
|
||||
if (!startTimeRef.current) {
|
||||
startTimeRef.current = timestamp
|
||||
}
|
||||
|
||||
const elapsed = timestamp - startTimeRef.current
|
||||
const progress = Math.min(elapsed / duration, 1)
|
||||
|
||||
// Easing function (ease-out)
|
||||
const easedProgress = 1 - Math.pow(1 - progress, 3)
|
||||
const newValue = easedProgress * targetValue
|
||||
|
||||
setCurrentValue(newValue)
|
||||
|
||||
if (progress < 1) {
|
||||
animationRef.current = requestAnimationFrame(animate)
|
||||
}
|
||||
}
|
||||
|
||||
// Reset and start animation
|
||||
startTimeRef.current = undefined
|
||||
animationRef.current = requestAnimationFrame(animate)
|
||||
|
||||
return () => {
|
||||
if (animationRef.current) {
|
||||
cancelAnimationFrame(animationRef.current)
|
||||
}
|
||||
}
|
||||
}, [targetValue, duration])
|
||||
|
||||
return currentValue
|
||||
}
|
||||
|
||||
// Custom component to handle styled credit display
|
||||
const StyledCreditDisplay = ({ balance }: { balance: number }) => {
|
||||
const animatedValue = useAnimatedCredits(formatCreditsBalance(balance))
|
||||
const formatted = animatedValue.toFixed(4)
|
||||
const parts = formatted.split(".")
|
||||
const wholePart = parts[0]
|
||||
const decimalPart = parts[1] || "0000"
|
||||
const firstTwoDecimals = decimalPart.slice(0, 2)
|
||||
const lastTwoDecimals = decimalPart.slice(2)
|
||||
|
||||
return (
|
||||
<span className="font-azeret-mono font-light tabular-nums">
|
||||
{wholePart}.{firstTwoDecimals}
|
||||
<span className="text-[var(--vscode-descriptionForeground)]">{lastTwoDecimals}</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
type VSCodeDropdownChangeEvent = Event & {
|
||||
target: {
|
||||
@@ -44,7 +101,7 @@ export const ClineAccountView = () => {
|
||||
|
||||
let user = apiConfiguration?.clineAccountId ? clineUser || userInfo : undefined
|
||||
|
||||
const [balance, setBalance] = useState(0)
|
||||
const [balance, setBalance] = useState<number | null>(null)
|
||||
const [userOrganizations, setUserOrganizations] = useState<UserOrganization[]>([])
|
||||
const [activeOrganization, setActiveOrganization] = useState<UserOrganization | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
@@ -60,12 +117,12 @@ export const ClineAccountView = () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const response = await AccountServiceClient.getUserCredits(EmptyRequest.create())
|
||||
setBalance(response.balance?.currentBalance || 0)
|
||||
setBalance(response.balance?.currentBalance ?? null)
|
||||
setUsageData(response.usageTransactions)
|
||||
setPaymentsData(response.paymentTransactions)
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch user credits data:", error)
|
||||
setBalance(0)
|
||||
setBalance(null)
|
||||
setUsageData([])
|
||||
setPaymentsData([])
|
||||
} finally {
|
||||
@@ -97,7 +154,7 @@ export const ClineAccountView = () => {
|
||||
Promise.all([getUserCredits(), getUserOrganizations()])
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch user data:", error)
|
||||
setBalance(0)
|
||||
setBalance(null)
|
||||
setUsageData([])
|
||||
setPaymentsData([])
|
||||
} finally {
|
||||
@@ -145,13 +202,13 @@ export const ClineAccountView = () => {
|
||||
<div className="flex flex-col pr-3 h-full">
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="flex items-center mb-6 flex-wrap gap-y-4">
|
||||
{user.photoUrl ? (
|
||||
{/* {user.photoUrl ? (
|
||||
<img src={user.photoUrl} alt="Profile" className="size-16 rounded-full mr-4" />
|
||||
) : (
|
||||
<div className="size-16 rounded-full bg-[var(--vscode-button-background)] flex items-center justify-center text-2xl text-[var(--vscode-button-foreground)] mr-4">
|
||||
{user.displayName?.[0] || user.email?.[0] || "?"}
|
||||
</div>
|
||||
)}
|
||||
) : ( */}
|
||||
<div className="size-16 rounded-full bg-[var(--vscode-button-background)] flex items-center justify-center text-2xl text-[var(--vscode-button-foreground)] mr-4">
|
||||
{user.displayName?.[0] || user.email?.[0] || "?"}
|
||||
</div>
|
||||
{/* )} */}
|
||||
|
||||
<div className="flex flex-col">
|
||||
{user.displayName && (
|
||||
@@ -199,18 +256,22 @@ export const ClineAccountView = () => {
|
||||
|
||||
{activeOrganization === null && (
|
||||
<div className="w-full flex flex-col items-center">
|
||||
<div className="text-sm text-[var(--vscode-descriptionForeground)] mb-3">CURRENT BALANCE</div>
|
||||
<div className="text-sm text-[var(--vscode-descriptionForeground)] mb-3 font-azeret-mono font-light">
|
||||
CURRENT BALANCE
|
||||
</div>
|
||||
|
||||
<div className="text-4xl font-bold text-[var(--vscode-foreground)] mb-6 flex items-center gap-2">
|
||||
{isLoading ? (
|
||||
<div className="text-[var(--vscode-descriptionForeground)]">Loading...</div>
|
||||
) : (
|
||||
<>
|
||||
<BadgeCent className="size-6 text-[var(--vscode-foreground)]" />
|
||||
{/* TODO: Do this in a more correct way. We have to divide by 10000
|
||||
* because the balance is stored in microcredits in the backend.
|
||||
*/}
|
||||
<CountUp end={balance / 10000} duration={0.66} decimals={4} />
|
||||
{balance === null ? (
|
||||
<span>----</span>
|
||||
) : (
|
||||
<>
|
||||
<StyledCreditDisplay balance={balance} />
|
||||
</>
|
||||
)}
|
||||
<VSCodeButton appearance="icon" className="mt-1" onClick={getUserCredits}>
|
||||
<span className="codicon codicon-refresh"></span>
|
||||
</VSCodeButton>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { VSCodeBadge, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
|
||||
import { VSCodeBadge, VSCodeButton, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
|
||||
import deepEqual from "fast-deep-equal"
|
||||
import React, { memo, MouseEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import styled from "styled-components"
|
||||
@@ -36,6 +36,7 @@ import NewTaskPreview from "./NewTaskPreview"
|
||||
import ReportBugPreview from "./ReportBugPreview"
|
||||
import UserMessage from "./UserMessage"
|
||||
import QuoteButton from "./QuoteButton"
|
||||
import { useClineAuth } from "@/context/ClineAuthContext"
|
||||
|
||||
const normalColor = "var(--vscode-foreground)"
|
||||
const errorColor = "var(--vscode-errorForeground)"
|
||||
@@ -184,6 +185,7 @@ 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>({
|
||||
@@ -1006,6 +1008,23 @@ export const ChatRowContent = memo(
|
||||
.
|
||||
</>
|
||||
)}
|
||||
{apiRequestFailedMessage?.includes(
|
||||
"Unauthorized: Please sign in to Cline before trying again.", // match with cline.ts (TODO: remove after some time)
|
||||
) && (
|
||||
<>
|
||||
<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>
|
||||
)
|
||||
})()}
|
||||
|
||||
@@ -49,8 +49,8 @@ const featuredModels = [
|
||||
label: "Best",
|
||||
},
|
||||
{
|
||||
id: "google/gemini-2.5-pro",
|
||||
description: "Large 1M context window, great value",
|
||||
id: "moonshotai/kimi-k2",
|
||||
description: "Latest open source model, trained for agentic tool calling.",
|
||||
label: "Trending",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
|
||||
@config "../tailwind.config.js";
|
||||
|
||||
/* Import Azeret Mono font from local package */
|
||||
@import "@fontsource/azeret-mono/300.css";
|
||||
@import "@fontsource/azeret-mono/400.css";
|
||||
@import "@fontsource/azeret-mono/700.css";
|
||||
|
||||
textarea:focus {
|
||||
outline: 1.5px solid var(--vscode-focusBorder, #007fd4);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,24 @@ export function formatDollars(cents?: number): string {
|
||||
return (cents / 100).toFixed(2)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts microcredits to credits for display purposes.
|
||||
*
|
||||
* The backend stores credit balances in microcredits (1 credit = 10,000 microcredits)
|
||||
* to avoid floating point precision issues when performing calculations.
|
||||
* This function converts the microcredits back to the user-facing credit amount.
|
||||
*
|
||||
* @param microcredits - The balance in microcredits from the backend
|
||||
* @returns The balance in credits (typically displayed with 4 decimal places)
|
||||
*
|
||||
* @example
|
||||
* formatCreditsBalance(50000) // returns 5.0000 (credits)
|
||||
* formatCreditsBalance(12345) // returns 1.2345 (credits)
|
||||
*/
|
||||
export function formatCreditsBalance(microcredits: number): number {
|
||||
return microcredits / 10000
|
||||
}
|
||||
|
||||
export function formatTimestamp(timestamp: string): string {
|
||||
const date = new Date(timestamp)
|
||||
|
||||
|
||||
@@ -4,7 +4,11 @@ const { heroui } = require("@heroui/react")
|
||||
module.exports = {
|
||||
content: ["./src/**/*.{js,ts,jsx,tsx,mdx}", "./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
extend: {},
|
||||
extend: {
|
||||
fontFamily: {
|
||||
"azeret-mono": ['"Azeret Mono"', "monospace"],
|
||||
},
|
||||
},
|
||||
},
|
||||
darkMode: "class",
|
||||
plugins: [
|
||||
|
||||
Reference in New Issue
Block a user