Compare commits

...

2 Commits

Author SHA1 Message Date
celestial-vault 5d20255140 store mode in controller and target sendStateUpdate by controller ID 2025-06-20 19:30:16 -07:00
celestial-vault b793d262ed you know what im talking about 2025-06-19 22:00:01 -07:00
8 changed files with 160 additions and 76 deletions
+26 -6
View File
@@ -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) || {}
+32 -29
View File
@@ -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)
}
}
+24 -2
View File
@@ -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>,
+14 -1
View File
@@ -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
View File
@@ -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()
+3
View File
@@ -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",
+2 -1
View File
@@ -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.