mirror of
https://github.com/cline/cline.git
synced 2026-09-01 15:11:04 +08:00
Compare commits
2 Commits
fixer
...
multi-cline
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d20255140 | |||
| b793d262ed |
@@ -16,7 +16,7 @@ import { McpHub } from "@services/mcp/McpHub"
|
||||
import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
|
||||
import { ApiProvider, ModelInfo } from "@shared/api"
|
||||
import { ChatContent } from "@shared/ChatContent"
|
||||
import { ChatSettings } from "@shared/ChatSettings"
|
||||
import { ChatSettings, StoredChatSettings } from "@shared/ChatSettings"
|
||||
import { ExtensionMessage, ExtensionState, Platform } from "@shared/ExtensionMessage"
|
||||
import { HistoryItem } from "@shared/HistoryItem"
|
||||
import { McpMarketplaceCatalog } from "@shared/mcp"
|
||||
@@ -51,10 +51,11 @@ https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/c
|
||||
*/
|
||||
|
||||
export class Controller {
|
||||
readonly id: string = uuidv4()
|
||||
readonly id: string
|
||||
private postMessage: (message: ExtensionMessage) => Thenable<boolean> | undefined
|
||||
|
||||
private disposables: vscode.Disposable[] = []
|
||||
private mode: "plan" | "act" = "plan" // In-memory plan/act mode state
|
||||
task?: Task
|
||||
workspaceTracker: WorkspaceTracker
|
||||
mcpHub: McpHub
|
||||
@@ -65,7 +66,9 @@ export class Controller {
|
||||
readonly context: vscode.ExtensionContext,
|
||||
private readonly outputChannel: vscode.OutputChannel,
|
||||
postMessage: (message: ExtensionMessage) => Thenable<boolean> | undefined,
|
||||
id: string,
|
||||
) {
|
||||
this.id = id
|
||||
this.outputChannel.appendLine("ClineProvider instantiated")
|
||||
this.postMessage = postMessage
|
||||
|
||||
@@ -132,7 +135,7 @@ export class Controller {
|
||||
apiConfiguration,
|
||||
autoApprovalSettings,
|
||||
browserSettings,
|
||||
chatSettings,
|
||||
chatSettings: storedChatSettings,
|
||||
shellIntegrationTimeout,
|
||||
terminalReuseEnabled,
|
||||
terminalOutputLineLimit,
|
||||
@@ -142,6 +145,12 @@ export class Controller {
|
||||
taskHistory,
|
||||
} = await getAllExtensionState(this.context)
|
||||
|
||||
// Reconstruct ChatSettings with in-memory mode and stored preferences
|
||||
const chatSettings: ChatSettings = {
|
||||
...storedChatSettings, // Spread stored preferences (preferredLanguage, openAIReasoningEffort)
|
||||
mode: this.mode, // Use in-memory mode (override any stored mode)
|
||||
}
|
||||
|
||||
const NEW_USER_TASK_COUNT_THRESHOLD = 10
|
||||
|
||||
// Check if the user has completed enough tasks to no longer be considered a "new user"
|
||||
@@ -271,6 +280,9 @@ export class Controller {
|
||||
async togglePlanActModeWithChatSettings(chatSettings: ChatSettings, chatContent?: ChatContent): Promise<boolean> {
|
||||
const didSwitchToActMode = chatSettings.mode === "act"
|
||||
|
||||
// Store mode in-memory only
|
||||
this.mode = chatSettings.mode
|
||||
|
||||
// Capture mode switch telemetry | Capture regardless of if we know the taskId
|
||||
telemetryService.captureModeSwitch(this.task?.taskId ?? "0", chatSettings.mode)
|
||||
|
||||
@@ -440,7 +452,9 @@ export class Controller {
|
||||
}
|
||||
}
|
||||
|
||||
await updateWorkspaceState(this.context, "chatSettings", chatSettings)
|
||||
// Save only non-mode properties to workspace storage
|
||||
const { mode, ...persistentChatSettings }: { mode: string } & StoredChatSettings = chatSettings
|
||||
await updateWorkspaceState(this.context, "chatSettings", persistentChatSettings)
|
||||
await this.postStateToWebview()
|
||||
|
||||
if (this.task) {
|
||||
@@ -968,7 +982,7 @@ export class Controller {
|
||||
|
||||
async postStateToWebview() {
|
||||
const state = await this.getStateToPostToWebview()
|
||||
await sendStateUpdate(state)
|
||||
await sendStateUpdate(this.id, state)
|
||||
}
|
||||
|
||||
async getStateToPostToWebview(): Promise<ExtensionState> {
|
||||
@@ -978,7 +992,7 @@ export class Controller {
|
||||
taskHistory,
|
||||
autoApprovalSettings,
|
||||
browserSettings,
|
||||
chatSettings,
|
||||
chatSettings: storedChatSettings,
|
||||
userInfo,
|
||||
mcpMarketplaceEnabled,
|
||||
mcpRichDisplayEnabled,
|
||||
@@ -995,6 +1009,12 @@ export class Controller {
|
||||
terminalOutputLineLimit,
|
||||
} = await getAllExtensionState(this.context)
|
||||
|
||||
// Reconstruct ChatSettings with in-memory mode and stored preferences
|
||||
const chatSettings: ChatSettings = {
|
||||
...storedChatSettings, // Spread stored preferences (preferredLanguage, openAIReasoningEffort)
|
||||
mode: this.mode, // Use in-memory mode (override any stored mode)
|
||||
}
|
||||
|
||||
const localClineRulesToggles =
|
||||
((await getWorkspaceState(this.context, "localClineRulesToggles")) as ClineRulesToggles) || {}
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ import { Controller } from "../index"
|
||||
import { EmptyRequest } from "../../../shared/proto/common"
|
||||
import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler"
|
||||
|
||||
// Keep track of active state subscriptions
|
||||
const activeStateSubscriptions = new Set<StreamingResponseHandler>()
|
||||
// Keep track of active state subscriptions by controller ID
|
||||
const activeStateSubscriptions = new Map<string, StreamingResponseHandler>()
|
||||
|
||||
/**
|
||||
* Subscribe to state updates
|
||||
@@ -19,23 +19,25 @@ export async function subscribeToState(
|
||||
responseStream: StreamingResponseHandler,
|
||||
requestId?: string,
|
||||
): Promise<void> {
|
||||
const controllerId = controller.id
|
||||
|
||||
// Send the initial state
|
||||
const initialState = await controller.getStateToPostToWebview()
|
||||
const initialStateJson = JSON.stringify(initialState)
|
||||
|
||||
console.log("[DEBUG] set up state subscription")
|
||||
console.log(`[DEBUG] set up state subscription for controller ${controllerId}`)
|
||||
|
||||
await responseStream({
|
||||
stateJson: initialStateJson,
|
||||
})
|
||||
|
||||
// Add this subscription to the active subscriptions
|
||||
activeStateSubscriptions.add(responseStream)
|
||||
// Add this subscription to the active subscriptions with the controller ID
|
||||
activeStateSubscriptions.set(controllerId, responseStream)
|
||||
|
||||
// Register cleanup when the connection is closed
|
||||
const cleanup = () => {
|
||||
activeStateSubscriptions.delete(responseStream)
|
||||
console.log("[DEBUG] Cleaned up state subscription")
|
||||
activeStateSubscriptions.delete(controllerId)
|
||||
console.log(`[DEBUG] Cleaned up state subscription for controller ${controllerId}`)
|
||||
}
|
||||
|
||||
// Register the cleanup function with the request registry if we have a requestId
|
||||
@@ -45,30 +47,31 @@ export async function subscribeToState(
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a state update to all active subscribers
|
||||
* Send a state update to a specific controller's subscription
|
||||
* @param controllerId The ID of the controller to send the state to
|
||||
* @param state The state to send
|
||||
*/
|
||||
export async function sendStateUpdate(state: any): Promise<void> {
|
||||
const stateJson = JSON.stringify(state)
|
||||
export async function sendStateUpdate(controllerId: string, state: any): Promise<void> {
|
||||
// Get the subscription for this specific controller
|
||||
const responseStream = activeStateSubscriptions.get(controllerId)
|
||||
|
||||
// Send the update to all active subscribers
|
||||
const promises = Array.from(activeStateSubscriptions).map(async (responseStream) => {
|
||||
try {
|
||||
// The issue might be that we're not properly formatting the response
|
||||
// Let's ensure we're sending a properly formatted State message
|
||||
await responseStream(
|
||||
{
|
||||
stateJson,
|
||||
},
|
||||
false, // Not the last message
|
||||
)
|
||||
console.log("[DEBUG] sending followup state", stateJson.length, "chars")
|
||||
} catch (error) {
|
||||
console.error("Error sending state update:", error)
|
||||
// Remove the subscription if there was an error
|
||||
activeStateSubscriptions.delete(responseStream)
|
||||
}
|
||||
})
|
||||
if (!responseStream) {
|
||||
console.log(`[DEBUG] No active state subscription for controller ${controllerId}`)
|
||||
return
|
||||
}
|
||||
|
||||
await Promise.all(promises)
|
||||
try {
|
||||
const stateJson = JSON.stringify(state)
|
||||
await responseStream(
|
||||
{
|
||||
stateJson,
|
||||
},
|
||||
false, // Not the last message
|
||||
)
|
||||
console.log(`[DEBUG] sending followup state to controller ${controllerId}`, stateJson.length, "chars")
|
||||
} catch (error) {
|
||||
console.error(`Error sending state update to controller ${controllerId}:`, error)
|
||||
// Remove the subscription if there was an error
|
||||
activeStateSubscriptions.delete(controllerId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
import { Controller } from "../index"
|
||||
import { Empty } from "@shared/proto/common"
|
||||
import { WebviewProviderType, WebviewProviderTypeRequest } from "@shared/proto/ui"
|
||||
import { EmptyRequest } from "@shared/proto/common"
|
||||
import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler"
|
||||
|
||||
// Track subscriptions with their provider type
|
||||
const mcpButtonClickedSubscriptions = new Map<StreamingResponseHandler, WebviewProviderType>()
|
||||
// Keep track of active mcpButtonClicked subscriptions by controller ID
|
||||
const activeMcpButtonClickedSubscriptions = new Map<string, StreamingResponseHandler>()
|
||||
|
||||
/**
|
||||
* Subscribe to mcpButtonClicked events
|
||||
* @param controller The controller instance
|
||||
* @param request The webview provider type request
|
||||
* @param request The empty request
|
||||
* @param responseStream The streaming response handler
|
||||
* @param requestId The ID of the request (passed by the gRPC handler)
|
||||
*/
|
||||
export async function subscribeToMcpButtonClicked(
|
||||
_controller: Controller,
|
||||
request: WebviewProviderTypeRequest,
|
||||
controller: Controller,
|
||||
_request: EmptyRequest,
|
||||
responseStream: StreamingResponseHandler,
|
||||
requestId?: string,
|
||||
): Promise<void> {
|
||||
const providerType = request.providerType
|
||||
console.log(`[DEBUG] set up mcpButtonClicked subscription for ${WebviewProviderType[providerType]} webview`)
|
||||
const controllerId = controller.id
|
||||
console.log(`[DEBUG] set up mcpButtonClicked subscription for controller ${controllerId}`)
|
||||
|
||||
// Store the subscription with its provider type
|
||||
mcpButtonClickedSubscriptions.set(responseStream, providerType)
|
||||
// Add this subscription to the active subscriptions with the controller ID
|
||||
activeMcpButtonClickedSubscriptions.set(controllerId, responseStream)
|
||||
|
||||
// Register cleanup when the connection is closed
|
||||
const cleanup = () => {
|
||||
mcpButtonClickedSubscriptions.delete(responseStream)
|
||||
activeMcpButtonClickedSubscriptions.delete(controllerId)
|
||||
}
|
||||
|
||||
// Register the cleanup function with the request registry if we have a requestId
|
||||
@@ -37,26 +37,27 @@ export async function subscribeToMcpButtonClicked(
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a mcpButtonClicked event to active subscribers based on webview type
|
||||
* @param webviewType The type of webview that triggered the event (SIDEBAR or TAB)
|
||||
* Send a mcpButtonClicked event to a specific controller's subscription
|
||||
* @param controllerId The ID of the controller to send the event to
|
||||
*/
|
||||
export async function sendMcpButtonClickedEvent(webviewType?: WebviewProviderType): Promise<void> {
|
||||
const event = Empty.create({})
|
||||
export async function sendMcpButtonClickedEvent(controllerId: string): Promise<void> {
|
||||
// Get the subscription for this specific controller
|
||||
const responseStream = activeMcpButtonClickedSubscriptions.get(controllerId)
|
||||
|
||||
// Process all subscriptions, filtering based on the source
|
||||
const promises = Array.from(mcpButtonClickedSubscriptions.entries()).map(async ([responseStream, providerType]) => {
|
||||
// Only send to subscribers of the same type as the event source
|
||||
if (webviewType !== providerType) {
|
||||
return // Skip subscribers of different types
|
||||
}
|
||||
if (!responseStream) {
|
||||
console.error(`[DEBUG] No active subscription for controller ${controllerId}`)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await responseStream(event, false)
|
||||
} catch (error) {
|
||||
console.error(`Error sending mcpButtonClicked event to ${WebviewProviderType[providerType]}:`, error)
|
||||
mcpButtonClickedSubscriptions.delete(responseStream)
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
try {
|
||||
const event = Empty.create({})
|
||||
await responseStream(
|
||||
event,
|
||||
false, // Not the last message
|
||||
)
|
||||
} catch (error) {
|
||||
console.error(`Error sending mcpButtonClicked event to controller ${controllerId}:`, error)
|
||||
// Remove the subscription if there was an error
|
||||
activeMcpButtonClickedSubscriptions.delete(controllerId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ApiConfiguration, ApiProvider, BedrockModelId, ModelInfo } from "@share
|
||||
import { HistoryItem } from "@shared/HistoryItem"
|
||||
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
|
||||
import { BrowserSettings } from "@shared/BrowserSettings"
|
||||
import { ChatSettings } from "@shared/ChatSettings"
|
||||
import { ChatSettings, StoredChatSettings } from "@shared/ChatSettings"
|
||||
import { TelemetrySetting } from "@shared/TelemetrySetting"
|
||||
import { UserInfo } from "@shared/UserInfo"
|
||||
import { ClineRulesToggles } from "@shared/cline-rules"
|
||||
@@ -173,6 +173,28 @@ export async function migrateCustomInstructionsToGlobalRules(context: vscode.Ext
|
||||
}
|
||||
}
|
||||
|
||||
export async function cleanupModeFromWorkspaceStorage(context: vscode.ExtensionContext) {
|
||||
try {
|
||||
// Get current chatSettings from workspace storage
|
||||
const chatSettings = (await getWorkspaceState(context, "chatSettings")) as any
|
||||
|
||||
if (chatSettings && typeof chatSettings === "object" && "mode" in chatSettings) {
|
||||
console.log("Cleaning up mode from workspace storage...")
|
||||
|
||||
// Remove mode property from chatSettings
|
||||
const { mode, ...cleanedChatSettings } = chatSettings
|
||||
|
||||
// Save cleaned chatSettings back to workspace storage
|
||||
await updateWorkspaceState(context, "chatSettings", cleanedChatSettings)
|
||||
|
||||
console.log("Successfully removed mode from workspace storage chatSettings")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to cleanup mode from workspace storage:", error)
|
||||
// Continue execution - migration failure shouldn't break extension startup
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
const [
|
||||
isNewUser,
|
||||
@@ -360,7 +382,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
previousModeSapAiCoreResourceGroup,
|
||||
previousModeSapAiCoreModelId,
|
||||
] = await Promise.all([
|
||||
getWorkspaceState(context, "chatSettings") as Promise<ChatSettings | undefined>,
|
||||
getWorkspaceState(context, "chatSettings") as Promise<StoredChatSettings | undefined>,
|
||||
getWorkspaceState(context, "apiProvider") as Promise<ApiProvider | undefined>,
|
||||
getWorkspaceState(context, "apiModelId") as Promise<string | undefined>,
|
||||
getWorkspaceState(context, "thinkingBudgetTokens") as Promise<number | undefined>,
|
||||
|
||||
@@ -28,7 +28,7 @@ export abstract class WebviewProvider {
|
||||
WebviewProvider.activeInstances.add(this)
|
||||
this.clientId = uuidv4()
|
||||
WebviewProvider.clientIdMap.set(this, this.clientId)
|
||||
this.controller = new Controller(context, outputChannel, (message) => this.postMessageToWebview(message))
|
||||
this.controller = new Controller(context, outputChannel, (message) => this.postMessageToWebview(message), this.clientId)
|
||||
}
|
||||
|
||||
// Add a method to get the client ID
|
||||
@@ -58,6 +58,19 @@ export abstract class WebviewProvider {
|
||||
return findLast(Array.from(this.activeInstances), (instance) => instance.isVisible() === true)
|
||||
}
|
||||
|
||||
public static getActiveInstance(): WebviewProvider | undefined {
|
||||
return Array.from(this.activeInstances).find((instance) => {
|
||||
if (
|
||||
instance.getWebview() &&
|
||||
instance.getWebview().viewType === "claude-dev.TabPanelProvider" &&
|
||||
"active" in instance.getWebview()
|
||||
) {
|
||||
return instance.getWebview().active === true
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
public static getAllInstances(): WebviewProvider[] {
|
||||
return Array.from(this.activeInstances)
|
||||
}
|
||||
|
||||
+28
-7
@@ -22,7 +22,11 @@ import { WebviewProviderType as WebviewProviderTypeEnum } from "@shared/proto/ui
|
||||
import { WebviewProviderType } from "./shared/webview/types"
|
||||
import { sendHistoryButtonClickedEvent } from "./core/controller/ui/subscribeToHistoryButtonClicked"
|
||||
import { sendAccountButtonClickedEvent } from "./core/controller/ui/subscribeToAccountButtonClicked"
|
||||
import { migratePlanActGlobalToWorkspaceStorage, migrateCustomInstructionsToGlobalRules } from "./core/storage/state"
|
||||
import {
|
||||
migratePlanActGlobalToWorkspaceStorage,
|
||||
migrateCustomInstructionsToGlobalRules,
|
||||
cleanupModeFromWorkspaceStorage,
|
||||
} from "./core/storage/state"
|
||||
|
||||
import { sendFocusChatInputEvent } from "./core/controller/ui/subscribeToFocusChatInput"
|
||||
import { FileContextTracker } from "./core/context/context-tracking/FileContextTracker"
|
||||
@@ -59,6 +63,9 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
// Migrate custom instructions to global Cline rules (one-time cleanup)
|
||||
await migrateCustomInstructionsToGlobalRules(context)
|
||||
|
||||
// Clean up mode from workspace storage (one-time cleanup)
|
||||
await cleanupModeFromWorkspaceStorage(context)
|
||||
|
||||
// Clean up orphaned file context warnings (startup cleanup)
|
||||
await FileContextTracker.cleanupOrphanedWarnings(context)
|
||||
|
||||
@@ -141,12 +148,26 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("cline.mcpButtonClicked", (webview: any) => {
|
||||
console.log("[DEBUG] mcpButtonClicked", webview)
|
||||
// Pass the webview type to the event sender
|
||||
const isSidebar = !webview
|
||||
const webviewType = isSidebar ? WebviewProviderTypeEnum.SIDEBAR : WebviewProviderTypeEnum.TAB
|
||||
|
||||
// Will send to appropriate subscribers based on the source webview type
|
||||
sendMcpButtonClickedEvent(webviewType)
|
||||
const activeInstance = WebviewProvider.getActiveInstance()
|
||||
const isSidebar = !webview
|
||||
|
||||
if (isSidebar) {
|
||||
const sidebarInstance = WebviewProvider.getSidebarInstance()
|
||||
const sidebarInstanceId = sidebarInstance?.getClientId()
|
||||
if (sidebarInstanceId) {
|
||||
sendMcpButtonClickedEvent(sidebarInstanceId)
|
||||
} else {
|
||||
console.error("[DEBUG] No sidebar instance found, cannot send MCP button event")
|
||||
}
|
||||
} else {
|
||||
const activeInstanceId = activeInstance?.getClientId()
|
||||
if (activeInstanceId) {
|
||||
sendMcpButtonClickedEvent(activeInstanceId)
|
||||
} else {
|
||||
console.error("[DEBUG] No active instance found, cannot send MCP button event")
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -628,7 +649,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
} else {
|
||||
// Create a temporary controller just for this operation
|
||||
const outputChannel = vscode.window.createOutputChannel("Cline Commit Generator")
|
||||
const tempController = new Controller(context, outputChannel, () => Promise.resolve(true))
|
||||
const tempController = new Controller(context, outputChannel, () => Promise.resolve(true), uuidv4())
|
||||
|
||||
await tempController.generateGitCommitMessage()
|
||||
outputChannel.dispose()
|
||||
|
||||
@@ -8,6 +8,9 @@ export interface ChatSettings {
|
||||
|
||||
export type PartialChatSettings = Partial<ChatSettings>
|
||||
|
||||
// Type for chat settings stored in workspace (excludes in-memory mode)
|
||||
export type StoredChatSettings = Omit<ChatSettings, "mode">
|
||||
|
||||
export const DEFAULT_CHAT_SETTINGS: ChatSettings = {
|
||||
mode: "act",
|
||||
preferredLanguage: "English",
|
||||
|
||||
@@ -11,13 +11,14 @@ import { addProtobusServices } from "@generated/standalone/server-setup"
|
||||
import { StreamingResponseHandler } from "@/core/controller/grpc-handler"
|
||||
import { ExternalHostBridgeClientManager } from "./host-bridge-client-manager"
|
||||
import { ExternalWebviewProvider } from "./ExternalWebviewProvider"
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
|
||||
async function main() {
|
||||
log("Starting standalone service...")
|
||||
|
||||
hostProviders.initializeHostProviders(ExternalWebviewProvider.create, new ExternalHostBridgeClientManager())
|
||||
activate(extensionContext)
|
||||
const controller = new Controller(extensionContext, outputChannel, postMessage)
|
||||
const controller = new Controller(extensionContext, outputChannel, postMessage, uuidv4())
|
||||
const server = new grpc.Server()
|
||||
|
||||
// Set up health check.
|
||||
|
||||
Reference in New Issue
Block a user