mirror of
https://github.com/cline/cline.git
synced 2026-09-04 20:02:30 +08:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| edb100b50f | |||
| 83f592fdae | |||
| e844dd62ce | |||
| 5a31ddcff3 | |||
| 8ac1d1cfc0 | |||
| 49cffbffbb |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Remove WeakRef usage
|
||||
@@ -21,8 +21,3 @@ export interface TaskMetadata {
|
||||
files_in_context: FileMetadataEntry[]
|
||||
model_usage: ModelMetadataEntry[]
|
||||
}
|
||||
|
||||
// Interface for the controller to avoid direct dependency
|
||||
export interface ControllerLike {
|
||||
context: vscode.ExtensionContext
|
||||
}
|
||||
|
||||
@@ -5,11 +5,10 @@ import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
import { FileContextTracker } from "./FileContextTracker"
|
||||
import * as diskModule from "../../storage/disk"
|
||||
import type { TaskMetadata, ControllerLike, FileMetadataEntry } from "./ContextTrackerTypes"
|
||||
import type { TaskMetadata, FileMetadataEntry } from "./ContextTrackerTypes"
|
||||
|
||||
describe("FileContextTracker", () => {
|
||||
let sandbox: sinon.SinonSandbox
|
||||
let mockController: ControllerLike
|
||||
let mockContext: vscode.ExtensionContext
|
||||
let mockWorkspace: sinon.SinonStub
|
||||
let mockFileSystemWatcher: any
|
||||
@@ -48,10 +47,6 @@ describe("FileContextTracker", () => {
|
||||
globalStorageUri: { fsPath: "/mock/storage" },
|
||||
} as unknown as vscode.ExtensionContext
|
||||
|
||||
mockController = {
|
||||
context: mockContext,
|
||||
}
|
||||
|
||||
// Mock disk module functions
|
||||
mockTaskMetadata = { files_in_context: [], model_usage: [] }
|
||||
getTaskMetadataStub = sandbox.stub(diskModule, "getTaskMetadata").resolves(mockTaskMetadata)
|
||||
@@ -59,7 +54,7 @@ describe("FileContextTracker", () => {
|
||||
|
||||
// Create tracker instance
|
||||
taskId = "test-task-id"
|
||||
tracker = new FileContextTracker(mockController, taskId)
|
||||
tracker = new FileContextTracker(mockContext, taskId)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { getTaskMetadata, saveTaskMetadata } from "../../storage/disk"
|
||||
import type { FileMetadataEntry, ControllerLike } from "./ContextTrackerTypes"
|
||||
import type { FileMetadataEntry } from "./ContextTrackerTypes"
|
||||
|
||||
// This class is responsible for tracking file operations that may result in stale context.
|
||||
// If a user modifies a file outside of Cline, the context may become stale and need to be updated.
|
||||
@@ -16,29 +16,19 @@ import type { FileMetadataEntry, ControllerLike } from "./ContextTrackerTypes"
|
||||
// If the full contents of a file are pass to Cline via a tool, mention, or edit, the file is marked as active.
|
||||
// If a file is modified outside of Cline, we detect and track this change to prevent stale context.
|
||||
export class FileContextTracker {
|
||||
private context: vscode.ExtensionContext
|
||||
readonly taskId: string
|
||||
private controllerRef: WeakRef<ControllerLike>
|
||||
|
||||
// File tracking and watching
|
||||
private fileWatchers = new Map<string, vscode.FileSystemWatcher>()
|
||||
private recentlyModifiedFiles = new Set<string>()
|
||||
private recentlyEditedByCline = new Set<string>()
|
||||
|
||||
constructor(controller: ControllerLike, taskId: string) {
|
||||
this.controllerRef = new WeakRef(controller)
|
||||
constructor(context: vscode.ExtensionContext, taskId: string) {
|
||||
this.context = context
|
||||
this.taskId = taskId
|
||||
}
|
||||
|
||||
// While a task is ref'd by a controller, it will always have access to the extension context
|
||||
// This error is thrown if the controller derefs the task after e.g., aborting the task
|
||||
private context(): vscode.ExtensionContext {
|
||||
const context = this.controllerRef.deref()?.context
|
||||
if (!context) {
|
||||
throw new Error("Unable to access extension context")
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
// Gets the current working directory or returns undefined if it cannot be determined
|
||||
private getCwd(): string | undefined {
|
||||
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
|
||||
@@ -89,9 +79,8 @@ export class FileContextTracker {
|
||||
return
|
||||
}
|
||||
|
||||
const context = this.context()
|
||||
// Add file to metadata
|
||||
await this.addFileToFileContextTracker(context, this.taskId, filePath, operation)
|
||||
await this.addFileToFileContextTracker(this.context, this.taskId, filePath, operation)
|
||||
|
||||
// Set up file watcher for this file
|
||||
await this.setupFileWatcher(filePath)
|
||||
|
||||
@@ -4,11 +4,10 @@ import * as sinon from "sinon"
|
||||
import * as vscode from "vscode"
|
||||
import { ModelContextTracker } from "./ModelContextTracker"
|
||||
import * as diskModule from "../../storage/disk"
|
||||
import type { TaskMetadata, ControllerLike } from "./ContextTrackerTypes"
|
||||
import type { TaskMetadata } from "./ContextTrackerTypes"
|
||||
|
||||
describe("ModelContextTracker", () => {
|
||||
let sandbox: sinon.SinonSandbox
|
||||
let mockController: ControllerLike
|
||||
let mockContext: vscode.ExtensionContext
|
||||
let tracker: ModelContextTracker
|
||||
let taskId: string
|
||||
@@ -24,10 +23,6 @@ describe("ModelContextTracker", () => {
|
||||
globalStorageUri: { fsPath: "/mock/storage" },
|
||||
} as unknown as vscode.ExtensionContext
|
||||
|
||||
mockController = {
|
||||
context: mockContext,
|
||||
}
|
||||
|
||||
// Mock disk module functions
|
||||
mockTaskMetadata = { files_in_context: [], model_usage: [] }
|
||||
getTaskMetadataStub = sandbox.stub(diskModule, "getTaskMetadata").resolves(mockTaskMetadata)
|
||||
@@ -35,7 +30,7 @@ describe("ModelContextTracker", () => {
|
||||
|
||||
// Create tracker instance
|
||||
taskId = "test-task-id"
|
||||
tracker = new ModelContextTracker(mockController, taskId)
|
||||
tracker = new ModelContextTracker(mockContext, taskId)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -83,8 +78,7 @@ describe("ModelContextTracker", () => {
|
||||
|
||||
it("should throw an error when controller is dereferenced", async () => {
|
||||
// Create a new tracker with a controller that will be garbage collected
|
||||
const weakMockController = { context: mockContext }
|
||||
const weakTracker = new ModelContextTracker(weakMockController, taskId)
|
||||
const weakTracker = new ModelContextTracker(mockContext, taskId)
|
||||
|
||||
// Force the WeakRef to return null by overriding the deref method
|
||||
const weakRef = { deref: sandbox.stub().returns(null) }
|
||||
|
||||
@@ -1,29 +1,17 @@
|
||||
import * as vscode from "vscode"
|
||||
import { getTaskMetadata, saveTaskMetadata } from "../../storage/disk"
|
||||
import type { ControllerLike } from "./ContextTrackerTypes"
|
||||
|
||||
export class ModelContextTracker {
|
||||
readonly taskId: string
|
||||
private controllerRef: WeakRef<ControllerLike>
|
||||
private context: vscode.ExtensionContext
|
||||
|
||||
constructor(controller: ControllerLike, taskId: string) {
|
||||
this.controllerRef = new WeakRef(controller)
|
||||
constructor(context: vscode.ExtensionContext, taskId: string) {
|
||||
this.context = context
|
||||
this.taskId = taskId
|
||||
}
|
||||
|
||||
// While a task is ref'd by a controller, it will always have access to the extension context
|
||||
// This error is thrown if the controller derefs the task after e.g., aborting the task
|
||||
private context(): vscode.ExtensionContext {
|
||||
const context = this.controllerRef.deref()?.context
|
||||
if (!context) {
|
||||
throw new Error("Unable to access extension context")
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
async recordModelUsage(apiProviderId: string, modelId: string, mode: string) {
|
||||
const context = this.context()
|
||||
const metadata = await getTaskMetadata(context, this.taskId)
|
||||
const metadata = await getTaskMetadata(this.context, this.taskId)
|
||||
|
||||
if (!metadata.model_usage) {
|
||||
metadata.model_usage = []
|
||||
@@ -47,6 +35,6 @@ export class ModelContextTracker {
|
||||
mode: mode,
|
||||
})
|
||||
|
||||
await saveTaskMetadata(context, this.taskId, metadata)
|
||||
await saveTaskMetadata(this.context, this.taskId, metadata)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,10 +17,12 @@ import { selectImages } from "../../integrations/misc/process-images"
|
||||
import { getTheme } from "../../integrations/theme/getTheme"
|
||||
import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
|
||||
import { ClineAccountService } from "../../services/account/ClineAccountService"
|
||||
import { discoverChromeInstances } from "../../services/browser/BrowserDiscovery"
|
||||
import { BrowserSession } from "../../services/browser/BrowserSession"
|
||||
import { McpHub } from "../../services/mcp/McpHub"
|
||||
import { searchWorkspaceFiles } from "../../services/search/file-search"
|
||||
import { telemetryService } from "../../services/telemetry/TelemetryService"
|
||||
import { ApiProvider, ModelInfo } from "../../shared/api"
|
||||
import { findLast } from "../../shared/array"
|
||||
import { ChatContent } from "../../shared/ChatContent"
|
||||
import { ChatSettings } from "../../shared/ChatSettings"
|
||||
import { ExtensionMessage, ExtensionState, Invoke, Platform } from "../../shared/ExtensionMessage"
|
||||
@@ -30,9 +32,10 @@ import { TelemetrySetting } from "../../shared/TelemetrySetting"
|
||||
import { ClineCheckpointRestore, WebviewMessage } from "../../shared/WebviewMessage"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import { searchCommits } from "../../utils/git"
|
||||
import { getWorkspacePath } from "../../utils/path"
|
||||
import { getTotalTasksSize } from "../../utils/storage"
|
||||
import { Task } from "../task"
|
||||
import { openMention } from "../mentions"
|
||||
import { GlobalFileNames } from "../storage/disk"
|
||||
import {
|
||||
getAllExtensionState,
|
||||
getGlobalState,
|
||||
@@ -42,12 +45,7 @@ import {
|
||||
updateApiConfiguration,
|
||||
updateGlobalState,
|
||||
} from "../storage/state"
|
||||
import { WebviewProvider } from "../webview"
|
||||
import { BrowserSession } from "../../services/browser/BrowserSession"
|
||||
import { GlobalFileNames } from "../storage/disk"
|
||||
import { discoverChromeInstances } from "../../services/browser/BrowserDiscovery"
|
||||
import { searchWorkspaceFiles } from "../../services/search/file-search"
|
||||
import { getWorkspacePath } from "../../utils/path"
|
||||
import { Task } from "../task"
|
||||
|
||||
/*
|
||||
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
||||
@@ -56,25 +54,37 @@ https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/c
|
||||
*/
|
||||
|
||||
export class Controller {
|
||||
private postMessage: (message: any) => Thenable<boolean> | undefined
|
||||
|
||||
private disposables: vscode.Disposable[] = []
|
||||
private task?: Task
|
||||
workspaceTracker?: WorkspaceTracker
|
||||
mcpHub?: McpHub
|
||||
accountService?: ClineAccountService
|
||||
workspaceTracker: WorkspaceTracker
|
||||
mcpHub: McpHub
|
||||
accountService: ClineAccountService
|
||||
private latestAnnouncementId = "april-10-2025" // update to some unique identifier when we add a new announcement
|
||||
private webviewProviderRef: WeakRef<WebviewProvider>
|
||||
|
||||
constructor(
|
||||
readonly context: vscode.ExtensionContext,
|
||||
private readonly outputChannel: vscode.OutputChannel,
|
||||
webviewProvider: WebviewProvider,
|
||||
postMessage: (message: any) => Thenable<boolean> | undefined,
|
||||
) {
|
||||
this.outputChannel.appendLine("ClineProvider instantiated")
|
||||
this.webviewProviderRef = new WeakRef(webviewProvider)
|
||||
this.postMessage = postMessage
|
||||
|
||||
this.workspaceTracker = new WorkspaceTracker(this)
|
||||
this.mcpHub = new McpHub(this)
|
||||
this.accountService = new ClineAccountService(this)
|
||||
this.workspaceTracker = new WorkspaceTracker((msg) => this.postMessageToWebview(msg))
|
||||
this.mcpHub = new McpHub(
|
||||
() => this.ensureMcpServersDirectoryExists(),
|
||||
() => this.ensureSettingsDirectoryExists(),
|
||||
(msg) => this.postMessageToWebview(msg),
|
||||
this.context.extension?.packageJSON?.version ?? "1.0.0",
|
||||
)
|
||||
this.accountService = new ClineAccountService(
|
||||
(msg) => this.postMessageToWebview(msg),
|
||||
async () => {
|
||||
const { apiConfiguration } = await this.getStateToPostToWebview()
|
||||
return apiConfiguration?.clineApiKey
|
||||
},
|
||||
)
|
||||
|
||||
// Clean up legacy checkpoints
|
||||
cleanupLegacyCheckpoints(this.context.globalStorageUri.fsPath, this.outputChannel).catch((error) => {
|
||||
@@ -97,11 +107,8 @@ export class Controller {
|
||||
x.dispose()
|
||||
}
|
||||
}
|
||||
this.workspaceTracker?.dispose()
|
||||
this.workspaceTracker = undefined
|
||||
this.mcpHub?.dispose()
|
||||
this.mcpHub = undefined
|
||||
this.accountService = undefined
|
||||
this.workspaceTracker.dispose()
|
||||
this.mcpHub.dispose()
|
||||
this.outputChannel.appendLine("Disposed all disposables")
|
||||
|
||||
console.error("Controller disposed")
|
||||
@@ -124,12 +131,19 @@ export class Controller {
|
||||
await updateGlobalState(this.context, "userInfo", info)
|
||||
}
|
||||
|
||||
async initClineWithTask(task?: string, images?: string[]) {
|
||||
async initTask(task?: string, images?: string[], historyItem?: HistoryItem) {
|
||||
await this.clearTask() // ensures that an existing task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one
|
||||
const { apiConfiguration, customInstructions, autoApprovalSettings, browserSettings, chatSettings } =
|
||||
await getAllExtensionState(this.context)
|
||||
this.task = new Task(
|
||||
this,
|
||||
this.context,
|
||||
this.mcpHub,
|
||||
this.workspaceTracker,
|
||||
(historyItem) => this.updateTaskHistory(historyItem),
|
||||
() => this.postStateToWebview(),
|
||||
(message) => this.postMessageToWebview(message),
|
||||
(taskId) => this.reinitExistingTaskFromId(taskId),
|
||||
() => this.cancelTask(),
|
||||
apiConfiguration,
|
||||
autoApprovalSettings,
|
||||
browserSettings,
|
||||
@@ -137,29 +151,20 @@ export class Controller {
|
||||
customInstructions,
|
||||
task,
|
||||
images,
|
||||
)
|
||||
}
|
||||
|
||||
async initClineWithHistoryItem(historyItem: HistoryItem) {
|
||||
await this.clearTask()
|
||||
const { apiConfiguration, customInstructions, autoApprovalSettings, browserSettings, chatSettings } =
|
||||
await getAllExtensionState(this.context)
|
||||
this.task = new Task(
|
||||
this,
|
||||
apiConfiguration,
|
||||
autoApprovalSettings,
|
||||
browserSettings,
|
||||
chatSettings,
|
||||
customInstructions,
|
||||
undefined,
|
||||
undefined,
|
||||
historyItem,
|
||||
)
|
||||
}
|
||||
|
||||
async reinitExistingTaskFromId(taskId: string) {
|
||||
const history = await this.getTaskWithId(taskId)
|
||||
if (history) {
|
||||
await this.initTask(undefined, undefined, history.historyItem)
|
||||
}
|
||||
}
|
||||
|
||||
// Send any JSON serializable data to the react app
|
||||
async postMessageToWebview(message: ExtensionMessage) {
|
||||
await this.webviewProviderRef.deref()?.view?.webview.postMessage(message)
|
||||
await this.postMessage(message)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -259,7 +264,7 @@ export class Controller {
|
||||
// Could also do this in extension .ts
|
||||
//this.postMessageToWebview({ type: "text", text: `Extension: ${Date.now()}` })
|
||||
// initializing new instance of Cline will make sure that any agentically running promises in old instance don't affect our new task. this essentially creates a fresh slate for the new task
|
||||
await this.initClineWithTask(message.text, message.images)
|
||||
await this.initTask(message.text, message.images)
|
||||
break
|
||||
case "apiConfiguration":
|
||||
if (message.apiConfiguration) {
|
||||
@@ -1078,7 +1083,7 @@ export class Controller {
|
||||
// 'abandoned' will prevent this cline instance from affecting future cline instance gui. this may happen if its hanging on a streaming request
|
||||
this.task.abandoned = true
|
||||
}
|
||||
await this.initClineWithHistoryItem(historyItem) // clears task again, so we need to abortTask manually above
|
||||
await this.initTask(undefined, undefined, historyItem) // clears task again, so we need to abortTask manually above
|
||||
// await this.postStateToWebview() // new Cline instance will post state when it's ready. having this here sent an empty messages array to webview leading to virtuoso having to reload the entire list
|
||||
}
|
||||
}
|
||||
@@ -1396,7 +1401,7 @@ export class Controller {
|
||||
Here is the project's README to help you get started:\n\n${mcpDetails.readmeContent}\n${mcpDetails.llmsInstallationContent}`
|
||||
|
||||
// Initialize task and show chat view
|
||||
await this.initClineWithTask(task)
|
||||
await this.initTask(task)
|
||||
await this.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "chatButtonClicked",
|
||||
@@ -1684,9 +1689,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
|
||||
|
||||
const fileMention = this.getFileMentionFromPath(filePath)
|
||||
const problemsString = this.convertDiagnosticsToProblemsString(diagnostics)
|
||||
await this.initClineWithTask(
|
||||
`Fix the following code in ${fileMention}\n\`\`\`\n${code}\n\`\`\`\n\nProblems:\n${problemsString}`,
|
||||
)
|
||||
await this.initTask(`Fix the following code in ${fileMention}\n\`\`\`\n${code}\n\`\`\`\n\nProblems:\n${problemsString}`)
|
||||
|
||||
console.log("fixWithCline", code, filePath, languageId, diagnostics, problemsString)
|
||||
}
|
||||
@@ -1762,7 +1765,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
|
||||
if (id !== this.task?.taskId) {
|
||||
// non-current task
|
||||
const { historyItem } = await this.getTaskWithId(id)
|
||||
await this.initClineWithHistoryItem(historyItem) // clears existing task
|
||||
await this.initTask(undefined, undefined, historyItem) // clears existing task
|
||||
}
|
||||
await this.postMessageToWebview({
|
||||
type: "action",
|
||||
|
||||
+74
-70
@@ -49,6 +49,7 @@ import {
|
||||
ClineSayBrowserAction,
|
||||
ClineSayTool,
|
||||
COMPLETION_RESULT_CHANGES_FLAG,
|
||||
ExtensionMessage,
|
||||
} from "../../shared/ExtensionMessage"
|
||||
import { getApiMetrics } from "../../shared/getApiMetrics"
|
||||
import { HistoryItem } from "../../shared/HistoryItem"
|
||||
@@ -82,6 +83,8 @@ import {
|
||||
GlobalFileNames,
|
||||
getTaskMetadata,
|
||||
} from "../storage/disk"
|
||||
import { McpHub } from "../../services/mcp/McpHub"
|
||||
import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
|
||||
|
||||
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution
|
||||
|
||||
@@ -89,6 +92,16 @@ type ToolResponse = string | Array<Anthropic.TextBlockParam | Anthropic.ImageBlo
|
||||
type UserContent = Array<Anthropic.ContentBlockParam>
|
||||
|
||||
export class Task {
|
||||
// dependencies
|
||||
private context: vscode.ExtensionContext
|
||||
private mcpHub: McpHub
|
||||
private workspaceTracker: WorkspaceTracker
|
||||
private updateTaskHistory: (historyItem: HistoryItem) => Promise<HistoryItem[]>
|
||||
private postStateToWebview: () => Promise<void>
|
||||
private postMessageToWebview: (message: ExtensionMessage) => Promise<void>
|
||||
private reinitExistingTaskFromId: (taskId: string) => Promise<void>
|
||||
private cancelTask: () => Promise<void>
|
||||
|
||||
readonly taskId: string
|
||||
readonly apiProvider?: string
|
||||
api: ApiHandler
|
||||
@@ -110,7 +123,6 @@ export class Task {
|
||||
private lastMessageTs?: number
|
||||
private consecutiveAutoApprovedRequestsCount: number = 0
|
||||
private consecutiveMistakeCount: number = 0
|
||||
private controllerRef: WeakRef<Controller>
|
||||
private abort: boolean = false
|
||||
didFinishAbortingStream = false
|
||||
abandoned = false
|
||||
@@ -141,7 +153,14 @@ export class Task {
|
||||
private didAutomaticallyRetryFailedApiRequest = false
|
||||
|
||||
constructor(
|
||||
controller: Controller,
|
||||
context: vscode.ExtensionContext,
|
||||
mcpHub: McpHub,
|
||||
workspaceTracker: WorkspaceTracker,
|
||||
updateTaskHistory: (historyItem: HistoryItem) => Promise<HistoryItem[]>,
|
||||
postStateToWebview: () => Promise<void>,
|
||||
postMessageToWebview: (message: ExtensionMessage) => Promise<void>,
|
||||
reinitExistingTaskFromId: (taskId: string) => Promise<void>,
|
||||
cancelTask: () => Promise<void>,
|
||||
apiConfiguration: ApiConfiguration,
|
||||
autoApprovalSettings: AutoApprovalSettings,
|
||||
browserSettings: BrowserSettings,
|
||||
@@ -151,15 +170,22 @@ export class Task {
|
||||
images?: string[],
|
||||
historyItem?: HistoryItem,
|
||||
) {
|
||||
this.context = context
|
||||
this.mcpHub = mcpHub
|
||||
this.workspaceTracker = workspaceTracker
|
||||
this.updateTaskHistory = updateTaskHistory
|
||||
this.postStateToWebview = postStateToWebview
|
||||
this.postMessageToWebview = postMessageToWebview
|
||||
this.reinitExistingTaskFromId = reinitExistingTaskFromId
|
||||
this.cancelTask = cancelTask
|
||||
this.clineIgnoreController = new ClineIgnoreController(cwd)
|
||||
this.clineIgnoreController.initialize().catch((error) => {
|
||||
console.error("Failed to initialize ClineIgnoreController:", error)
|
||||
})
|
||||
this.controllerRef = new WeakRef(controller)
|
||||
this.apiProvider = apiConfiguration.apiProvider
|
||||
this.terminalManager = new TerminalManager()
|
||||
this.urlContentFetcher = new UrlContentFetcher(controller.context)
|
||||
this.browserSession = new BrowserSession(controller.context, browserSettings)
|
||||
this.urlContentFetcher = new UrlContentFetcher(context)
|
||||
this.browserSession = new BrowserSession(context, browserSettings)
|
||||
this.contextManager = new ContextManager()
|
||||
this.diffViewProvider = new DiffViewProvider(cwd)
|
||||
this.customInstructions = customInstructions
|
||||
@@ -178,8 +204,8 @@ export class Task {
|
||||
}
|
||||
|
||||
// Initialize file context tracker
|
||||
this.fileContextTracker = new FileContextTracker(controller, this.taskId)
|
||||
this.modelContextTracker = new ModelContextTracker(controller, this.taskId)
|
||||
this.fileContextTracker = new FileContextTracker(context, this.taskId)
|
||||
this.modelContextTracker = new ModelContextTracker(context, this.taskId)
|
||||
// Now that taskId is initialized, we can build the API handler
|
||||
this.api = buildApiHandler({
|
||||
...apiConfiguration,
|
||||
@@ -208,7 +234,7 @@ export class Task {
|
||||
// While a task is ref'd by a controller, it will always have access to the extension context
|
||||
// This error is thrown if the controller derefs the task after e.g., aborting the task
|
||||
private getContext(): vscode.ExtensionContext {
|
||||
const context = this.controllerRef.deref()?.context
|
||||
const context = this.context
|
||||
if (!context) {
|
||||
throw new Error("Unable to access extension context")
|
||||
}
|
||||
@@ -260,7 +286,7 @@ export class Task {
|
||||
} catch (error) {
|
||||
console.error("Failed to get task directory size:", taskDir, error)
|
||||
}
|
||||
await this.controllerRef.deref()?.updateTaskHistory({
|
||||
await this.updateTaskHistory({
|
||||
id: this.taskId,
|
||||
ts: lastRelevantMessage.ts,
|
||||
task: taskMessage.text ?? "",
|
||||
@@ -295,15 +321,12 @@ export class Task {
|
||||
case "workspace":
|
||||
if (!this.checkpointTracker && !this.checkpointTrackerErrorMessage) {
|
||||
try {
|
||||
this.checkpointTracker = await CheckpointTracker.create(
|
||||
this.taskId,
|
||||
this.controllerRef.deref()?.context.globalStorageUri.fsPath,
|
||||
)
|
||||
this.checkpointTracker = await CheckpointTracker.create(this.taskId, this.context.globalStorageUri.fsPath)
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error"
|
||||
console.error("Failed to initialize checkpoint tracker:", errorMessage)
|
||||
this.checkpointTrackerErrorMessage = errorMessage
|
||||
await this.controllerRef.deref()?.postStateToWebview()
|
||||
await this.postStateToWebview()
|
||||
vscode.window.showErrorMessage(errorMessage)
|
||||
didWorkspaceRestoreFail = true
|
||||
}
|
||||
@@ -385,17 +408,17 @@ export class Task {
|
||||
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
|
||||
await this.controllerRef.deref()?.postMessageToWebview({ type: "relinquishControl" })
|
||||
await this.postMessageToWebview({ type: "relinquishControl" })
|
||||
|
||||
this.controllerRef.deref()?.cancelTask() // the task is already cancelled by the provider beforehand, but we need to re-init to get the updated messages
|
||||
this.cancelTask() // the task is already cancelled by the provider beforehand, but we need to re-init to get the updated messages
|
||||
} else {
|
||||
await this.controllerRef.deref()?.postMessageToWebview({ type: "relinquishControl" })
|
||||
await this.postMessageToWebview({ type: "relinquishControl" })
|
||||
}
|
||||
}
|
||||
|
||||
async presentMultifileDiff(messageTs: number, seeNewChangesSinceLastTaskCompletion: boolean) {
|
||||
const relinquishButton = () => {
|
||||
this.controllerRef.deref()?.postMessageToWebview({ type: "relinquishControl" })
|
||||
this.postMessageToWebview({ type: "relinquishControl" })
|
||||
}
|
||||
|
||||
console.log("presentMultifileDiff", messageTs)
|
||||
@@ -416,15 +439,12 @@ export class Task {
|
||||
// TODO: handle if this is called from outside original workspace, in which case we need to show user error message we cant show diff outside of workspace?
|
||||
if (!this.checkpointTracker && !this.checkpointTrackerErrorMessage) {
|
||||
try {
|
||||
this.checkpointTracker = await CheckpointTracker.create(
|
||||
this.taskId,
|
||||
this.controllerRef.deref()?.context.globalStorageUri.fsPath,
|
||||
)
|
||||
this.checkpointTracker = await CheckpointTracker.create(this.taskId, this.context.globalStorageUri.fsPath)
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error"
|
||||
console.error("Failed to initialize checkpoint tracker:", errorMessage)
|
||||
this.checkpointTrackerErrorMessage = errorMessage
|
||||
await this.controllerRef.deref()?.postStateToWebview()
|
||||
await this.postStateToWebview()
|
||||
vscode.window.showErrorMessage(errorMessage)
|
||||
relinquishButton()
|
||||
return
|
||||
@@ -531,10 +551,7 @@ export class Task {
|
||||
|
||||
if (!this.checkpointTracker && !this.checkpointTrackerErrorMessage) {
|
||||
try {
|
||||
this.checkpointTracker = await CheckpointTracker.create(
|
||||
this.taskId,
|
||||
this.controllerRef.deref()?.context.globalStorageUri.fsPath,
|
||||
)
|
||||
this.checkpointTracker = await CheckpointTracker.create(this.taskId, this.context.globalStorageUri.fsPath)
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error"
|
||||
console.error("Failed to initialize checkpoint tracker:", errorMessage)
|
||||
@@ -605,8 +622,8 @@ export class Task {
|
||||
lastMessage.partial = partial
|
||||
// todo be more efficient about saving and posting only new data or one whole message at a time so ignore partial for saves, and only post parts of partial message instead of whole array in new listener
|
||||
// await this.saveClineMessagesAndUpdateHistory()
|
||||
// await this.controllerRef.deref()?.postStateToWebview()
|
||||
await this.controllerRef.deref()?.postMessageToWebview({
|
||||
// await this.postStateToWebview()
|
||||
await this.postMessageToWebview({
|
||||
type: "partialMessage",
|
||||
partialMessage: lastMessage,
|
||||
})
|
||||
@@ -625,7 +642,7 @@ export class Task {
|
||||
text,
|
||||
partial,
|
||||
})
|
||||
await this.controllerRef.deref()?.postStateToWebview()
|
||||
await this.postStateToWebview()
|
||||
throw new Error("Current ask promise was ignored 2")
|
||||
}
|
||||
} else {
|
||||
@@ -648,8 +665,8 @@ export class Task {
|
||||
lastMessage.text = text
|
||||
lastMessage.partial = false
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
// await this.controllerRef.deref()?.postStateToWebview()
|
||||
await this.controllerRef.deref()?.postMessageToWebview({
|
||||
// await this.postStateToWebview()
|
||||
await this.postMessageToWebview({
|
||||
type: "partialMessage",
|
||||
partialMessage: lastMessage,
|
||||
})
|
||||
@@ -666,7 +683,7 @@ export class Task {
|
||||
ask: type,
|
||||
text,
|
||||
})
|
||||
await this.controllerRef.deref()?.postStateToWebview()
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -683,7 +700,7 @@ export class Task {
|
||||
ask: type,
|
||||
text,
|
||||
})
|
||||
await this.controllerRef.deref()?.postStateToWebview()
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
|
||||
await pWaitFor(() => this.askResponse !== undefined || this.lastMessageTs !== askTs, { interval: 100 })
|
||||
@@ -722,7 +739,7 @@ export class Task {
|
||||
lastMessage.text = text
|
||||
lastMessage.images = images
|
||||
lastMessage.partial = partial
|
||||
await this.controllerRef.deref()?.postMessageToWebview({
|
||||
await this.postMessageToWebview({
|
||||
type: "partialMessage",
|
||||
partialMessage: lastMessage,
|
||||
})
|
||||
@@ -738,7 +755,7 @@ export class Task {
|
||||
images,
|
||||
partial,
|
||||
})
|
||||
await this.controllerRef.deref()?.postStateToWebview()
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
} else {
|
||||
// partial=false means its a complete version of a previously partial message
|
||||
@@ -752,8 +769,8 @@ export class Task {
|
||||
|
||||
// instead of streaming partialMessage events, we do a save and post like normal to persist to disk
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
// await this.controllerRef.deref()?.postStateToWebview()
|
||||
await this.controllerRef.deref()?.postMessageToWebview({
|
||||
// await this.postStateToWebview()
|
||||
await this.postMessageToWebview({
|
||||
type: "partialMessage",
|
||||
partialMessage: lastMessage,
|
||||
}) // more performant than an entire postStateToWebview
|
||||
@@ -768,7 +785,7 @@ export class Task {
|
||||
text,
|
||||
images,
|
||||
})
|
||||
await this.controllerRef.deref()?.postStateToWebview()
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -782,7 +799,7 @@ export class Task {
|
||||
text,
|
||||
images,
|
||||
})
|
||||
await this.controllerRef.deref()?.postStateToWebview()
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -801,7 +818,7 @@ export class Task {
|
||||
if (lastMessage?.partial && lastMessage.type === type && (lastMessage.ask === askOrSay || lastMessage.say === askOrSay)) {
|
||||
this.clineMessages.pop()
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
await this.controllerRef.deref()?.postStateToWebview()
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -813,7 +830,7 @@ export class Task {
|
||||
this.clineMessages = []
|
||||
this.apiConversationHistory = []
|
||||
|
||||
await this.controllerRef.deref()?.postStateToWebview()
|
||||
await this.postStateToWebview()
|
||||
|
||||
await this.say("text", task, images)
|
||||
|
||||
@@ -1209,21 +1226,16 @@ export class Task {
|
||||
|
||||
async *attemptApiRequest(previousApiReqIndex: number): ApiStream {
|
||||
// Wait for MCP servers to be connected before generating system prompt
|
||||
await pWaitFor(() => this.controllerRef.deref()?.mcpHub?.isConnecting !== true, { timeout: 10_000 }).catch(() => {
|
||||
await pWaitFor(() => this.mcpHub.isConnecting !== true, { timeout: 10_000 }).catch(() => {
|
||||
console.error("MCP servers failed to connect in time")
|
||||
})
|
||||
|
||||
const mcpHub = this.controllerRef.deref()?.mcpHub
|
||||
if (!mcpHub) {
|
||||
throw new Error("MCP hub not available")
|
||||
}
|
||||
|
||||
const disableBrowserTool = vscode.workspace.getConfiguration("cline").get<boolean>("disableBrowserTool") ?? false
|
||||
const modelSupportsComputerUse = this.api.getModel().info.supportsComputerUse ?? false
|
||||
|
||||
const supportsComputerUse = modelSupportsComputerUse && !disableBrowserTool // only enable computer use if the model supports it and the user hasn't disabled it
|
||||
|
||||
let systemPrompt = await SYSTEM_PROMPT(cwd, supportsComputerUse, mcpHub, this.browserSettings)
|
||||
let systemPrompt = await SYSTEM_PROMPT(cwd, supportsComputerUse, this.mcpHub, this.browserSettings)
|
||||
|
||||
let settingsCustomInstructions = this.customInstructions?.trim()
|
||||
const preferredLanguage = getLanguageKey(
|
||||
@@ -1900,7 +1912,7 @@ export class Task {
|
||||
}
|
||||
|
||||
if (!fileExists) {
|
||||
this.controllerRef.deref()?.workspaceTracker?.populateFilePaths()
|
||||
this.workspaceTracker.populateFilePaths()
|
||||
}
|
||||
|
||||
await this.diffViewProvider.reset()
|
||||
@@ -2296,10 +2308,9 @@ export class Task {
|
||||
await this.say("browser_action_result", "") // starts loading spinner
|
||||
|
||||
// Re-make browserSession to make sure latest settings apply
|
||||
const localContext = this.controllerRef.deref()?.context
|
||||
if (localContext) {
|
||||
if (this.context) {
|
||||
await this.browserSession.dispose()
|
||||
this.browserSession = new BrowserSession(localContext, this.browserSettings)
|
||||
this.browserSession = new BrowserSession(this.context, this.browserSettings)
|
||||
} else {
|
||||
console.warn("no controller context available for browserSession")
|
||||
}
|
||||
@@ -2498,7 +2509,7 @@ export class Task {
|
||||
}
|
||||
|
||||
// Re-populate file paths in case the command modified the workspace (vscode listeners do not trigger unless the user manually creates/deletes files)
|
||||
this.controllerRef.deref()?.workspaceTracker?.populateFilePaths()
|
||||
this.workspaceTracker.populateFilePaths()
|
||||
|
||||
pushToolResult(result)
|
||||
|
||||
@@ -2580,9 +2591,8 @@ export class Task {
|
||||
arguments: mcp_arguments,
|
||||
} satisfies ClineAskUseMcpServer)
|
||||
|
||||
const isToolAutoApproved = this.controllerRef
|
||||
.deref()
|
||||
?.mcpHub?.connections?.find((conn) => conn.server.name === server_name)
|
||||
const isToolAutoApproved = this.mcpHub.connections
|
||||
?.find((conn) => conn.server.name === server_name)
|
||||
?.server.tools?.find((tool) => tool.name === tool_name)?.autoApprove
|
||||
|
||||
if (this.shouldAutoApproveTool(block.name) && isToolAutoApproved) {
|
||||
@@ -2603,9 +2613,7 @@ export class Task {
|
||||
|
||||
// now execute the tool
|
||||
await this.say("mcp_server_request_started") // same as browser_action_result
|
||||
const toolResult = await this.controllerRef
|
||||
.deref()
|
||||
?.mcpHub?.callTool(server_name, tool_name, parsedArguments)
|
||||
const toolResult = await this.mcpHub.callTool(server_name, tool_name, parsedArguments)
|
||||
|
||||
// TODO: add progress indicator and ability to parse images and non-text responses
|
||||
const toolResultPretty =
|
||||
@@ -2694,7 +2702,7 @@ export class Task {
|
||||
|
||||
// now execute the tool
|
||||
await this.say("mcp_server_request_started")
|
||||
const resourceResult = await this.controllerRef.deref()?.mcpHub?.readResource(server_name, uri)
|
||||
const resourceResult = await this.mcpHub.readResource(server_name, uri)
|
||||
const resourceResultPretty =
|
||||
resourceResult?.contents
|
||||
.map((item) => {
|
||||
@@ -3189,7 +3197,7 @@ export class Task {
|
||||
if (!this.checkpointTracker && !this.checkpointTrackerErrorMessage) {
|
||||
try {
|
||||
this.checkpointTracker = await pTimeout(
|
||||
CheckpointTracker.create(this.taskId, this.controllerRef.deref()?.context.globalStorageUri.fsPath),
|
||||
CheckpointTracker.create(this.taskId, this.context.globalStorageUri.fsPath),
|
||||
{
|
||||
milliseconds: 15_000,
|
||||
message:
|
||||
@@ -3231,7 +3239,7 @@ export class Task {
|
||||
request: userContent.map((block) => formatContentBlockToMarkdown(block)).join("\n\n"),
|
||||
} satisfies ClineApiReqInfo)
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
await this.controllerRef.deref()?.postStateToWebview()
|
||||
await this.postStateToWebview()
|
||||
|
||||
try {
|
||||
let cacheWriteTokens = 0
|
||||
@@ -3391,11 +3399,7 @@ export class Task {
|
||||
const errorMessage = this.formatErrorWithStatusCode(error)
|
||||
|
||||
await abortStream("streaming_failed", errorMessage)
|
||||
const history = await this.controllerRef.deref()?.getTaskWithId(this.taskId)
|
||||
if (history) {
|
||||
await this.controllerRef.deref()?.initClineWithHistoryItem(history.historyItem)
|
||||
// await this.controllerRef.deref()?.postStateToWebview()
|
||||
}
|
||||
await this.reinitExistingTaskFromId(this.taskId)
|
||||
}
|
||||
} finally {
|
||||
this.isStreaming = false
|
||||
@@ -3414,7 +3418,7 @@ export class Task {
|
||||
}
|
||||
updateApiReqMsg()
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
await this.controllerRef.deref()?.postStateToWebview()
|
||||
await this.postStateToWebview()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3438,7 +3442,7 @@ export class Task {
|
||||
|
||||
updateApiReqMsg()
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
await this.controllerRef.deref()?.postStateToWebview()
|
||||
await this.postStateToWebview()
|
||||
|
||||
// now add to apiconversationhistory
|
||||
// need to save assistant responses to file before proceeding to tool use since user can exit at any moment and we wouldn't be able to save the assistant's response
|
||||
|
||||
@@ -23,7 +23,7 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
|
||||
private readonly outputChannel: vscode.OutputChannel,
|
||||
) {
|
||||
WebviewProvider.activeInstances.add(this)
|
||||
this.controller = new Controller(context, outputChannel, this)
|
||||
this.controller = new Controller(context, outputChannel, (message) => this.view?.webview.postMessage(message))
|
||||
}
|
||||
|
||||
async dispose() {
|
||||
|
||||
@@ -2,17 +2,17 @@ import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
import { listFiles } from "../../services/glob/list-files"
|
||||
import { Controller } from "../../core/controller"
|
||||
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 controllerRef: WeakRef<Controller>
|
||||
private disposables: vscode.Disposable[] = []
|
||||
private filePaths: Set<string> = new Set()
|
||||
|
||||
constructor(controller: Controller) {
|
||||
this.controllerRef = new WeakRef(controller)
|
||||
constructor(private readonly postMessageToWebview: (message: ExtensionMessage) => Promise<void>) {
|
||||
this.postMessageToWebview = postMessageToWebview
|
||||
this.registerListeners()
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ class WorkspaceTracker {
|
||||
if (!cwd) {
|
||||
return
|
||||
}
|
||||
this.controllerRef.deref()?.postMessageToWebview({
|
||||
this.postMessageToWebview({
|
||||
type: "workspaceUpdated",
|
||||
filePaths: Array.from(this.filePaths).map((file) => {
|
||||
const relativePath = path.relative(cwd, file).toPosix()
|
||||
|
||||
@@ -1,26 +1,18 @@
|
||||
import axios, { AxiosRequestConfig, AxiosResponse } from "axios"
|
||||
import { Controller } from "../../core/controller"
|
||||
import type { BalanceResponse, PaymentTransaction, UsageTransaction } from "../../shared/ClineAccount"
|
||||
import { ExtensionMessage } from "../../shared/ExtensionMessage"
|
||||
|
||||
export class ClineAccountService {
|
||||
private readonly baseUrl = "https://api.cline.bot/v1"
|
||||
private controllerRef: WeakRef<Controller>
|
||||
private postMessageToWebview: (message: ExtensionMessage) => Promise<void>
|
||||
private getClineApiKey: () => Promise<string | undefined>
|
||||
|
||||
constructor(controller: Controller) {
|
||||
this.controllerRef = new WeakRef(controller)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's Cline Account key from the apiConfiguration
|
||||
*/
|
||||
private async getClineApiKey(): Promise<string | undefined> {
|
||||
const provider = this.controllerRef.deref()
|
||||
if (!provider) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const { apiConfiguration } = await provider.getStateToPostToWebview()
|
||||
return apiConfiguration?.clineApiKey
|
||||
constructor(
|
||||
postMessageToWebview: (message: ExtensionMessage) => Promise<void>,
|
||||
getClineApiKey: () => Promise<string | undefined>,
|
||||
) {
|
||||
this.postMessageToWebview = postMessageToWebview
|
||||
this.getClineApiKey = getClineApiKey
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,7 +56,7 @@ export class ClineAccountService {
|
||||
const data = await this.authenticatedRequest<BalanceResponse>("/user/credits/balance")
|
||||
|
||||
// Post to webview
|
||||
await this.controllerRef.deref()?.postMessageToWebview({
|
||||
await this.postMessageToWebview({
|
||||
type: "userCreditsBalance",
|
||||
userCreditsBalance: data,
|
||||
})
|
||||
@@ -84,7 +76,7 @@ export class ClineAccountService {
|
||||
const data = await this.authenticatedRequest<UsageTransaction[]>("/user/credits/usage")
|
||||
|
||||
// Post to webview
|
||||
await this.controllerRef.deref()?.postMessageToWebview({
|
||||
await this.postMessageToWebview({
|
||||
type: "userCreditsUsage",
|
||||
userCreditsUsage: data,
|
||||
})
|
||||
@@ -104,7 +96,7 @@ export class ClineAccountService {
|
||||
const data = await this.authenticatedRequest<PaymentTransaction[]>("/user/credits/payments")
|
||||
|
||||
// Post to webview
|
||||
await this.controllerRef.deref()?.postMessageToWebview({
|
||||
await this.postMessageToWebview({
|
||||
type: "userCreditsPayments",
|
||||
userCreditsPayments: data,
|
||||
})
|
||||
|
||||
+19
-24
@@ -14,7 +14,6 @@ import * as fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { z } from "zod"
|
||||
import { Controller } from "../../core/controller"
|
||||
import {
|
||||
DEFAULT_MCP_TIMEOUT_SECONDS,
|
||||
McpMode,
|
||||
@@ -31,6 +30,7 @@ import { arePathsEqual } from "../../utils/path"
|
||||
import { secondsToMs } from "../../utils/time"
|
||||
import { GlobalFileNames } from "../../core/storage/disk"
|
||||
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
|
||||
import { ExtensionMessage } from "../../shared/ExtensionMessage"
|
||||
|
||||
// Default timeout for internal MCP data requests in milliseconds; is not the same as the user facing timeout stored as DEFAULT_MCP_TIMEOUT_SECONDS
|
||||
const DEFAULT_REQUEST_TIMEOUT_MS = 5000
|
||||
@@ -76,15 +76,27 @@ const McpSettingsSchema = z.object({
|
||||
})
|
||||
|
||||
export class McpHub {
|
||||
private controllerRef: WeakRef<Controller>
|
||||
getMcpServersPath: () => Promise<string>
|
||||
private getSettingsDirectoryPath: () => Promise<string>
|
||||
private postMessageToWebview: (message: ExtensionMessage) => Promise<void>
|
||||
private clientVersion: string
|
||||
|
||||
private disposables: vscode.Disposable[] = []
|
||||
private settingsWatcher?: vscode.FileSystemWatcher
|
||||
private fileWatchers: Map<string, FSWatcher> = new Map()
|
||||
connections: McpConnection[] = []
|
||||
isConnecting: boolean = false
|
||||
|
||||
constructor(controller: Controller) {
|
||||
this.controllerRef = new WeakRef(controller)
|
||||
constructor(
|
||||
getMcpServersPath: () => Promise<string>,
|
||||
getSettingsDirectoryPath: () => Promise<string>,
|
||||
postMessageToWebview: (message: ExtensionMessage) => Promise<void>,
|
||||
clientVersion: string,
|
||||
) {
|
||||
this.getMcpServersPath = getMcpServersPath
|
||||
this.getSettingsDirectoryPath = getSettingsDirectoryPath
|
||||
this.postMessageToWebview = postMessageToWebview
|
||||
this.clientVersion = clientVersion
|
||||
this.watchMcpSettingsFile()
|
||||
this.initializeMcpServers()
|
||||
}
|
||||
@@ -98,21 +110,8 @@ export class McpHub {
|
||||
return vscode.workspace.getConfiguration("cline.mcp").get<McpMode>("mode", "full")
|
||||
}
|
||||
|
||||
async getMcpServersPath(): Promise<string> {
|
||||
const provider = this.controllerRef.deref()
|
||||
if (!provider) {
|
||||
throw new Error("Provider not available")
|
||||
}
|
||||
const mcpServersPath = await provider.ensureMcpServersDirectoryExists()
|
||||
return mcpServersPath
|
||||
}
|
||||
|
||||
async getMcpSettingsFilePath(): Promise<string> {
|
||||
const provider = this.controllerRef.deref()
|
||||
if (!provider) {
|
||||
throw new Error("Provider not available")
|
||||
}
|
||||
const mcpSettingsFilePath = path.join(await provider.ensureSettingsDirectoryExists(), GlobalFileNames.mcpSettings)
|
||||
const mcpSettingsFilePath = path.join(await this.getSettingsDirectoryPath(), GlobalFileNames.mcpSettings)
|
||||
const fileExists = await fileExistsAtPath(mcpSettingsFilePath)
|
||||
if (!fileExists) {
|
||||
await fs.writeFile(
|
||||
@@ -197,7 +196,7 @@ export class McpHub {
|
||||
const client = new Client(
|
||||
{
|
||||
name: "Cline",
|
||||
version: this.controllerRef.deref()?.context.extension?.packageJSON?.version ?? "1.0.0",
|
||||
version: this.clientVersion,
|
||||
},
|
||||
{
|
||||
capabilities: {},
|
||||
@@ -455,10 +454,6 @@ export class McpHub {
|
||||
|
||||
async restartConnection(serverName: string): Promise<void> {
|
||||
this.isConnecting = true
|
||||
const provider = this.controllerRef.deref()
|
||||
if (!provider) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get existing connection and update its status
|
||||
const connection = this.connections.find((conn) => conn.server.name === serverName)
|
||||
@@ -490,7 +485,7 @@ export class McpHub {
|
||||
const content = await fs.readFile(settingsPath, "utf-8")
|
||||
const config = JSON.parse(content)
|
||||
const serverOrder = Object.keys(config.mcpServers || {})
|
||||
await this.controllerRef.deref()?.postMessageToWebview({
|
||||
await this.postMessageToWebview({
|
||||
type: "mcpServers",
|
||||
mcpServers: [...this.connections]
|
||||
.sort((a, b) => {
|
||||
|
||||
@@ -116,7 +116,7 @@ export function createTestServer(webviewProvider?: WebviewProvider): http.Server
|
||||
}
|
||||
|
||||
// Initiate the new task
|
||||
const taskId = await visibleWebview.controller.initClineWithTask(task)
|
||||
const taskId = await visibleWebview.controller.initTask(task)
|
||||
|
||||
// Return success response with the task ID
|
||||
res.writeHead(200, { "Content-Type": "application/json" })
|
||||
|
||||
Reference in New Issue
Block a user