Compare commits

...

3 Commits

Author SHA1 Message Date
celestial-vault 5df675a51a merge conflicts 2025-06-20 19:35:32 -07:00
celestial-vault 8eb06bedeb store mode in controller and target sendStateUpdate by controller ID 2025-06-20 19:31:18 -07:00
celestial-vault b793d262ed you know what im talking about 2025-06-19 22:00:01 -07:00
5 changed files with 90 additions and 37 deletions
+23 -5
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"
@@ -55,6 +55,7 @@ export class Controller {
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
@@ -134,7 +135,7 @@ export class Controller {
apiConfiguration,
autoApprovalSettings,
browserSettings,
chatSettings,
chatSettings: storedChatSettings,
shellIntegrationTimeout,
terminalReuseEnabled,
terminalOutputLineLimit,
@@ -144,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"
@@ -273,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)
@@ -442,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) {
@@ -970,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> {
@@ -980,7 +992,7 @@ export class Controller {
taskHistory,
autoApprovalSettings,
browserSettings,
chatSettings,
chatSettings: storedChatSettings,
userInfo,
mcpMarketplaceEnabled,
mcpRichDisplayEnabled,
@@ -997,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)
}
}
+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>,
+8 -1
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"
@@ -60,6 +64,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)
+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",