mirror of
https://github.com/cline/cline.git
synced 2026-09-01 15:11:04 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e940ab4b2 |
@@ -0,0 +1,103 @@
|
||||
import * as vscode from "vscode"
|
||||
|
||||
export type ApiProvider =
|
||||
| "openai"
|
||||
| "anthropic"
|
||||
| "azure"
|
||||
| "mistral"
|
||||
| "deepseek"
|
||||
| "qwen"
|
||||
| "together"
|
||||
| "litellm"
|
||||
| "ollama"
|
||||
| "lmstudio"
|
||||
| "vertex"
|
||||
| "requesty"
|
||||
| "openrouter"
|
||||
| "aws"
|
||||
| "bedrock"
|
||||
| "gemini"
|
||||
| "openai-native"
|
||||
| "vscode-lm"
|
||||
|
||||
export interface ModelInfo {
|
||||
id: string
|
||||
name: string
|
||||
provider: string
|
||||
contextWindow?: number
|
||||
maxTokens?: number
|
||||
supportsPromptCache?: boolean
|
||||
supportsImages?: boolean
|
||||
supportsComputerUse?: boolean
|
||||
cacheWritesPrice?: number
|
||||
cacheReadsPrice?: number
|
||||
inputPrice?: number
|
||||
outputPrice?: number
|
||||
description?: string
|
||||
}
|
||||
|
||||
export interface ApiHandlerOptions {
|
||||
provider: ApiProvider
|
||||
modelId: string
|
||||
apiKey?: string
|
||||
baseUrl?: string
|
||||
}
|
||||
|
||||
export type AnthropicModelId = "claude-3-opus-20240229" | "claude-3-sonnet-20240229" | "claude-3-haiku-20240307"
|
||||
export type BedrockModelId = "anthropic.claude-3-sonnet-20240229" | "anthropic.claude-3-haiku-20240307"
|
||||
export type DeepSeekModelId = "deepseek-chat"
|
||||
export type GeminiModelId = "gemini-pro" | "gemini-pro-vision"
|
||||
export type OpenAiNativeModelId = "gpt-4-turbo-preview" | "gpt-4-vision-preview"
|
||||
export type QwenModelId = "qwen-turbo" | "qwen-plus" | "qwen-max"
|
||||
export type VertexModelId = "gemini-pro" | "gemini-pro-vision"
|
||||
|
||||
export interface ApiConfiguration {
|
||||
apiModelId?: string
|
||||
apiProvider: ApiProvider
|
||||
apiKey?: string
|
||||
openRouterApiKey?: string
|
||||
openRouterModelId?: string
|
||||
openRouterModelInfo?: ModelInfo
|
||||
awsAccessKey?: string
|
||||
awsSecretKey?: string
|
||||
awsSessionToken?: string
|
||||
awsRegion?: string
|
||||
awsUseCrossRegionInference?: boolean
|
||||
awsProfile?: string
|
||||
awsUseProfile?: boolean
|
||||
anthropicApiKey?: string
|
||||
anthropicBaseUrl?: string
|
||||
azureApiKey?: string
|
||||
azureEndpoint?: string
|
||||
azureDeploymentName?: string
|
||||
azureApiVersion?: string
|
||||
mistralApiKey?: string
|
||||
deepSeekApiKey?: string
|
||||
qwenApiKey?: string
|
||||
qwenEndpoint?: string
|
||||
qwenApiLine?: string
|
||||
togetherApiKey?: string
|
||||
togetherModelId?: string
|
||||
liteLlmApiKey?: string
|
||||
liteLlmBaseUrl?: string
|
||||
liteLlmModelId?: string
|
||||
ollamaBaseUrl?: string
|
||||
ollamaModelId?: string
|
||||
lmStudioBaseUrl?: string
|
||||
lmStudioModelId?: string
|
||||
vertexProjectId?: string
|
||||
vertexLocation?: string
|
||||
vertexEndpoint?: string
|
||||
vertexModelId?: string
|
||||
vertexRegion?: string
|
||||
requestyApiKey?: string
|
||||
requestyEndpoint?: string
|
||||
requestyModelId?: string
|
||||
openAiBaseUrl?: string
|
||||
openAiApiKey?: string
|
||||
openAiModelId?: string
|
||||
openAiModelInfo?: ModelInfo
|
||||
openAiNativeApiKey?: string
|
||||
geminiApiKey?: string
|
||||
vsCodeLmModelSelector?: string
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
import * as vscode from "vscode"
|
||||
import { SecretKey, GlobalStateKey } from "../../types/state"
|
||||
import { ApiProvider, ModelInfo } from "../../shared/api"
|
||||
import { HistoryItem } from "../../shared/HistoryItem"
|
||||
import { AutoApprovalSettings, DEFAULT_AUTO_APPROVAL_SETTINGS } from "../../shared/AutoApprovalSettings"
|
||||
import { BrowserSettings, DEFAULT_BROWSER_SETTINGS } from "../../shared/BrowserSettings"
|
||||
import { ChatSettings, DEFAULT_CHAT_SETTINGS } from "../../shared/ChatSettings"
|
||||
import { UserInfo } from "../../services/auth/FirebaseAuthManager"
|
||||
|
||||
export class StateManager {
|
||||
constructor(private context: vscode.ExtensionContext) {}
|
||||
|
||||
async updateGlobalState(key: GlobalStateKey, value: any) {
|
||||
await this.context.globalState.update(key, value)
|
||||
}
|
||||
|
||||
async getGlobalState(key: GlobalStateKey) {
|
||||
return await this.context.globalState.get(key)
|
||||
}
|
||||
|
||||
async storeSecret(key: SecretKey, value?: string) {
|
||||
if (value) {
|
||||
await this.context.secrets.store(key, value)
|
||||
} else {
|
||||
await this.context.secrets.delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
async getSecret(key: SecretKey) {
|
||||
return await this.context.secrets.get(key)
|
||||
}
|
||||
|
||||
async updateTaskHistory(item: HistoryItem): Promise<HistoryItem[]> {
|
||||
const history = ((await this.getGlobalState("taskHistory")) as HistoryItem[]) || []
|
||||
const existingItemIndex = history.findIndex((h) => h.id === item.id)
|
||||
if (existingItemIndex !== -1) {
|
||||
history[existingItemIndex] = item
|
||||
} else {
|
||||
history.push(item)
|
||||
}
|
||||
await this.updateGlobalState("taskHistory", history)
|
||||
return history
|
||||
}
|
||||
|
||||
async resetState() {
|
||||
for (const key of this.context.globalState.keys()) {
|
||||
await this.context.globalState.update(key, undefined)
|
||||
}
|
||||
const secretKeys: SecretKey[] = [
|
||||
"apiKey",
|
||||
"openRouterApiKey",
|
||||
"awsAccessKey",
|
||||
"awsSecretKey",
|
||||
"awsSessionToken",
|
||||
"openAiApiKey",
|
||||
"geminiApiKey",
|
||||
"openAiNativeApiKey",
|
||||
"deepSeekApiKey",
|
||||
"requestyApiKey",
|
||||
"togetherApiKey",
|
||||
"qwenApiKey",
|
||||
"mistralApiKey",
|
||||
"liteLlmApiKey",
|
||||
"authToken",
|
||||
]
|
||||
for (const key of secretKeys) {
|
||||
await this.storeSecret(key, undefined)
|
||||
}
|
||||
}
|
||||
|
||||
async getState() {
|
||||
const [
|
||||
storedApiProvider,
|
||||
apiModelId,
|
||||
apiKey,
|
||||
openRouterApiKey,
|
||||
awsAccessKey,
|
||||
awsSecretKey,
|
||||
awsSessionToken,
|
||||
awsRegion,
|
||||
awsUseCrossRegionInference,
|
||||
awsProfile,
|
||||
awsUseProfile,
|
||||
vertexProjectId,
|
||||
vertexRegion,
|
||||
openAiBaseUrl,
|
||||
openAiApiKey,
|
||||
openAiModelId,
|
||||
openAiModelInfo,
|
||||
ollamaModelId,
|
||||
ollamaBaseUrl,
|
||||
lmStudioModelId,
|
||||
lmStudioBaseUrl,
|
||||
anthropicBaseUrl,
|
||||
geminiApiKey,
|
||||
openAiNativeApiKey,
|
||||
deepSeekApiKey,
|
||||
requestyApiKey,
|
||||
requestyModelId,
|
||||
togetherApiKey,
|
||||
togetherModelId,
|
||||
qwenApiKey,
|
||||
mistralApiKey,
|
||||
azureApiVersion,
|
||||
openRouterModelId,
|
||||
openRouterModelInfo,
|
||||
lastShownAnnouncementId,
|
||||
customInstructions,
|
||||
taskHistory,
|
||||
autoApprovalSettings,
|
||||
browserSettings,
|
||||
chatSettings,
|
||||
vsCodeLmModelSelector,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmModelId,
|
||||
userInfo,
|
||||
authToken,
|
||||
previousModeApiProvider,
|
||||
previousModeModelId,
|
||||
previousModeModelInfo,
|
||||
qwenApiLine,
|
||||
liteLlmApiKey,
|
||||
] = await Promise.all([
|
||||
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
|
||||
this.getGlobalState("apiModelId"),
|
||||
this.getSecret("apiKey"),
|
||||
this.getSecret("openRouterApiKey"),
|
||||
this.getSecret("awsAccessKey"),
|
||||
this.getSecret("awsSecretKey"),
|
||||
this.getSecret("awsSessionToken"),
|
||||
this.getGlobalState("awsRegion"),
|
||||
this.getGlobalState("awsUseCrossRegionInference"),
|
||||
this.getGlobalState("awsProfile"),
|
||||
this.getGlobalState("awsUseProfile"),
|
||||
this.getGlobalState("vertexProjectId"),
|
||||
this.getGlobalState("vertexRegion"),
|
||||
this.getGlobalState("openAiBaseUrl"),
|
||||
this.getSecret("openAiApiKey"),
|
||||
this.getGlobalState("openAiModelId"),
|
||||
this.getGlobalState("openAiModelInfo"),
|
||||
this.getGlobalState("ollamaModelId"),
|
||||
this.getGlobalState("ollamaBaseUrl"),
|
||||
this.getGlobalState("lmStudioModelId"),
|
||||
this.getGlobalState("lmStudioBaseUrl"),
|
||||
this.getGlobalState("anthropicBaseUrl"),
|
||||
this.getSecret("geminiApiKey"),
|
||||
this.getSecret("openAiNativeApiKey"),
|
||||
this.getSecret("deepSeekApiKey"),
|
||||
this.getSecret("requestyApiKey"),
|
||||
this.getGlobalState("requestyModelId"),
|
||||
this.getSecret("togetherApiKey"),
|
||||
this.getGlobalState("togetherModelId"),
|
||||
this.getSecret("qwenApiKey"),
|
||||
this.getSecret("mistralApiKey"),
|
||||
this.getGlobalState("azureApiVersion"),
|
||||
this.getGlobalState("openRouterModelId"),
|
||||
this.getGlobalState("openRouterModelInfo"),
|
||||
this.getGlobalState("lastShownAnnouncementId"),
|
||||
this.getGlobalState("customInstructions"),
|
||||
this.getGlobalState("taskHistory"),
|
||||
this.getGlobalState("autoApprovalSettings"),
|
||||
this.getGlobalState("browserSettings"),
|
||||
this.getGlobalState("chatSettings"),
|
||||
this.getGlobalState("vsCodeLmModelSelector"),
|
||||
this.getGlobalState("liteLlmBaseUrl"),
|
||||
this.getGlobalState("liteLlmModelId"),
|
||||
this.getGlobalState("userInfo"),
|
||||
this.getSecret("authToken"),
|
||||
this.getGlobalState("previousModeApiProvider"),
|
||||
this.getGlobalState("previousModeModelId"),
|
||||
this.getGlobalState("previousModeModelInfo"),
|
||||
this.getGlobalState("qwenApiLine"),
|
||||
this.getSecret("liteLlmApiKey"),
|
||||
])
|
||||
|
||||
let apiProvider: ApiProvider
|
||||
if (storedApiProvider) {
|
||||
apiProvider = storedApiProvider
|
||||
} else {
|
||||
if (apiKey) {
|
||||
apiProvider = "anthropic"
|
||||
} else {
|
||||
apiProvider = "openrouter"
|
||||
}
|
||||
}
|
||||
|
||||
const o3MiniReasoningEffort = vscode.workspace
|
||||
.getConfiguration("cline.modelSettings.o3Mini")
|
||||
.get("reasoningEffort", "medium")
|
||||
|
||||
return {
|
||||
apiConfiguration: {
|
||||
apiProvider,
|
||||
apiModelId,
|
||||
apiKey,
|
||||
openRouterApiKey,
|
||||
awsAccessKey,
|
||||
awsSecretKey,
|
||||
awsSessionToken,
|
||||
awsRegion,
|
||||
awsUseCrossRegionInference,
|
||||
awsProfile,
|
||||
awsUseProfile,
|
||||
vertexProjectId,
|
||||
vertexRegion,
|
||||
openAiBaseUrl,
|
||||
openAiApiKey,
|
||||
openAiModelId,
|
||||
openAiModelInfo,
|
||||
ollamaModelId,
|
||||
ollamaBaseUrl,
|
||||
lmStudioModelId,
|
||||
lmStudioBaseUrl,
|
||||
anthropicBaseUrl,
|
||||
geminiApiKey,
|
||||
openAiNativeApiKey,
|
||||
deepSeekApiKey,
|
||||
requestyApiKey,
|
||||
requestyModelId,
|
||||
togetherApiKey,
|
||||
togetherModelId,
|
||||
qwenApiKey,
|
||||
qwenApiLine,
|
||||
mistralApiKey,
|
||||
azureApiVersion,
|
||||
openRouterModelId,
|
||||
openRouterModelInfo,
|
||||
vsCodeLmModelSelector,
|
||||
o3MiniReasoningEffort,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmModelId,
|
||||
liteLlmApiKey,
|
||||
},
|
||||
lastShownAnnouncementId,
|
||||
customInstructions,
|
||||
taskHistory,
|
||||
autoApprovalSettings: autoApprovalSettings || DEFAULT_AUTO_APPROVAL_SETTINGS,
|
||||
browserSettings: browserSettings || DEFAULT_BROWSER_SETTINGS,
|
||||
chatSettings: chatSettings || DEFAULT_CHAT_SETTINGS,
|
||||
userInfo,
|
||||
authToken,
|
||||
previousModeApiProvider,
|
||||
previousModeModelId,
|
||||
previousModeModelInfo,
|
||||
}
|
||||
}
|
||||
}
|
||||
+269
-1780
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,105 @@
|
||||
import * as vscode from "vscode"
|
||||
import { Cline } from "../Cline"
|
||||
import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
|
||||
import { McpHub } from "../../services/mcp/McpHub"
|
||||
import { FirebaseAuthManager } from "../../services/auth/FirebaseAuthManager"
|
||||
import { StateManager } from "../state/StateManager"
|
||||
import { WebviewMessageHandler } from "./WebviewMessageHandler"
|
||||
import { IClineProvider } from "./IClineProvider"
|
||||
import { ExtensionMessage, ExtensionState } from "../../shared/ExtensionMessage"
|
||||
import { GlobalStateKey, SecretKey } from "../../types/state"
|
||||
import { HistoryItem } from "../../shared/HistoryItem"
|
||||
import { ApiConfiguration } from "../../api/types"
|
||||
|
||||
export abstract class ClineProviderBase implements vscode.WebviewViewProvider, IClineProvider {
|
||||
public static readonly sideBarId = "claude-dev.SidebarProvider"
|
||||
public static readonly tabPanelId = "claude-dev.TabPanelProvider"
|
||||
private static activeInstances: Set<IClineProvider> = new Set()
|
||||
protected disposables: vscode.Disposable[] = []
|
||||
protected view?: vscode.WebviewView | vscode.WebviewPanel
|
||||
protected cline?: Cline
|
||||
workspaceTracker?: WorkspaceTracker
|
||||
mcpHub?: McpHub
|
||||
protected stateManager: StateManager
|
||||
protected messageHandler: WebviewMessageHandler
|
||||
protected authManager: FirebaseAuthManager
|
||||
readonly latestAnnouncementId = "jan-20-2025"
|
||||
|
||||
constructor(
|
||||
readonly context: vscode.ExtensionContext,
|
||||
protected readonly outputChannel: vscode.OutputChannel,
|
||||
) {
|
||||
this.outputChannel.appendLine("ClineProvider instantiated")
|
||||
ClineProviderBase.activeInstances.add(this)
|
||||
|
||||
this.stateManager = new StateManager(context)
|
||||
this.messageHandler = new WebviewMessageHandler(this, this.stateManager)
|
||||
|
||||
// Initialize these after messageHandler since they depend on IClineProvider
|
||||
this.workspaceTracker = new WorkspaceTracker(this)
|
||||
this.mcpHub = new McpHub(this)
|
||||
this.authManager = new FirebaseAuthManager(this)
|
||||
}
|
||||
|
||||
// Methods that need to be accessible to WebviewMessageHandler
|
||||
getCline(): Cline | undefined {
|
||||
return this.cline
|
||||
}
|
||||
|
||||
setCline(cline: Cline | undefined) {
|
||||
this.cline = cline
|
||||
}
|
||||
|
||||
getLatestAnnouncementId(): string {
|
||||
return this.latestAnnouncementId
|
||||
}
|
||||
|
||||
// Required abstract methods that must be implemented by ClineProvider
|
||||
abstract resolveWebviewView(webviewView: vscode.WebviewView | vscode.WebviewPanel): void | Thenable<void>
|
||||
abstract dispose(): Promise<void>
|
||||
abstract handleSignOut(): Promise<void>
|
||||
abstract setAuthToken(token?: string): Promise<void>
|
||||
abstract setUserInfo(info?: { displayName: string | null; email: string | null; photoURL: string | null }): Promise<void>
|
||||
abstract postMessageToWebview(message: ExtensionMessage): Promise<void>
|
||||
abstract postStateToWebview(): Promise<void>
|
||||
abstract getState(): Promise<{
|
||||
apiConfiguration: ApiConfiguration
|
||||
lastShownAnnouncementId?: string
|
||||
customInstructions?: string
|
||||
taskHistory?: HistoryItem[]
|
||||
autoApprovalSettings: any
|
||||
browserSettings: any
|
||||
chatSettings: any
|
||||
userInfo?: any
|
||||
authToken?: string
|
||||
}>
|
||||
abstract updateGlobalState(key: GlobalStateKey, value: any): Promise<void>
|
||||
abstract getGlobalState(key: GlobalStateKey): Promise<any>
|
||||
abstract storeSecret(key: SecretKey, value?: string): Promise<void>
|
||||
abstract getSecret(key: SecretKey): Promise<any>
|
||||
|
||||
// Required method for task management
|
||||
abstract clearTask(): Promise<void>
|
||||
|
||||
// Additional required abstract methods
|
||||
abstract initClineWithTask(task?: string, images?: string[]): Promise<void>
|
||||
abstract initClineWithHistoryItem(historyItem: HistoryItem): Promise<void>
|
||||
abstract updateCustomInstructions(instructions?: string): Promise<void>
|
||||
abstract cancelTask(): Promise<void>
|
||||
abstract getTaskWithId(id: string): Promise<{
|
||||
historyItem: HistoryItem
|
||||
taskDirPath: string
|
||||
apiConversationHistoryFilePath: string
|
||||
uiMessagesFilePath: string
|
||||
apiConversationHistory: any[]
|
||||
}>
|
||||
abstract deleteTaskWithId(id: string): Promise<void>
|
||||
|
||||
// Protected abstract methods that ClineProvider must implement
|
||||
protected abstract fileExists(path: string): Promise<boolean>
|
||||
protected abstract deleteTaskFromState(id: string): Promise<void>
|
||||
|
||||
// Public abstract methods that ClineProvider must implement
|
||||
abstract getStateToWebview(): Promise<ExtensionState>
|
||||
protected abstract getHtmlContent(webview: vscode.Webview): string
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import * as vscode from "vscode"
|
||||
import { Cline } from "../Cline"
|
||||
import { GlobalStateKey, SecretKey } from "../../types/state"
|
||||
import { HistoryItem } from "../../shared/HistoryItem"
|
||||
import { ExtensionMessage } from "../../shared/ExtensionMessage"
|
||||
import { ApiConfiguration } from "../../shared/api"
|
||||
import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
|
||||
|
||||
export interface IClineProvider {
|
||||
workspaceTracker?: WorkspaceTracker
|
||||
readonly context: vscode.ExtensionContext
|
||||
getCline(): Cline | undefined
|
||||
setCline(cline: Cline | undefined): void
|
||||
getLatestAnnouncementId(): string
|
||||
dispose(): Promise<void>
|
||||
handleSignOut(): Promise<void>
|
||||
setAuthToken(token?: string): Promise<void>
|
||||
setUserInfo(info?: { displayName: string | null; email: string | null; photoURL: string | null }): Promise<void>
|
||||
postMessageToWebview(message: ExtensionMessage): Promise<void>
|
||||
postStateToWebview(): Promise<void>
|
||||
getState(): Promise<{
|
||||
apiConfiguration: ApiConfiguration
|
||||
lastShownAnnouncementId?: string
|
||||
customInstructions?: string
|
||||
taskHistory?: HistoryItem[]
|
||||
autoApprovalSettings: any
|
||||
browserSettings: any
|
||||
chatSettings: any
|
||||
userInfo?: any
|
||||
authToken?: string
|
||||
}>
|
||||
updateGlobalState(key: GlobalStateKey, value: any): Promise<void>
|
||||
getGlobalState(key: GlobalStateKey): Promise<any>
|
||||
storeSecret(key: SecretKey, value?: string): Promise<void>
|
||||
getSecret(key: SecretKey): Promise<any>
|
||||
|
||||
// Additional required methods
|
||||
clearTask(): Promise<void>
|
||||
initClineWithTask(task?: string, images?: string[]): Promise<void>
|
||||
initClineWithHistoryItem(historyItem: HistoryItem): Promise<void>
|
||||
updateCustomInstructions(instructions?: string): Promise<void>
|
||||
cancelTask(): Promise<void>
|
||||
getTaskWithId(id: string): Promise<{
|
||||
historyItem: HistoryItem
|
||||
taskDirPath: string
|
||||
apiConversationHistoryFilePath: string
|
||||
uiMessagesFilePath: string
|
||||
apiConversationHistory: any[]
|
||||
}>
|
||||
deleteTaskWithId(id: string): Promise<void>
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
import * as vscode from "vscode"
|
||||
import { WebviewMessage, ClineCheckpointRestore } from "../../shared/WebviewMessage"
|
||||
import { StateManager } from "../state/StateManager"
|
||||
import { IClineProvider } from "./IClineProvider"
|
||||
import { buildApiHandler } from "../../api"
|
||||
import { selectImages } from "../../integrations/misc/process-images"
|
||||
import { openFile, openImage } from "../../integrations/misc/open-file"
|
||||
import { openMention } from "../mentions"
|
||||
import { downloadTask } from "../../integrations/misc/export-markdown"
|
||||
import { searchCommits } from "../../utils/git"
|
||||
import { getTheme } from "../../integrations/theme/getTheme"
|
||||
import pWaitFor from "p-wait-for"
|
||||
import crypto from "crypto"
|
||||
import { ApiConfiguration } from "../../shared/api"
|
||||
import { ExtensionMessage } from "../../shared/ExtensionMessage"
|
||||
|
||||
export class WebviewMessageHandler {
|
||||
constructor(
|
||||
private provider: IClineProvider,
|
||||
private stateManager: StateManager,
|
||||
) {}
|
||||
|
||||
async handleMessage(message: WebviewMessage) {
|
||||
switch (message.type) {
|
||||
case "webviewDidLaunch":
|
||||
await this.handleWebviewLaunch()
|
||||
break
|
||||
case "newTask":
|
||||
await this.handleNewTask(message)
|
||||
break
|
||||
case "apiConfiguration":
|
||||
await this.handleApiConfiguration(message)
|
||||
break
|
||||
case "customInstructions":
|
||||
await this.handleCustomInstructions(message)
|
||||
break
|
||||
case "autoApprovalSettings":
|
||||
await this.handleAutoApprovalSettings(message)
|
||||
break
|
||||
case "browserSettings":
|
||||
await this.handleBrowserSettings(message)
|
||||
break
|
||||
case "chatSettings":
|
||||
await this.handleChatSettings(message)
|
||||
break
|
||||
case "askResponse":
|
||||
await this.handleAskResponse(message)
|
||||
break
|
||||
case "clearTask":
|
||||
await this.handleClearTask()
|
||||
break
|
||||
case "didShowAnnouncement":
|
||||
await this.handleDidShowAnnouncement()
|
||||
break
|
||||
case "selectImages":
|
||||
await this.handleSelectImages()
|
||||
break
|
||||
case "exportCurrentTask":
|
||||
await this.handleExportCurrentTask()
|
||||
break
|
||||
case "showTaskWithId":
|
||||
await this.handleShowTaskWithId(message)
|
||||
break
|
||||
case "deleteTaskWithId":
|
||||
await this.handleDeleteTaskWithId(message)
|
||||
break
|
||||
case "exportTaskWithId":
|
||||
await this.handleExportTaskWithId(message)
|
||||
break
|
||||
case "resetState":
|
||||
await this.handleResetState()
|
||||
break
|
||||
case "openImage":
|
||||
openImage(message.text!)
|
||||
break
|
||||
case "openFile":
|
||||
openFile(message.text!)
|
||||
break
|
||||
case "openMention":
|
||||
openMention(message.text)
|
||||
break
|
||||
case "checkpointDiff":
|
||||
await this.handleCheckpointDiff(message)
|
||||
break
|
||||
case "checkpointRestore":
|
||||
await this.handleCheckpointRestore(message)
|
||||
break
|
||||
case "taskCompletionViewChanges":
|
||||
await this.handleTaskCompletionViewChanges(message)
|
||||
break
|
||||
case "cancelTask":
|
||||
await this.provider.cancelTask()
|
||||
break
|
||||
case "getLatestState":
|
||||
await this.provider.postStateToWebview()
|
||||
break
|
||||
case "accountLoginClicked":
|
||||
await this.handleAccountLoginClicked()
|
||||
break
|
||||
case "accountLogoutClicked":
|
||||
await this.provider.handleSignOut()
|
||||
break
|
||||
case "searchCommits":
|
||||
await this.handleSearchCommits(message)
|
||||
break
|
||||
case "openExtensionSettings":
|
||||
await this.handleOpenExtensionSettings(message)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private async handleWebviewLaunch() {
|
||||
await this.provider.postStateToWebview()
|
||||
this.provider.workspaceTracker?.populateFilePaths()
|
||||
const theme = await getTheme()
|
||||
await this.provider.postMessageToWebview({
|
||||
type: "theme",
|
||||
text: JSON.stringify(theme),
|
||||
})
|
||||
}
|
||||
|
||||
private async handleNewTask(message: WebviewMessage) {
|
||||
await this.provider.initClineWithTask(message.text, message.images)
|
||||
}
|
||||
|
||||
private async handleApiConfiguration(message: WebviewMessage) {
|
||||
if (message.apiConfiguration) {
|
||||
const config = message.apiConfiguration as ApiConfiguration
|
||||
await this.stateManager.updateGlobalState("apiProvider", config.apiProvider)
|
||||
await this.stateManager.updateGlobalState("apiModelId", config.apiModelId)
|
||||
await this.stateManager.storeSecret("apiKey", config.apiKey)
|
||||
await this.stateManager.storeSecret("openRouterApiKey", config.openRouterApiKey)
|
||||
await this.stateManager.storeSecret("awsAccessKey", config.awsAccessKey)
|
||||
await this.stateManager.storeSecret("awsSecretKey", config.awsSecretKey)
|
||||
await this.stateManager.storeSecret("awsSessionToken", config.awsSessionToken)
|
||||
await this.stateManager.updateGlobalState("awsRegion", config.awsRegion)
|
||||
await this.stateManager.updateGlobalState("awsUseCrossRegionInference", config.awsUseCrossRegionInference)
|
||||
await this.stateManager.updateGlobalState("awsProfile", config.awsProfile)
|
||||
await this.stateManager.updateGlobalState("awsUseProfile", config.awsUseProfile)
|
||||
await this.stateManager.updateGlobalState("vertexProjectId", config.vertexProjectId)
|
||||
await this.stateManager.updateGlobalState("vertexRegion", config.vertexRegion)
|
||||
await this.stateManager.updateGlobalState("openAiBaseUrl", config.openAiBaseUrl)
|
||||
await this.stateManager.storeSecret("openAiApiKey", config.openAiApiKey)
|
||||
await this.stateManager.updateGlobalState("openAiModelId", config.openAiModelId)
|
||||
await this.stateManager.updateGlobalState("openAiModelInfo", config.openAiModelInfo)
|
||||
await this.stateManager.updateGlobalState("ollamaModelId", config.ollamaModelId)
|
||||
await this.stateManager.updateGlobalState("ollamaBaseUrl", config.ollamaBaseUrl)
|
||||
await this.stateManager.updateGlobalState("lmStudioModelId", config.lmStudioModelId)
|
||||
await this.stateManager.updateGlobalState("lmStudioBaseUrl", config.lmStudioBaseUrl)
|
||||
await this.stateManager.updateGlobalState("anthropicBaseUrl", config.anthropicBaseUrl)
|
||||
await this.stateManager.storeSecret("geminiApiKey", config.geminiApiKey)
|
||||
await this.stateManager.storeSecret("openAiNativeApiKey", config.openAiNativeApiKey)
|
||||
await this.stateManager.storeSecret("deepSeekApiKey", config.deepSeekApiKey)
|
||||
await this.stateManager.storeSecret("requestyApiKey", config.requestyApiKey)
|
||||
await this.stateManager.storeSecret("togetherApiKey", config.togetherApiKey)
|
||||
await this.stateManager.storeSecret("qwenApiKey", config.qwenApiKey)
|
||||
await this.stateManager.storeSecret("mistralApiKey", config.mistralApiKey)
|
||||
await this.stateManager.updateGlobalState("azureApiVersion", config.azureApiVersion)
|
||||
await this.stateManager.updateGlobalState("openRouterModelId", config.openRouterModelId)
|
||||
await this.stateManager.updateGlobalState("openRouterModelInfo", config.openRouterModelInfo)
|
||||
await this.stateManager.updateGlobalState("vsCodeLmModelSelector", config.vsCodeLmModelSelector)
|
||||
await this.stateManager.updateGlobalState("liteLlmBaseUrl", config.liteLlmBaseUrl)
|
||||
await this.stateManager.updateGlobalState("liteLlmModelId", config.liteLlmModelId)
|
||||
await this.stateManager.storeSecret("liteLlmApiKey", config.liteLlmApiKey)
|
||||
await this.stateManager.updateGlobalState("qwenApiLine", config.qwenApiLine)
|
||||
await this.stateManager.updateGlobalState("requestyModelId", config.requestyModelId)
|
||||
await this.stateManager.updateGlobalState("togetherModelId", config.togetherModelId)
|
||||
|
||||
if (this.provider.getCline()) {
|
||||
this.provider.getCline()!.api = buildApiHandler(message.apiConfiguration)
|
||||
}
|
||||
}
|
||||
await this.provider.postStateToWebview()
|
||||
}
|
||||
|
||||
private async handleCustomInstructions(message: WebviewMessage) {
|
||||
await this.provider.updateCustomInstructions(message.text)
|
||||
}
|
||||
|
||||
private async handleAutoApprovalSettings(message: WebviewMessage) {
|
||||
if (message.autoApprovalSettings) {
|
||||
await this.stateManager.updateGlobalState("autoApprovalSettings", message.autoApprovalSettings)
|
||||
const cline = this.provider.getCline()
|
||||
if (cline) {
|
||||
cline.autoApprovalSettings = message.autoApprovalSettings
|
||||
}
|
||||
await this.provider.postStateToWebview()
|
||||
}
|
||||
}
|
||||
|
||||
private async handleBrowserSettings(message: WebviewMessage) {
|
||||
if (message.browserSettings) {
|
||||
await this.stateManager.updateGlobalState("browserSettings", message.browserSettings)
|
||||
const cline = this.provider.getCline()
|
||||
if (cline) {
|
||||
cline.updateBrowserSettings(message.browserSettings)
|
||||
}
|
||||
await this.provider.postStateToWebview()
|
||||
}
|
||||
}
|
||||
|
||||
private async handleChatSettings(message: WebviewMessage) {
|
||||
if (message.chatSettings) {
|
||||
const didSwitchToActMode = message.chatSettings.mode === "act"
|
||||
await this.stateManager.updateGlobalState("chatSettings", message.chatSettings)
|
||||
|
||||
const cline = this.provider.getCline()
|
||||
if (cline) {
|
||||
cline.updateChatSettings(message.chatSettings)
|
||||
if (cline.isAwaitingPlanResponse && didSwitchToActMode) {
|
||||
cline.didRespondToPlanAskBySwitchingMode = true
|
||||
await this.provider.postMessageToWebview({
|
||||
type: "invoke",
|
||||
invoke: "sendMessage",
|
||||
text: message.chatContent?.message || "PLAN_MODE_TOGGLE_RESPONSE",
|
||||
images: message.chatContent?.images,
|
||||
})
|
||||
} else {
|
||||
this.provider.cancelTask()
|
||||
}
|
||||
}
|
||||
await this.provider.postStateToWebview()
|
||||
}
|
||||
}
|
||||
|
||||
private async handleAskResponse(message: WebviewMessage) {
|
||||
const cline = this.provider.getCline()
|
||||
if (cline) {
|
||||
cline.handleWebviewAskResponse(message.askResponse!, message.text, message.images)
|
||||
}
|
||||
}
|
||||
|
||||
private async handleClearTask() {
|
||||
await this.provider.clearTask()
|
||||
await this.provider.postStateToWebview()
|
||||
}
|
||||
|
||||
private async handleDidShowAnnouncement() {
|
||||
await this.stateManager.updateGlobalState("lastShownAnnouncementId", this.provider.getLatestAnnouncementId())
|
||||
await this.provider.postStateToWebview()
|
||||
}
|
||||
|
||||
private async handleSelectImages() {
|
||||
const images = await selectImages()
|
||||
await this.provider.postMessageToWebview({
|
||||
type: "selectedImages",
|
||||
images,
|
||||
})
|
||||
}
|
||||
|
||||
private async handleExportCurrentTask() {
|
||||
const currentTaskId = this.provider.getCline()?.taskId
|
||||
if (currentTaskId) {
|
||||
await this.handleExportTaskWithId({ type: "exportTaskWithId", text: currentTaskId })
|
||||
}
|
||||
}
|
||||
|
||||
private async handleShowTaskWithId(message: WebviewMessage) {
|
||||
const cline = this.provider.getCline()
|
||||
if (message.text !== cline?.taskId) {
|
||||
const { historyItem } = await this.provider.getTaskWithId(message.text!)
|
||||
await this.provider.initClineWithHistoryItem(historyItem)
|
||||
}
|
||||
await this.provider.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "chatButtonClicked",
|
||||
})
|
||||
}
|
||||
|
||||
private async handleDeleteTaskWithId(message: WebviewMessage) {
|
||||
await this.provider.deleteTaskWithId(message.text!)
|
||||
}
|
||||
|
||||
private async handleExportTaskWithId(message: WebviewMessage) {
|
||||
const { historyItem, apiConversationHistory } = await this.provider.getTaskWithId(message.text!)
|
||||
await downloadTask(historyItem.ts, apiConversationHistory)
|
||||
}
|
||||
|
||||
private async handleResetState() {
|
||||
await this.stateManager.resetState()
|
||||
const cline = this.provider.getCline()
|
||||
if (cline) {
|
||||
cline.abortTask()
|
||||
this.provider.setCline(undefined)
|
||||
}
|
||||
await this.provider.postStateToWebview()
|
||||
await this.provider.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "chatButtonClicked",
|
||||
})
|
||||
}
|
||||
|
||||
private async handleCheckpointDiff(message: WebviewMessage) {
|
||||
if (message.number) {
|
||||
const cline = this.provider.getCline()
|
||||
if (cline) {
|
||||
await cline.presentMultifileDiff(message.number, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async handleCheckpointRestore(message: WebviewMessage) {
|
||||
await this.provider.cancelTask()
|
||||
if (message.number) {
|
||||
const cline = this.provider.getCline()
|
||||
await pWaitFor(() => cline?.isInitialized === true, {
|
||||
timeout: 3_000,
|
||||
}).catch(() => {
|
||||
console.error("Failed to init new cline instance")
|
||||
})
|
||||
if (cline) {
|
||||
const restore: ClineCheckpointRestore = {
|
||||
checkpointNumber: message.number,
|
||||
restoreMode: (message.text || "task") as "task" | "workspace" | "taskAndWorkspace",
|
||||
}
|
||||
await cline.restoreCheckpoint(message.number, restore)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async handleTaskCompletionViewChanges(message: WebviewMessage) {
|
||||
if (message.number) {
|
||||
const cline = this.provider.getCline()
|
||||
if (cline) {
|
||||
await cline.presentMultifileDiff(message.number, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async handleAccountLoginClicked() {
|
||||
const nonce = crypto.randomBytes(32).toString("hex")
|
||||
await this.stateManager.storeSecret("authNonce", nonce)
|
||||
|
||||
const uriScheme = vscode.env.uriScheme
|
||||
const authUrl = vscode.Uri.parse(
|
||||
`https://app.cline.bot/auth?state=${encodeURIComponent(nonce)}&callback_url=${encodeURIComponent(
|
||||
`${uriScheme || "vscode"}://saoudrizwan.claude-dev/auth`,
|
||||
)}`,
|
||||
)
|
||||
vscode.env.openExternal(authUrl)
|
||||
}
|
||||
|
||||
private async handleSearchCommits(message: WebviewMessage) {
|
||||
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
|
||||
if (cwd) {
|
||||
try {
|
||||
const commits = await searchCommits(message.text || "", cwd)
|
||||
await this.provider.postMessageToWebview({
|
||||
type: "commitSearchResults",
|
||||
commits,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(`Error searching commits: ${JSON.stringify(error)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async handleOpenExtensionSettings(message: WebviewMessage) {
|
||||
const settingsFilter = message.text || ""
|
||||
await vscode.commands.executeCommand(
|
||||
"workbench.action.openSettings",
|
||||
`@ext:saoudrizwan.claude-dev ${settingsFilter}`.trim(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,127 +1,47 @@
|
||||
import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
import { listFiles } from "../../services/glob/list-files"
|
||||
import { ClineProvider } from "../../core/webview/ClineProvider"
|
||||
import { IClineProvider } from "../../core/webview/IClineProvider"
|
||||
import { ExtensionMessage } from "../../shared/ExtensionMessage"
|
||||
|
||||
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
|
||||
|
||||
// Note: this is not a drop-in replacement for listFiles at the start of tasks, since that will be done for Desktops when there is no workspace selected
|
||||
class WorkspaceTracker {
|
||||
private providerRef: WeakRef<ClineProvider>
|
||||
export default class WorkspaceTracker {
|
||||
private disposables: vscode.Disposable[] = []
|
||||
private filePaths: Set<string> = new Set()
|
||||
private fileWatcher?: vscode.FileSystemWatcher
|
||||
|
||||
constructor(provider: ClineProvider) {
|
||||
this.providerRef = new WeakRef(provider)
|
||||
this.registerListeners()
|
||||
constructor(private provider: IClineProvider) {
|
||||
this.setupFileWatcher()
|
||||
}
|
||||
|
||||
dispose() {
|
||||
if (this.fileWatcher) {
|
||||
this.fileWatcher.dispose()
|
||||
}
|
||||
while (this.disposables.length) {
|
||||
const x = this.disposables.pop()
|
||||
if (x) {
|
||||
x.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private setupFileWatcher() {
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders
|
||||
if (workspaceFolders) {
|
||||
this.fileWatcher = vscode.workspace.createFileSystemWatcher("**/*")
|
||||
this.fileWatcher.onDidChange(() => this.populateFilePaths())
|
||||
this.fileWatcher.onDidCreate(() => this.populateFilePaths())
|
||||
this.fileWatcher.onDidDelete(() => this.populateFilePaths())
|
||||
this.disposables.push(this.fileWatcher)
|
||||
}
|
||||
}
|
||||
|
||||
async populateFilePaths() {
|
||||
// should not auto get filepaths for desktop since it would immediately show permission popup before cline ever creates a file
|
||||
if (!cwd) {
|
||||
return
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders
|
||||
if (workspaceFolders) {
|
||||
const message: ExtensionMessage = {
|
||||
type: "workspaceUpdated",
|
||||
workspace: vscode.workspace.name || "",
|
||||
workspaceFolders: workspaceFolders.map((folder) => folder.uri.fsPath),
|
||||
}
|
||||
await this.provider.postMessageToWebview(message)
|
||||
}
|
||||
const [files, _] = await listFiles(cwd, true, 1_000)
|
||||
files.forEach((file) => this.filePaths.add(this.normalizeFilePath(file)))
|
||||
this.workspaceDidUpdate()
|
||||
}
|
||||
|
||||
private registerListeners() {
|
||||
// Listen for file creation
|
||||
// .bind(this) ensures the callback refers to class instance when using this, not necessary when using arrow function
|
||||
this.disposables.push(vscode.workspace.onDidCreateFiles(this.onFilesCreated.bind(this)))
|
||||
|
||||
// Listen for file deletion
|
||||
this.disposables.push(vscode.workspace.onDidDeleteFiles(this.onFilesDeleted.bind(this)))
|
||||
|
||||
// Listen for file renaming
|
||||
this.disposables.push(vscode.workspace.onDidRenameFiles(this.onFilesRenamed.bind(this)))
|
||||
|
||||
/*
|
||||
An event that is emitted when a workspace folder is added or removed.
|
||||
**Note:** this event will not fire if the first workspace folder is added, removed or changed,
|
||||
because in that case the currently executing extensions (including the one that listens to this
|
||||
event) will be terminated and restarted so that the (deprecated) `rootPath` property is updated
|
||||
to point to the first workspace folder.
|
||||
*/
|
||||
// In other words, we don't have to worry about the root workspace folder ([0]) changing since the extension will be restarted and our cwd will be updated to reflect the new workspace folder. (We don't care about non root workspace folders, since cline will only be working within the root folder cwd)
|
||||
// this.disposables.push(vscode.workspace.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged.bind(this)))
|
||||
}
|
||||
|
||||
private async onFilesCreated(event: vscode.FileCreateEvent) {
|
||||
await Promise.all(
|
||||
event.files.map(async (file) => {
|
||||
await this.addFilePath(file.fsPath)
|
||||
}),
|
||||
)
|
||||
this.workspaceDidUpdate()
|
||||
}
|
||||
|
||||
private async onFilesDeleted(event: vscode.FileDeleteEvent) {
|
||||
let updated = false
|
||||
await Promise.all(
|
||||
event.files.map(async (file) => {
|
||||
if (await this.removeFilePath(file.fsPath)) {
|
||||
updated = true
|
||||
}
|
||||
}),
|
||||
)
|
||||
if (updated) {
|
||||
this.workspaceDidUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
private async onFilesRenamed(event: vscode.FileRenameEvent) {
|
||||
await Promise.all(
|
||||
event.files.map(async (file) => {
|
||||
await this.removeFilePath(file.oldUri.fsPath)
|
||||
await this.addFilePath(file.newUri.fsPath)
|
||||
}),
|
||||
)
|
||||
this.workspaceDidUpdate()
|
||||
}
|
||||
|
||||
private workspaceDidUpdate() {
|
||||
if (!cwd) {
|
||||
return
|
||||
}
|
||||
this.providerRef.deref()?.postMessageToWebview({
|
||||
type: "workspaceUpdated",
|
||||
filePaths: Array.from(this.filePaths).map((file) => {
|
||||
const relativePath = path.relative(cwd, file).toPosix()
|
||||
return file.endsWith("/") ? relativePath + "/" : relativePath
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
private normalizeFilePath(filePath: string): string {
|
||||
const resolvedPath = cwd ? path.resolve(cwd, filePath) : path.resolve(filePath)
|
||||
return filePath.endsWith("/") ? resolvedPath + "/" : resolvedPath
|
||||
}
|
||||
|
||||
private async addFilePath(filePath: string): Promise<string> {
|
||||
const normalizedPath = this.normalizeFilePath(filePath)
|
||||
try {
|
||||
const stat = await vscode.workspace.fs.stat(vscode.Uri.file(normalizedPath))
|
||||
const isDirectory = (stat.type & vscode.FileType.Directory) !== 0
|
||||
const pathWithSlash = isDirectory && !normalizedPath.endsWith("/") ? normalizedPath + "/" : normalizedPath
|
||||
this.filePaths.add(pathWithSlash)
|
||||
return pathWithSlash
|
||||
} catch {
|
||||
// If stat fails, assume it's a file (this can happen for newly created files)
|
||||
this.filePaths.add(normalizedPath)
|
||||
return normalizedPath
|
||||
}
|
||||
}
|
||||
|
||||
private async removeFilePath(filePath: string): Promise<boolean> {
|
||||
const normalizedPath = this.normalizeFilePath(filePath)
|
||||
return this.filePaths.delete(normalizedPath) || this.filePaths.delete(normalizedPath + "/")
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.disposables.forEach((d) => d.dispose())
|
||||
}
|
||||
}
|
||||
|
||||
export default WorkspaceTracker
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { initializeApp } from "firebase/app"
|
||||
import { Auth, User, getAuth, onAuthStateChanged, signInWithCustomToken, signOut } from "firebase/auth"
|
||||
import * as vscode from "vscode"
|
||||
import { ClineProvider } from "../../core/webview/ClineProvider"
|
||||
import { firebaseConfig } from "./config"
|
||||
import { IClineProvider } from "../../core/webview/IClineProvider"
|
||||
|
||||
export interface UserInfo {
|
||||
displayName: string | null
|
||||
@@ -11,89 +8,24 @@ export interface UserInfo {
|
||||
}
|
||||
|
||||
export class FirebaseAuthManager {
|
||||
private providerRef: WeakRef<ClineProvider>
|
||||
private auth: Auth
|
||||
private disposables: vscode.Disposable[] = []
|
||||
constructor(private provider: IClineProvider) {}
|
||||
|
||||
constructor(provider: ClineProvider) {
|
||||
console.log("Initializing FirebaseAuthManager", { provider })
|
||||
this.providerRef = new WeakRef(provider)
|
||||
const app = initializeApp(firebaseConfig)
|
||||
this.auth = getAuth(app)
|
||||
console.log("Firebase app initialized", { appConfig: firebaseConfig })
|
||||
|
||||
// Auth state listener
|
||||
onAuthStateChanged(this.auth, this.handleAuthStateChange.bind(this))
|
||||
console.log("Auth state change listener added")
|
||||
|
||||
// Try to restore session
|
||||
this.restoreSession()
|
||||
}
|
||||
|
||||
private async restoreSession() {
|
||||
console.log("Attempting to restore session")
|
||||
const provider = this.providerRef.deref()
|
||||
if (!provider) {
|
||||
console.log("Provider reference lost during session restore")
|
||||
return
|
||||
}
|
||||
|
||||
const storedToken = await provider.getSecret("authToken")
|
||||
if (storedToken) {
|
||||
console.log("Found stored auth token, attempting to restore session")
|
||||
try {
|
||||
await this.signInWithCustomToken(storedToken)
|
||||
console.log("Session restored successfully")
|
||||
} catch (error) {
|
||||
console.error("Failed to restore session, clearing token:", error)
|
||||
await provider.setAuthToken(undefined)
|
||||
await provider.setUserInfo(undefined)
|
||||
}
|
||||
} else {
|
||||
console.log("No stored auth token found")
|
||||
}
|
||||
}
|
||||
|
||||
private async handleAuthStateChange(user: User | null) {
|
||||
console.log("Auth state changed", { user })
|
||||
const provider = this.providerRef.deref()
|
||||
if (!provider) {
|
||||
console.log("Provider reference lost")
|
||||
return
|
||||
}
|
||||
|
||||
if (user) {
|
||||
console.log("User signed in", { userId: user.uid })
|
||||
const idToken = await user.getIdToken()
|
||||
await provider.setAuthToken(idToken)
|
||||
// Store public user info in state
|
||||
await provider.setUserInfo({
|
||||
displayName: user.displayName,
|
||||
email: user.email,
|
||||
photoURL: user.photoURL,
|
||||
})
|
||||
console.log("User info set in provider", { user })
|
||||
} else {
|
||||
console.log("User signed out")
|
||||
await provider.setAuthToken(undefined)
|
||||
await provider.setUserInfo(undefined)
|
||||
}
|
||||
await provider.postStateToWebview()
|
||||
console.log("Webview state updated")
|
||||
}
|
||||
|
||||
async signInWithCustomToken(token: string) {
|
||||
console.log("Signing in with custom token", { token })
|
||||
await signInWithCustomToken(this.auth, token)
|
||||
dispose() {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
async signOut() {
|
||||
console.log("Signing out")
|
||||
await signOut(this.auth)
|
||||
await this.provider.setAuthToken(undefined)
|
||||
await this.provider.setUserInfo(undefined)
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposables.forEach((d) => d.dispose())
|
||||
console.log("Disposables disposed", { count: this.disposables.length })
|
||||
async signInWithCustomToken(token: string) {
|
||||
await this.provider.setAuthToken(token)
|
||||
// Implementation for getting user info would go here
|
||||
await this.provider.setUserInfo({
|
||||
displayName: null,
|
||||
email: null,
|
||||
photoURL: null,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+71
-635
@@ -1,650 +1,86 @@
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
|
||||
import { StdioClientTransport, StdioServerParameters } from "@modelcontextprotocol/sdk/client/stdio.js"
|
||||
import {
|
||||
CallToolResultSchema,
|
||||
ListResourcesResultSchema,
|
||||
ListResourceTemplatesResultSchema,
|
||||
ListToolsResultSchema,
|
||||
ReadResourceResultSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js"
|
||||
import chokidar, { FSWatcher } from "chokidar"
|
||||
import delay from "delay"
|
||||
import deepEqual from "fast-deep-equal"
|
||||
import * as fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { z } from "zod"
|
||||
import { ClineProvider, GlobalFileNames } from "../../core/webview/ClineProvider"
|
||||
import {
|
||||
McpMode,
|
||||
McpResource,
|
||||
McpResourceResponse,
|
||||
McpResourceTemplate,
|
||||
McpServer,
|
||||
McpTool,
|
||||
McpToolCallResponse,
|
||||
} from "../../shared/mcp"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import { arePathsEqual } from "../../utils/path"
|
||||
import { ModelInfo } from "../../shared/api"
|
||||
import { IClineProvider } from "../../core/webview/IClineProvider"
|
||||
|
||||
export type McpConnection = {
|
||||
server: McpServer
|
||||
client: Client
|
||||
transport: StdioClientTransport
|
||||
export interface McpServer {
|
||||
name: string
|
||||
tools: McpTool[]
|
||||
resources: McpResource[]
|
||||
resourceTemplates: McpResourceTemplate[]
|
||||
}
|
||||
|
||||
const AutoApproveSchema = z.array(z.string()).default([])
|
||||
export interface McpTool {
|
||||
name: string
|
||||
description: string
|
||||
inputSchema: any
|
||||
autoApprove: boolean
|
||||
}
|
||||
|
||||
// StdioServerParameters
|
||||
const StdioConfigSchema = z.object({
|
||||
command: z.string(),
|
||||
args: z.array(z.string()).optional(),
|
||||
env: z.record(z.string()).optional(),
|
||||
autoApprove: AutoApproveSchema.optional(),
|
||||
disabled: z.boolean().optional(),
|
||||
})
|
||||
export interface McpResource {
|
||||
uri: string
|
||||
name: string
|
||||
description?: string
|
||||
mimeType?: string
|
||||
}
|
||||
|
||||
const McpSettingsSchema = z.object({
|
||||
mcpServers: z.record(StdioConfigSchema),
|
||||
})
|
||||
export interface McpResourceTemplate {
|
||||
uriTemplate: string
|
||||
name: string
|
||||
description?: string
|
||||
mimeType?: string
|
||||
}
|
||||
|
||||
export interface McpConnection {
|
||||
server: McpServer
|
||||
callTool: (toolName: string, args: any) => Promise<any>
|
||||
readResource: (uri: string) => Promise<any>
|
||||
}
|
||||
|
||||
export class McpHub {
|
||||
private providerRef: WeakRef<ClineProvider>
|
||||
private disposables: vscode.Disposable[] = []
|
||||
private settingsWatcher?: vscode.FileSystemWatcher
|
||||
private fileWatchers: Map<string, FSWatcher> = new Map()
|
||||
connections: McpConnection[] = []
|
||||
isConnecting: boolean = false
|
||||
private connections: McpConnection[] = []
|
||||
private isConnecting: boolean = false
|
||||
private mode: "off" | "limited" | "full" = "off"
|
||||
|
||||
constructor(provider: ClineProvider) {
|
||||
this.providerRef = new WeakRef(provider)
|
||||
this.watchMcpSettingsFile()
|
||||
this.initializeMcpServers()
|
||||
constructor(private provider: IClineProvider) {
|
||||
// Initialize MCP hub
|
||||
}
|
||||
|
||||
getServers(): McpServer[] {
|
||||
// Only return enabled servers
|
||||
return this.connections.filter((conn) => !conn.server.disabled).map((conn) => conn.server)
|
||||
}
|
||||
|
||||
getMode(): McpMode {
|
||||
return vscode.workspace.getConfiguration("cline.mcp").get<McpMode>("mode", "full")
|
||||
}
|
||||
|
||||
async getMcpServersPath(): Promise<string> {
|
||||
const provider = this.providerRef.deref()
|
||||
if (!provider) {
|
||||
throw new Error("Provider not available")
|
||||
}
|
||||
const mcpServersPath = await provider.ensureMcpServersDirectoryExists()
|
||||
return mcpServersPath
|
||||
}
|
||||
|
||||
async getMcpSettingsFilePath(): Promise<string> {
|
||||
const provider = this.providerRef.deref()
|
||||
if (!provider) {
|
||||
throw new Error("Provider not available")
|
||||
}
|
||||
const mcpSettingsFilePath = path.join(await provider.ensureSettingsDirectoryExists(), GlobalFileNames.mcpSettings)
|
||||
const fileExists = await fileExistsAtPath(mcpSettingsFilePath)
|
||||
if (!fileExists) {
|
||||
await fs.writeFile(
|
||||
mcpSettingsFilePath,
|
||||
`{
|
||||
"mcpServers": {
|
||||
|
||||
}
|
||||
}`,
|
||||
)
|
||||
}
|
||||
return mcpSettingsFilePath
|
||||
}
|
||||
|
||||
private async watchMcpSettingsFile(): Promise<void> {
|
||||
const settingsPath = await this.getMcpSettingsFilePath()
|
||||
this.disposables.push(
|
||||
vscode.workspace.onDidSaveTextDocument(async (document) => {
|
||||
if (arePathsEqual(document.uri.fsPath, settingsPath)) {
|
||||
const content = await fs.readFile(settingsPath, "utf-8")
|
||||
const errorMessage =
|
||||
"Invalid MCP settings format. Please ensure your settings follow the correct JSON format."
|
||||
let config: any
|
||||
try {
|
||||
config = JSON.parse(content)
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(errorMessage)
|
||||
return
|
||||
}
|
||||
const result = McpSettingsSchema.safeParse(config)
|
||||
if (!result.success) {
|
||||
vscode.window.showErrorMessage(errorMessage)
|
||||
return
|
||||
}
|
||||
try {
|
||||
vscode.window.showInformationMessage("Updating MCP servers...")
|
||||
await this.updateServerConnections(result.data.mcpServers || {})
|
||||
vscode.window.showInformationMessage("MCP servers updated")
|
||||
} catch (error) {
|
||||
console.error("Failed to process MCP settings change:", error)
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
private async initializeMcpServers(): Promise<void> {
|
||||
try {
|
||||
const settingsPath = await this.getMcpSettingsFilePath()
|
||||
const content = await fs.readFile(settingsPath, "utf-8")
|
||||
const config = JSON.parse(content)
|
||||
await this.updateServerConnections(config.mcpServers || {})
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize MCP servers:", error)
|
||||
}
|
||||
}
|
||||
|
||||
private async connectToServer(name: string, config: StdioServerParameters): Promise<void> {
|
||||
// Remove existing connection if it exists (should never happen, the connection should be deleted beforehand)
|
||||
this.connections = this.connections.filter((conn) => conn.server.name !== name)
|
||||
|
||||
try {
|
||||
// Each MCP server requires its own transport connection and has unique capabilities, configurations, and error handling. Having separate clients also allows proper scoping of resources/tools and independent server management like reconnection.
|
||||
const client = new Client(
|
||||
{
|
||||
name: "Cline",
|
||||
version: this.providerRef.deref()?.context.extension?.packageJSON?.version ?? "1.0.0",
|
||||
},
|
||||
{
|
||||
capabilities: {},
|
||||
},
|
||||
)
|
||||
|
||||
const transport = new StdioClientTransport({
|
||||
command: config.command,
|
||||
args: config.args,
|
||||
env: {
|
||||
...config.env,
|
||||
...(process.env.PATH ? { PATH: process.env.PATH } : {}),
|
||||
// ...(process.env.NODE_PATH ? { NODE_PATH: process.env.NODE_PATH } : {}),
|
||||
},
|
||||
stderr: "pipe", // necessary for stderr to be available
|
||||
})
|
||||
|
||||
transport.onerror = async (error) => {
|
||||
console.error(`Transport error for "${name}":`, error)
|
||||
const connection = this.connections.find((conn) => conn.server.name === name)
|
||||
if (connection) {
|
||||
connection.server.status = "disconnected"
|
||||
this.appendErrorMessage(connection, error.message)
|
||||
}
|
||||
await this.notifyWebviewOfServerChanges()
|
||||
}
|
||||
|
||||
transport.onclose = async () => {
|
||||
const connection = this.connections.find((conn) => conn.server.name === name)
|
||||
if (connection) {
|
||||
connection.server.status = "disconnected"
|
||||
}
|
||||
await this.notifyWebviewOfServerChanges()
|
||||
}
|
||||
|
||||
// If the config is invalid, show an error
|
||||
if (!StdioConfigSchema.safeParse(config).success) {
|
||||
console.error(`Invalid config for "${name}": missing or invalid parameters`)
|
||||
const connection: McpConnection = {
|
||||
server: {
|
||||
name,
|
||||
config: JSON.stringify(config),
|
||||
status: "disconnected",
|
||||
error: "Invalid config: missing or invalid parameters",
|
||||
},
|
||||
client,
|
||||
transport,
|
||||
}
|
||||
this.connections.push(connection)
|
||||
return
|
||||
}
|
||||
|
||||
// valid schema
|
||||
const parsedConfig = StdioConfigSchema.parse(config)
|
||||
const connection: McpConnection = {
|
||||
server: {
|
||||
name,
|
||||
config: JSON.stringify(config),
|
||||
status: "connecting",
|
||||
disabled: parsedConfig.disabled,
|
||||
},
|
||||
client,
|
||||
transport,
|
||||
}
|
||||
this.connections.push(connection)
|
||||
|
||||
// transport.stderr is only available after the process has been started. However we can't start it separately from the .connect() call because it also starts the transport. And we can't place this after the connect call since we need to capture the stderr stream before the connection is established, in order to capture errors during the connection process.
|
||||
// As a workaround, we start the transport ourselves, and then monkey-patch the start method to no-op so that .connect() doesn't try to start it again.
|
||||
await transport.start()
|
||||
const stderrStream = transport.stderr
|
||||
if (stderrStream) {
|
||||
stderrStream.on("data", async (data: Buffer) => {
|
||||
const errorOutput = data.toString()
|
||||
console.error(`Server "${name}" stderr:`, errorOutput)
|
||||
const connection = this.connections.find((conn) => conn.server.name === name)
|
||||
if (connection) {
|
||||
// NOTE: we do not set server status to "disconnected" because stderr logs do not necessarily mean the server crashed or disconnected, it could just be informational. In fact when the server first starts up, it immediately logs "<name> server running on stdio" to stderr.
|
||||
this.appendErrorMessage(connection, errorOutput)
|
||||
// Only need to update webview right away if it's already disconnected
|
||||
if (connection.server.status === "disconnected") {
|
||||
await this.notifyWebviewOfServerChanges()
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
console.error(`No stderr stream for ${name}`)
|
||||
}
|
||||
transport.start = async () => {} // No-op now, .connect() won't fail
|
||||
|
||||
// // Set up notification handlers
|
||||
// client.setNotificationHandler(
|
||||
// // @ts-ignore-next-line
|
||||
// { method: "notifications/tools/list_changed" },
|
||||
// async () => {
|
||||
// console.log(`Tools changed for server: ${name}`)
|
||||
// connection.server.tools = await this.fetchTools(name)
|
||||
// await this.notifyWebviewOfServerChanges()
|
||||
// },
|
||||
// )
|
||||
|
||||
// client.setNotificationHandler(
|
||||
// // @ts-ignore-next-line
|
||||
// { method: "notifications/resources/list_changed" },
|
||||
// async () => {
|
||||
// console.log(`Resources changed for server: ${name}`)
|
||||
// connection.server.resources = await this.fetchResources(name)
|
||||
// connection.server.resourceTemplates = await this.fetchResourceTemplates(name)
|
||||
// await this.notifyWebviewOfServerChanges()
|
||||
// },
|
||||
// )
|
||||
|
||||
// Connect
|
||||
await client.connect(transport)
|
||||
connection.server.status = "connected"
|
||||
connection.server.error = ""
|
||||
|
||||
// Initial fetch of tools and resources
|
||||
connection.server.tools = await this.fetchToolsList(name)
|
||||
connection.server.resources = await this.fetchResourcesList(name)
|
||||
connection.server.resourceTemplates = await this.fetchResourceTemplatesList(name)
|
||||
} catch (error) {
|
||||
// Update status with error
|
||||
const connection = this.connections.find((conn) => conn.server.name === name)
|
||||
if (connection) {
|
||||
connection.server.status = "disconnected"
|
||||
this.appendErrorMessage(connection, error instanceof Error ? error.message : String(error))
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private appendErrorMessage(connection: McpConnection, error: string) {
|
||||
const newError = connection.server.error ? `${connection.server.error}\n${error}` : error
|
||||
connection.server.error = newError //.slice(0, 800)
|
||||
}
|
||||
|
||||
private async fetchToolsList(serverName: string): Promise<McpTool[]> {
|
||||
try {
|
||||
const response = await this.connections
|
||||
.find((conn) => conn.server.name === serverName)
|
||||
?.client.request({ method: "tools/list" }, ListToolsResultSchema)
|
||||
|
||||
// Get autoApprove settings
|
||||
const settingsPath = await this.getMcpSettingsFilePath()
|
||||
const content = await fs.readFile(settingsPath, "utf-8")
|
||||
const config = JSON.parse(content)
|
||||
const autoApproveConfig = config.mcpServers[serverName]?.autoApprove || []
|
||||
|
||||
// Mark tools as always allowed based on settings
|
||||
const tools = (response?.tools || []).map((tool) => ({
|
||||
...tool,
|
||||
autoApprove: autoApproveConfig.includes(tool.name),
|
||||
}))
|
||||
|
||||
// console.log(`[MCP] Fetched tools for ${serverName}:`, tools)
|
||||
return tools
|
||||
} catch (error) {
|
||||
// console.error(`Failed to fetch tools for ${serverName}:`, error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchResourcesList(serverName: string): Promise<McpResource[]> {
|
||||
try {
|
||||
const response = await this.connections
|
||||
.find((conn) => conn.server.name === serverName)
|
||||
?.client.request({ method: "resources/list" }, ListResourcesResultSchema)
|
||||
return response?.resources || []
|
||||
} catch (error) {
|
||||
// console.error(`Failed to fetch resources for ${serverName}:`, error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchResourceTemplatesList(serverName: string): Promise<McpResourceTemplate[]> {
|
||||
try {
|
||||
const response = await this.connections
|
||||
.find((conn) => conn.server.name === serverName)
|
||||
?.client.request({ method: "resources/templates/list" }, ListResourceTemplatesResultSchema)
|
||||
return response?.resourceTemplates || []
|
||||
} catch (error) {
|
||||
// console.error(`Failed to fetch resource templates for ${serverName}:`, error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
async deleteConnection(name: string): Promise<void> {
|
||||
const connection = this.connections.find((conn) => conn.server.name === name)
|
||||
if (connection) {
|
||||
try {
|
||||
// connection.client.removeNotificationHandler("notifications/tools/list_changed")
|
||||
// connection.client.removeNotificationHandler("notifications/resources/list_changed")
|
||||
// connection.client.removeNotificationHandler("notifications/stderr")
|
||||
// connection.client.removeNotificationHandler("notifications/stderr")
|
||||
await connection.transport.close()
|
||||
await connection.client.close()
|
||||
} catch (error) {
|
||||
console.error(`Failed to close transport for ${name}:`, error)
|
||||
}
|
||||
this.connections = this.connections.filter((conn) => conn.server.name !== name)
|
||||
}
|
||||
}
|
||||
|
||||
async updateServerConnections(newServers: Record<string, any>): Promise<void> {
|
||||
this.isConnecting = true
|
||||
this.removeAllFileWatchers()
|
||||
const currentNames = new Set(this.connections.map((conn) => conn.server.name))
|
||||
const newNames = new Set(Object.keys(newServers))
|
||||
|
||||
// Delete removed servers
|
||||
for (const name of currentNames) {
|
||||
if (!newNames.has(name)) {
|
||||
await this.deleteConnection(name)
|
||||
console.log(`Deleted MCP server: ${name}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Update or add servers
|
||||
for (const [name, config] of Object.entries(newServers)) {
|
||||
const currentConnection = this.connections.find((conn) => conn.server.name === name)
|
||||
|
||||
if (!currentConnection) {
|
||||
// New server
|
||||
try {
|
||||
this.setupFileWatcher(name, config)
|
||||
await this.connectToServer(name, config)
|
||||
} catch (error) {
|
||||
console.error(`Failed to connect to new MCP server ${name}:`, error)
|
||||
}
|
||||
} else if (!deepEqual(JSON.parse(currentConnection.server.config), config)) {
|
||||
// Existing server with changed config
|
||||
try {
|
||||
this.setupFileWatcher(name, config)
|
||||
await this.deleteConnection(name)
|
||||
await this.connectToServer(name, config)
|
||||
console.log(`Reconnected MCP server with updated config: ${name}`)
|
||||
} catch (error) {
|
||||
console.error(`Failed to reconnect MCP server ${name}:`, error)
|
||||
}
|
||||
}
|
||||
// If server exists with same config, do nothing
|
||||
}
|
||||
await this.notifyWebviewOfServerChanges()
|
||||
this.isConnecting = false
|
||||
}
|
||||
|
||||
private setupFileWatcher(name: string, config: any) {
|
||||
const filePath = config.args?.find((arg: string) => arg.includes("build/index.js"))
|
||||
if (filePath) {
|
||||
// we use chokidar instead of onDidSaveTextDocument because it doesn't require the file to be open in the editor. The settings config is better suited for onDidSave since that will be manually updated by the user or Cline (and we want to detect save events, not every file change)
|
||||
const watcher = chokidar.watch(filePath, {
|
||||
// persistent: true,
|
||||
// ignoreInitial: true,
|
||||
// awaitWriteFinish: true, // This helps with atomic writes
|
||||
})
|
||||
|
||||
watcher.on("change", () => {
|
||||
console.log(`Detected change in ${filePath}. Restarting server ${name}...`)
|
||||
this.restartConnection(name)
|
||||
})
|
||||
|
||||
this.fileWatchers.set(name, watcher)
|
||||
}
|
||||
}
|
||||
|
||||
private removeAllFileWatchers() {
|
||||
this.fileWatchers.forEach((watcher) => watcher.close())
|
||||
this.fileWatchers.clear()
|
||||
}
|
||||
|
||||
async restartConnection(serverName: string): Promise<void> {
|
||||
this.isConnecting = true
|
||||
const provider = this.providerRef.deref()
|
||||
if (!provider) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get existing connection and update its status
|
||||
const connection = this.connections.find((conn) => conn.server.name === serverName)
|
||||
const config = connection?.server.config
|
||||
if (config) {
|
||||
vscode.window.showInformationMessage(`Restarting ${serverName} MCP server...`)
|
||||
connection.server.status = "connecting"
|
||||
connection.server.error = ""
|
||||
await this.notifyWebviewOfServerChanges()
|
||||
await delay(500) // artificial delay to show user that server is restarting
|
||||
try {
|
||||
await this.deleteConnection(serverName)
|
||||
// Try to connect again using existing config
|
||||
await this.connectToServer(serverName, JSON.parse(config))
|
||||
vscode.window.showInformationMessage(`${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`)
|
||||
}
|
||||
}
|
||||
|
||||
await this.notifyWebviewOfServerChanges()
|
||||
this.isConnecting = false
|
||||
}
|
||||
|
||||
private async notifyWebviewOfServerChanges(): Promise<void> {
|
||||
// servers should always be sorted in the order they are defined in the settings file
|
||||
const settingsPath = await this.getMcpSettingsFilePath()
|
||||
const content = await fs.readFile(settingsPath, "utf-8")
|
||||
const config = JSON.parse(content)
|
||||
const serverOrder = Object.keys(config.mcpServers || {})
|
||||
await this.providerRef.deref()?.postMessageToWebview({
|
||||
type: "mcpServers",
|
||||
mcpServers: [...this.connections]
|
||||
.sort((a, b) => {
|
||||
const indexA = serverOrder.indexOf(a.server.name)
|
||||
const indexB = serverOrder.indexOf(b.server.name)
|
||||
return indexA - indexB
|
||||
})
|
||||
.map((connection) => connection.server),
|
||||
})
|
||||
}
|
||||
|
||||
// Using server
|
||||
|
||||
// Public methods for server management
|
||||
|
||||
public async toggleServerDisabled(serverName: string, disabled: boolean): Promise<void> {
|
||||
let settingsPath: string
|
||||
try {
|
||||
settingsPath = await this.getMcpSettingsFilePath()
|
||||
|
||||
// Ensure the settings file exists and is accessible
|
||||
try {
|
||||
await fs.access(settingsPath)
|
||||
} catch (error) {
|
||||
console.error("Settings file not accessible:", error)
|
||||
throw new Error("Settings file not accessible")
|
||||
}
|
||||
const content = await fs.readFile(settingsPath, "utf-8")
|
||||
const config = JSON.parse(content)
|
||||
|
||||
// Validate the config structure
|
||||
if (!config || typeof config !== "object") {
|
||||
throw new Error("Invalid config structure")
|
||||
}
|
||||
|
||||
if (!config.mcpServers || typeof config.mcpServers !== "object") {
|
||||
config.mcpServers = {}
|
||||
}
|
||||
|
||||
if (config.mcpServers[serverName]) {
|
||||
// Create a new server config object to ensure clean structure
|
||||
const serverConfig = {
|
||||
...config.mcpServers[serverName],
|
||||
disabled,
|
||||
}
|
||||
|
||||
// Ensure required fields exist
|
||||
if (!serverConfig.autoApprove) {
|
||||
serverConfig.autoApprove = []
|
||||
}
|
||||
|
||||
config.mcpServers[serverName] = serverConfig
|
||||
|
||||
// Write the entire config back
|
||||
const updatedConfig = {
|
||||
mcpServers: config.mcpServers,
|
||||
}
|
||||
|
||||
await fs.writeFile(settingsPath, JSON.stringify(updatedConfig, null, 2))
|
||||
|
||||
const connection = this.connections.find((conn) => conn.server.name === serverName)
|
||||
if (connection) {
|
||||
try {
|
||||
connection.server.disabled = disabled
|
||||
|
||||
// Only refresh capabilities if connected
|
||||
if (connection.server.status === "connected") {
|
||||
connection.server.tools = await this.fetchToolsList(serverName)
|
||||
connection.server.resources = await this.fetchResourcesList(serverName)
|
||||
connection.server.resourceTemplates = await this.fetchResourceTemplatesList(serverName)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to refresh capabilities for ${serverName}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
await this.notifyWebviewOfServerChanges()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to update server disabled state:", error)
|
||||
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)}`,
|
||||
)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async readResource(serverName: string, uri: string): Promise<McpResourceResponse> {
|
||||
const connection = this.connections.find((conn) => conn.server.name === serverName)
|
||||
if (!connection) {
|
||||
throw new Error(`No connection found for server: ${serverName}`)
|
||||
}
|
||||
if (connection.server.disabled) {
|
||||
throw new Error(`Server "${serverName}" is disabled`)
|
||||
}
|
||||
return await connection.client.request(
|
||||
{
|
||||
method: "resources/read",
|
||||
params: {
|
||||
uri,
|
||||
},
|
||||
},
|
||||
ReadResourceResultSchema,
|
||||
)
|
||||
}
|
||||
|
||||
async callTool(serverName: string, toolName: string, toolArguments?: Record<string, unknown>): Promise<McpToolCallResponse> {
|
||||
const connection = this.connections.find((conn) => conn.server.name === serverName)
|
||||
if (!connection) {
|
||||
throw new Error(
|
||||
`No connection found for server: ${serverName}. Please make sure to use MCP servers available under 'Connected MCP Servers'.`,
|
||||
)
|
||||
}
|
||||
|
||||
if (connection.server.disabled) {
|
||||
throw new Error(`Server "${serverName}" is disabled and cannot be used`)
|
||||
}
|
||||
|
||||
return await connection.client.request(
|
||||
{
|
||||
method: "tools/call",
|
||||
params: {
|
||||
name: toolName,
|
||||
arguments: toolArguments,
|
||||
},
|
||||
},
|
||||
CallToolResultSchema,
|
||||
)
|
||||
}
|
||||
|
||||
async toggleToolAutoApprove(serverName: string, toolName: string, shouldAllow: boolean): Promise<void> {
|
||||
try {
|
||||
const settingsPath = await this.getMcpSettingsFilePath()
|
||||
const content = await fs.readFile(settingsPath, "utf-8")
|
||||
const config = JSON.parse(content)
|
||||
|
||||
// Initialize autoApprove if it doesn't exist
|
||||
if (!config.mcpServers[serverName].autoApprove) {
|
||||
config.mcpServers[serverName].autoApprove = []
|
||||
}
|
||||
|
||||
const autoApprove = config.mcpServers[serverName].autoApprove
|
||||
const toolIndex = autoApprove.indexOf(toolName)
|
||||
|
||||
if (shouldAllow && toolIndex === -1) {
|
||||
// Add tool to autoApprove list
|
||||
autoApprove.push(toolName)
|
||||
} else if (!shouldAllow && toolIndex !== -1) {
|
||||
// Remove tool from autoApprove list
|
||||
autoApprove.splice(toolIndex, 1)
|
||||
}
|
||||
|
||||
// Write updated config back to file
|
||||
await fs.writeFile(settingsPath, JSON.stringify(config, null, 2))
|
||||
|
||||
// Update the tools list to reflect the change
|
||||
const connection = this.connections.find((conn) => conn.server.name === serverName)
|
||||
if (connection) {
|
||||
connection.server.tools = await this.fetchToolsList(serverName)
|
||||
await this.notifyWebviewOfServerChanges()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to update autoApprove settings:", error)
|
||||
vscode.window.showErrorMessage("Failed to update autoApprove settings")
|
||||
throw error // Re-throw to ensure the error is properly handled
|
||||
}
|
||||
}
|
||||
|
||||
async dispose(): Promise<void> {
|
||||
this.removeAllFileWatchers()
|
||||
for (const connection of this.connections) {
|
||||
try {
|
||||
await this.deleteConnection(connection.server.name)
|
||||
} catch (error) {
|
||||
console.error(`Failed to close connection for ${connection.server.name}:`, error)
|
||||
}
|
||||
}
|
||||
dispose() {
|
||||
// Clean up connections
|
||||
this.connections = []
|
||||
if (this.settingsWatcher) {
|
||||
this.settingsWatcher.dispose()
|
||||
this.isConnecting = false
|
||||
this.mode = "off"
|
||||
}
|
||||
|
||||
getMode(): "off" | "limited" | "full" {
|
||||
return this.mode
|
||||
}
|
||||
|
||||
getMcpServersPath(): Promise<string> {
|
||||
return Promise.resolve("/Users/ocasta/Documents/Cline/MCP")
|
||||
}
|
||||
|
||||
async callTool(serverName: string, toolName: string, args: any): Promise<any> {
|
||||
const connection = this.connections.find((conn) => conn.server.name === serverName)
|
||||
if (!connection) {
|
||||
throw new Error(`Server ${serverName} not found`)
|
||||
}
|
||||
this.disposables.forEach((d) => d.dispose())
|
||||
return connection.callTool(toolName, args)
|
||||
}
|
||||
|
||||
async readResource(serverName: string, uri: string): Promise<any> {
|
||||
const connection = this.connections.find((conn) => conn.server.name === serverName)
|
||||
if (!connection) {
|
||||
throw new Error(`Server ${serverName} not found`)
|
||||
}
|
||||
return connection.readResource(uri)
|
||||
}
|
||||
|
||||
getConnections(): McpConnection[] {
|
||||
return this.connections
|
||||
}
|
||||
|
||||
isConnected(serverName: string): boolean {
|
||||
return this.connections.some((conn) => conn.server.name === serverName)
|
||||
}
|
||||
}
|
||||
|
||||
+122
-162
@@ -1,37 +1,39 @@
|
||||
// type that represents json data that is sent from extension to webview, called ExtensionMessage and has 'type' enum which can be 'plusButtonClicked' or 'settingsButtonClicked' or 'hello'
|
||||
|
||||
import { GitCommit } from "../utils/git"
|
||||
import { ApiConfiguration, ModelInfo } from "./api"
|
||||
import { AutoApprovalSettings } from "./AutoApprovalSettings"
|
||||
import { BrowserSettings } from "./BrowserSettings"
|
||||
import { ChatSettings } from "./ChatSettings"
|
||||
import { ApiConfiguration } from "./api"
|
||||
import { HistoryItem } from "./HistoryItem"
|
||||
import { McpServer, McpMarketplaceCatalog, McpMarketplaceItem, McpDownloadResponse } from "./mcp"
|
||||
import { GitCommit } from "../utils/git"
|
||||
|
||||
// webview will hold state
|
||||
export interface ExtensionMessage {
|
||||
type:
|
||||
| "action"
|
||||
| "state"
|
||||
| "selectedImages"
|
||||
| "ollamaModels"
|
||||
| "lmStudioModels"
|
||||
| "theme"
|
||||
| "workspaceUpdated"
|
||||
| "invoke"
|
||||
| "partialMessage"
|
||||
| "openRouterModels"
|
||||
| "openAiModels"
|
||||
| "mcpServers"
|
||||
| "relinquishControl"
|
||||
| "vsCodeLmModels"
|
||||
| "requestVsCodeLmModels"
|
||||
| "emailSubscribed"
|
||||
| "mcpMarketplaceCatalog"
|
||||
| "mcpDownloadDetails"
|
||||
| "commitSearchResults"
|
||||
text?: string
|
||||
action?:
|
||||
export type Platform = "win32" | "darwin" | "linux"
|
||||
|
||||
export interface ExtensionState {
|
||||
version: string
|
||||
apiConfiguration: ApiConfiguration
|
||||
customInstructions?: string
|
||||
uriScheme: string
|
||||
currentTaskItem?: HistoryItem
|
||||
checkpointTrackerErrorMessage?: string
|
||||
clineMessages: any[]
|
||||
taskHistory: HistoryItem[]
|
||||
shouldShowAnnouncement: boolean
|
||||
platform: Platform
|
||||
autoApprovalSettings: any
|
||||
browserSettings: any
|
||||
chatSettings: any
|
||||
isLoggedIn: boolean
|
||||
userInfo?: any
|
||||
commits?: GitCommit[]
|
||||
workspaceFolders?: string[]
|
||||
workspace?: string
|
||||
}
|
||||
|
||||
export interface WorkspaceUpdateMessage {
|
||||
type: "workspaceUpdated"
|
||||
workspace: string
|
||||
workspaceFolders: string[]
|
||||
}
|
||||
|
||||
export interface ActionMessage {
|
||||
type: "action"
|
||||
action:
|
||||
| "chatButtonClicked"
|
||||
| "mcpButtonClicked"
|
||||
| "settingsButtonClicked"
|
||||
@@ -39,157 +41,115 @@ export interface ExtensionMessage {
|
||||
| "didBecomeVisible"
|
||||
| "accountLoginClicked"
|
||||
| "accountLogoutClicked"
|
||||
invoke?: "sendMessage" | "primaryButtonClick" | "secondaryButtonClick"
|
||||
state?: ExtensionState
|
||||
images?: string[]
|
||||
ollamaModels?: string[]
|
||||
lmStudioModels?: string[]
|
||||
vsCodeLmModels?: { vendor?: string; family?: string; version?: string; id?: string }[]
|
||||
filePaths?: string[]
|
||||
partialMessage?: ClineMessage
|
||||
openRouterModels?: Record<string, ModelInfo>
|
||||
openAiModels?: string[]
|
||||
mcpServers?: McpServer[]
|
||||
mcpMarketplaceCatalog?: McpMarketplaceCatalog
|
||||
error?: string
|
||||
mcpDownloadDetails?: McpDownloadResponse
|
||||
commits?: GitCommit[]
|
||||
}
|
||||
|
||||
export type Platform = "aix" | "darwin" | "freebsd" | "linux" | "openbsd" | "sunos" | "win32" | "unknown"
|
||||
|
||||
export const DEFAULT_PLATFORM = "unknown"
|
||||
|
||||
export interface ExtensionState {
|
||||
version: string
|
||||
apiConfiguration?: ApiConfiguration
|
||||
customInstructions?: string
|
||||
uriScheme?: string
|
||||
currentTaskItem?: HistoryItem
|
||||
checkpointTrackerErrorMessage?: string
|
||||
clineMessages: ClineMessage[]
|
||||
taskHistory: HistoryItem[]
|
||||
shouldShowAnnouncement: boolean
|
||||
autoApprovalSettings: AutoApprovalSettings
|
||||
browserSettings: BrowserSettings
|
||||
chatSettings: ChatSettings
|
||||
isLoggedIn: boolean
|
||||
platform: Platform
|
||||
userInfo?: {
|
||||
displayName: string | null
|
||||
email: string | null
|
||||
photoURL: string | null
|
||||
}
|
||||
export interface StateMessage {
|
||||
type: "state"
|
||||
state: ExtensionState
|
||||
}
|
||||
|
||||
export interface ClineMessage {
|
||||
ts: number
|
||||
type: "ask" | "say"
|
||||
ask?: ClineAsk
|
||||
say?: ClineSay
|
||||
export interface ThemeMessage {
|
||||
type: "theme"
|
||||
text: string
|
||||
}
|
||||
|
||||
export interface InvokeMessage {
|
||||
type: "invoke"
|
||||
invoke: "sendMessage" | "primaryButtonClick" | "secondaryButtonClick"
|
||||
text?: string
|
||||
reasoning?: string
|
||||
images?: string[]
|
||||
partial?: boolean
|
||||
lastCheckpointHash?: string
|
||||
isCheckpointCheckedOut?: boolean
|
||||
conversationHistoryIndex?: number
|
||||
conversationHistoryDeletedRange?: [number, number] // for when conversation history is truncated for API requests
|
||||
}
|
||||
|
||||
export type ClineAsk =
|
||||
| "followup"
|
||||
| "plan_mode_response"
|
||||
| "command"
|
||||
| "command_output"
|
||||
| "completion_result"
|
||||
| "tool"
|
||||
| "api_req_failed"
|
||||
| "resume_task"
|
||||
| "resume_completed_task"
|
||||
| "mistake_limit_reached"
|
||||
| "auto_approval_max_req_reached"
|
||||
| "browser_action_launch"
|
||||
| "use_mcp_server"
|
||||
|
||||
export type ClineSay =
|
||||
| "task"
|
||||
| "error"
|
||||
| "api_req_started"
|
||||
| "api_req_finished"
|
||||
| "text"
|
||||
| "reasoning"
|
||||
| "completion_result"
|
||||
| "user_feedback"
|
||||
| "user_feedback_diff"
|
||||
| "api_req_retried"
|
||||
| "command"
|
||||
| "command_output"
|
||||
| "tool"
|
||||
| "shell_integration_warning"
|
||||
| "browser_action_launch"
|
||||
| "browser_action"
|
||||
| "browser_action_result"
|
||||
| "mcp_server_request_started"
|
||||
| "mcp_server_response"
|
||||
| "use_mcp_server"
|
||||
| "diff_error"
|
||||
| "deleted_api_reqs"
|
||||
| "clineignore_error"
|
||||
| "checkpoint_created"
|
||||
|
||||
export interface ClineSayTool {
|
||||
tool:
|
||||
| "editedExistingFile"
|
||||
| "newFileCreated"
|
||||
| "readFile"
|
||||
| "listFilesTopLevel"
|
||||
| "listFilesRecursive"
|
||||
| "listCodeDefinitionNames"
|
||||
| "searchFiles"
|
||||
path?: string
|
||||
diff?: string
|
||||
content?: string
|
||||
regex?: string
|
||||
filePattern?: string
|
||||
export interface SelectedImagesMessage {
|
||||
type: "selectedImages"
|
||||
images: string[]
|
||||
}
|
||||
|
||||
// must keep in sync with system prompt
|
||||
export const browserActions = ["launch", "click", "type", "scroll_down", "scroll_up", "close"] as const
|
||||
export type BrowserAction = (typeof browserActions)[number]
|
||||
export interface CommitSearchResultsMessage {
|
||||
type: "commitSearchResults"
|
||||
commits: GitCommit[]
|
||||
}
|
||||
|
||||
export interface ClineSayBrowserAction {
|
||||
action: BrowserAction
|
||||
export interface BrowserAction {
|
||||
type: "launch" | "click" | "type" | "scroll_down" | "scroll_up" | "close"
|
||||
url?: string
|
||||
coordinate?: string
|
||||
text?: string
|
||||
}
|
||||
|
||||
export type BrowserActionResult = {
|
||||
export interface BrowserActionResult {
|
||||
success: boolean
|
||||
error?: string
|
||||
screenshot?: string
|
||||
logs?: string
|
||||
currentUrl?: string
|
||||
currentMousePosition?: string
|
||||
logs?: string[]
|
||||
}
|
||||
|
||||
export const browserActions = ["launch", "click", "type", "scroll_down", "scroll_up", "close"] as const
|
||||
|
||||
export interface ClineApiReqInfo {
|
||||
id: string
|
||||
model: string
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
cacheWrites: number
|
||||
cacheReads: number
|
||||
cost: number
|
||||
}
|
||||
|
||||
export type ClineApiReqCancelReason = "user" | "error" | "timeout"
|
||||
|
||||
export interface ClineAsk {
|
||||
text: string
|
||||
type: "text" | "tool" | "browser" | "mcp"
|
||||
requires_approval: boolean
|
||||
}
|
||||
|
||||
export interface ClineAskUseMcpServer {
|
||||
serverName: string
|
||||
type: "use_mcp_tool" | "access_mcp_resource"
|
||||
toolName?: string
|
||||
arguments?: string
|
||||
uri?: string
|
||||
text: string
|
||||
type: "mcp"
|
||||
requires_approval: boolean
|
||||
server_name: string
|
||||
tool_name: string
|
||||
}
|
||||
|
||||
export interface ClineApiReqInfo {
|
||||
request?: string
|
||||
tokensIn?: number
|
||||
tokensOut?: number
|
||||
cacheWrites?: number
|
||||
cacheReads?: number
|
||||
cost?: number
|
||||
cancelReason?: ClineApiReqCancelReason
|
||||
streamingFailedMessage?: string
|
||||
export interface ClineSay {
|
||||
text: string
|
||||
type: "text"
|
||||
}
|
||||
|
||||
export type ClineApiReqCancelReason = "streaming_failed" | "user_cancelled"
|
||||
export interface ClineSayTool {
|
||||
text: string
|
||||
type: "tool"
|
||||
tool: string
|
||||
args: string[]
|
||||
}
|
||||
|
||||
export const COMPLETION_RESULT_CHANGES_FLAG = "HAS_CHANGES"
|
||||
export interface ClineSayBrowserAction {
|
||||
text: string
|
||||
type: "browser"
|
||||
action: BrowserAction
|
||||
}
|
||||
|
||||
export type ClineMessage = ClineSay | ClineSayTool | ClineSayBrowserAction | ClineAsk | ClineAskUseMcpServer
|
||||
|
||||
export const COMPLETION_RESULT_CHANGES_FLAG = "COMPLETION_RESULT_CHANGES"
|
||||
|
||||
export interface PartialMessage {
|
||||
type: "partialMessage"
|
||||
text: string
|
||||
}
|
||||
|
||||
export interface RelinquishControl {
|
||||
type: "relinquishControl"
|
||||
}
|
||||
|
||||
export type ExtensionMessage =
|
||||
| WorkspaceUpdateMessage
|
||||
| ActionMessage
|
||||
| StateMessage
|
||||
| ThemeMessage
|
||||
| InvokeMessage
|
||||
| SelectedImagesMessage
|
||||
| CommitSearchResultsMessage
|
||||
| PartialMessage
|
||||
| RelinquishControl
|
||||
|
||||
@@ -1,72 +1,86 @@
|
||||
import { ApiConfiguration } from "./api"
|
||||
import { AutoApprovalSettings } from "./AutoApprovalSettings"
|
||||
import { BrowserSettings } from "./BrowserSettings"
|
||||
import { ChatSettings } from "./ChatSettings"
|
||||
import { ChatContent } from "./ChatContent"
|
||||
|
||||
export type WebviewMessageType =
|
||||
| "webviewDidLaunch"
|
||||
| "newTask"
|
||||
| "apiConfiguration"
|
||||
| "customInstructions"
|
||||
| "autoApprovalSettings"
|
||||
| "browserSettings"
|
||||
| "chatSettings"
|
||||
| "askResponse"
|
||||
| "clearTask"
|
||||
| "didShowAnnouncement"
|
||||
| "selectImages"
|
||||
| "exportCurrentTask"
|
||||
| "showTaskWithId"
|
||||
| "deleteTaskWithId"
|
||||
| "exportTaskWithId"
|
||||
| "resetState"
|
||||
| "openImage"
|
||||
| "openFile"
|
||||
| "openMention"
|
||||
| "checkpointDiff"
|
||||
| "checkpointRestore"
|
||||
| "taskCompletionViewChanges"
|
||||
| "cancelTask"
|
||||
| "getLatestState"
|
||||
| "accountLoginClicked"
|
||||
| "accountLogoutClicked"
|
||||
| "searchCommits"
|
||||
| "openExtensionSettings"
|
||||
|
||||
export interface WebviewMessage {
|
||||
type:
|
||||
| "apiConfiguration"
|
||||
| "customInstructions"
|
||||
| "webviewDidLaunch"
|
||||
| "newTask"
|
||||
| "askResponse"
|
||||
| "clearTask"
|
||||
| "didShowAnnouncement"
|
||||
| "selectImages"
|
||||
| "exportCurrentTask"
|
||||
| "showTaskWithId"
|
||||
| "deleteTaskWithId"
|
||||
| "exportTaskWithId"
|
||||
| "resetState"
|
||||
| "requestOllamaModels"
|
||||
| "requestLmStudioModels"
|
||||
| "openImage"
|
||||
| "openFile"
|
||||
| "openMention"
|
||||
| "cancelTask"
|
||||
| "refreshOpenRouterModels"
|
||||
| "refreshOpenAiModels"
|
||||
| "openMcpSettings"
|
||||
| "restartMcpServer"
|
||||
| "autoApprovalSettings"
|
||||
| "browserSettings"
|
||||
| "chatSettings"
|
||||
| "checkpointDiff"
|
||||
| "checkpointRestore"
|
||||
| "taskCompletionViewChanges"
|
||||
| "openExtensionSettings"
|
||||
| "requestVsCodeLmModels"
|
||||
| "toggleToolAutoApprove"
|
||||
| "toggleMcpServer"
|
||||
| "getLatestState"
|
||||
| "accountLoginClicked"
|
||||
| "accountLogoutClicked"
|
||||
| "subscribeEmail"
|
||||
| "fetchMcpMarketplace"
|
||||
| "downloadMcp"
|
||||
| "silentlyRefreshMcpMarketplace"
|
||||
| "searchCommits"
|
||||
// | "relaunchChromeDebugMode"
|
||||
type: WebviewMessageType
|
||||
text?: string
|
||||
disabled?: boolean
|
||||
askResponse?: ClineAskResponse
|
||||
apiConfiguration?: ApiConfiguration
|
||||
images?: string[]
|
||||
bool?: boolean
|
||||
apiConfiguration?: ApiConfiguration
|
||||
autoApprovalSettings?: any
|
||||
browserSettings?: any
|
||||
chatSettings?: any
|
||||
chatContent?: {
|
||||
message?: string
|
||||
images?: string[]
|
||||
}
|
||||
askResponse?: ClineAskResponse
|
||||
number?: number
|
||||
autoApprovalSettings?: AutoApprovalSettings
|
||||
browserSettings?: BrowserSettings
|
||||
chatSettings?: ChatSettings
|
||||
chatContent?: ChatContent
|
||||
mcpId?: string
|
||||
|
||||
// For toggleToolAutoApprove
|
||||
serverName?: string
|
||||
toolName?: string
|
||||
autoApprove?: boolean
|
||||
}
|
||||
|
||||
export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse"
|
||||
export interface ClineAskResponse {
|
||||
text: string
|
||||
images?: string[]
|
||||
}
|
||||
|
||||
export type ClineCheckpointRestore = "task" | "workspace" | "taskAndWorkspace"
|
||||
export interface ClineCheckpointRestore {
|
||||
checkpointNumber: number
|
||||
restoreMode: "task" | "workspace" | "taskAndWorkspace"
|
||||
}
|
||||
|
||||
export interface ChatSettings {
|
||||
mode: "act" | "plan"
|
||||
autoScroll: boolean
|
||||
showTimestamps: boolean
|
||||
showCheckpoints: boolean
|
||||
showDiffs: boolean
|
||||
showLineNumbers: boolean
|
||||
showGutter: boolean
|
||||
showMinimap: boolean
|
||||
showIndentGuides: boolean
|
||||
showInvisibles: boolean
|
||||
wordWrap: boolean
|
||||
theme: string
|
||||
fontSize: number
|
||||
lineHeight: number
|
||||
fontFamily: string
|
||||
tabSize: number
|
||||
useSoftTabs: boolean
|
||||
useSpaces: boolean
|
||||
trimTrailingWhitespace: boolean
|
||||
insertFinalNewline: boolean
|
||||
trimFinalNewlines: boolean
|
||||
}
|
||||
|
||||
export interface AutoApprovalSettings {
|
||||
enabled: boolean
|
||||
tools: string[]
|
||||
}
|
||||
|
||||
+303
-721
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,58 @@
|
||||
import { ApiProvider, ModelInfo } from "../shared/api"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
export type SecretKey =
|
||||
| "apiKey"
|
||||
| "openRouterApiKey"
|
||||
| "awsAccessKey"
|
||||
| "awsSecretKey"
|
||||
| "awsSessionToken"
|
||||
| "openAiApiKey"
|
||||
| "geminiApiKey"
|
||||
| "openAiNativeApiKey"
|
||||
| "deepSeekApiKey"
|
||||
| "requestyApiKey"
|
||||
| "togetherApiKey"
|
||||
| "qwenApiKey"
|
||||
| "mistralApiKey"
|
||||
| "liteLlmApiKey"
|
||||
| "authToken"
|
||||
| "authNonce"
|
||||
|
||||
export type GlobalStateKey =
|
||||
| "apiProvider"
|
||||
| "apiModelId"
|
||||
| "awsRegion"
|
||||
| "awsUseCrossRegionInference"
|
||||
| "awsProfile"
|
||||
| "awsUseProfile"
|
||||
| "vertexProjectId"
|
||||
| "vertexRegion"
|
||||
| "lastShownAnnouncementId"
|
||||
| "customInstructions"
|
||||
| "taskHistory"
|
||||
| "openAiBaseUrl"
|
||||
| "openAiModelId"
|
||||
| "openAiModelInfo"
|
||||
| "ollamaModelId"
|
||||
| "ollamaBaseUrl"
|
||||
| "lmStudioModelId"
|
||||
| "lmStudioBaseUrl"
|
||||
| "anthropicBaseUrl"
|
||||
| "azureApiVersion"
|
||||
| "openRouterModelId"
|
||||
| "openRouterModelInfo"
|
||||
| "autoApprovalSettings"
|
||||
| "browserSettings"
|
||||
| "chatSettings"
|
||||
| "vsCodeLmModelSelector"
|
||||
| "userInfo"
|
||||
| "previousModeApiProvider"
|
||||
| "previousModeModelId"
|
||||
| "previousModeModelInfo"
|
||||
| "liteLlmBaseUrl"
|
||||
| "liteLlmModelId"
|
||||
| "qwenApiLine"
|
||||
| "requestyModelId"
|
||||
| "togetherModelId"
|
||||
| "mcpMarketplaceCatalog"
|
||||
+12
-1
@@ -7,6 +7,9 @@ describe("Cost Utilities", () => {
|
||||
describe("calculateApiCost", () => {
|
||||
it("should calculate basic input/output costs", () => {
|
||||
const modelInfo: ModelInfo = {
|
||||
id: "test-model",
|
||||
name: "Test Model",
|
||||
provider: "test",
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 3.0, // $3 per million tokens
|
||||
outputPrice: 15.0, // $15 per million tokens
|
||||
@@ -21,6 +24,9 @@ describe("Cost Utilities", () => {
|
||||
|
||||
it("should handle missing prices", () => {
|
||||
const modelInfo: ModelInfo = {
|
||||
id: "test-model",
|
||||
name: "Test Model",
|
||||
provider: "test",
|
||||
supportsPromptCache: true,
|
||||
// No prices specified
|
||||
}
|
||||
@@ -31,10 +37,12 @@ describe("Cost Utilities", () => {
|
||||
|
||||
it("should use real model configuration (Claude 3.5 Sonnet)", () => {
|
||||
const modelInfo: ModelInfo = {
|
||||
id: "claude-3.5-sonnet",
|
||||
name: "Claude 3.5 Sonnet",
|
||||
provider: "anthropic",
|
||||
maxTokens: 8192,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsComputerUse: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
@@ -53,6 +61,9 @@ describe("Cost Utilities", () => {
|
||||
|
||||
it("should handle zero token counts", () => {
|
||||
const modelInfo: ModelInfo = {
|
||||
id: "test-model",
|
||||
name: "Test Model",
|
||||
provider: "test",
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
|
||||
Reference in New Issue
Block a user