import { safeWriteJson } from "../../utils/safeWriteJson" import * as path from "path" import * as os from "os" import * as fs from "fs/promises" import pWaitFor from "p-wait-for" import * as vscode from "vscode" // kilocode_change start import axios from "axios" import { getKiloBaseUriFromToken, isGlobalStateKey } from "@roo-code/types" import { MaybeTypedWebviewMessage, ProfileData, SeeNewChangesPayload, TaskHistoryRequestPayload, TasksByIdRequestPayload, UpdateGlobalStateMessage, } from "../../shared/WebviewMessage" // kilocode_change end import { type Language, type GlobalState, type ClineMessage, type TelemetrySetting, TelemetryEventName, // kilocode_change start ghostServiceSettingsSchema, fastApplyModelSchema, // kilocode_change end UserSettingsConfig, } from "@roo-code/types" import { CloudService } from "@roo-code/cloud" import { TelemetryService } from "@roo-code/telemetry" import { type ApiMessage } from "../task-persistence/apiMessages" import { saveTaskMessages } from "../task-persistence" import { ClineProvider } from "./ClineProvider" import { handleCheckpointRestoreOperation } from "./checkpointRestoreHandler" import { changeLanguage, t } from "../../i18n" import { Package } from "../../shared/package" import { type RouterName, type ModelRecord, toRouterName } from "../../shared/api" import { MessageEnhancer } from "./messageEnhancer" import { type WebviewMessage, type EditQueuedMessagePayload, checkoutDiffPayloadSchema, checkoutRestorePayloadSchema, } from "../../shared/WebviewMessage" import { checkExistKey } from "../../shared/checkExistApiConfig" import { experimentDefault } from "../../shared/experiments" import { Terminal } from "../../integrations/terminal/Terminal" import { openFile } from "../../integrations/misc/open-file" import { openImage, saveImage } from "../../integrations/misc/image-handler" import { selectImages } from "../../integrations/misc/process-images" import { getTheme } from "../../integrations/theme/getTheme" import { discoverChromeHostUrl, tryChromeHostUrl } from "../../services/browser/browserDiscovery" import { searchWorkspaceFiles } from "../../services/search/file-search" import { fileExistsAtPath } from "../../utils/fs" import { playTts, setTtsEnabled, setTtsSpeed, stopTts } from "../../utils/tts" import { showSystemNotification } from "../../integrations/notifications" // kilocode_change import { singleCompletionHandler } from "../../utils/single-completion-handler" // kilocode_change import { searchCommits } from "../../utils/git" import { exportSettings, importSettingsWithFeedback } from "../config/importExport" import { getOpenAiModels } from "../../api/providers/openai" import { getVsCodeLmModels } from "../../api/providers/vscode-lm" import { openMention } from "../mentions" import { getWorkspacePath } from "../../utils/path" import { Mode, defaultModeSlug } from "../../shared/modes" import { getModels, flushModels } from "../../api/providers/fetchers/modelCache" import { GetModelsOptions } from "../../shared/api" import { generateSystemPrompt } from "./generateSystemPrompt" import { getCommand } from "../../utils/commands" import { toggleWorkflow, toggleRule, createRuleFile, deleteRuleFile } from "./kilorules" import { mermaidFixPrompt } from "../prompts/utilities/mermaid" // kilocode_change import { editMessageHandler, fetchKilocodeNotificationsHandler } from "../kilocode/webview/webviewMessageHandlerUtils" // kilocode_change const ALLOWED_VSCODE_SETTINGS = new Set(["terminal.integrated.inheritEnv"]) import { MarketplaceManager, MarketplaceItemType } from "../../services/marketplace" import { setPendingTodoList } from "../tools/updateTodoListTool" import { UsageTracker } from "../../utils/usage-tracker" import { seeNewChanges } from "../checkpoints/kilocode/seeNewChanges" // kilocode_change import { getTaskHistory } from "../../shared/kilocode/getTaskHistory" // kilocode_change import { fetchAndRefreshOrganizationModesOnStartup, refreshOrganizationModes } from "./kiloWebviewMessgeHandlerHelpers" export const webviewMessageHandler = async ( provider: ClineProvider, message: MaybeTypedWebviewMessage, // kilocode_change switch to MaybeTypedWebviewMessage for better type-safety marketplaceManager?: MarketplaceManager, ) => { // Utility functions provided for concise get/update of global state via contextProxy API. const getGlobalState = (key: K) => provider.contextProxy.getValue(key) const updateGlobalState = async (key: K, value: GlobalState[K]) => await provider.contextProxy.setValue(key, value) const getCurrentCwd = () => { return provider.getCurrentTask()?.cwd || provider.cwd } /** * Shared utility to find message indices based on timestamp */ const findMessageIndices = (messageTs: number, currentCline: any) => { // Find the exact message by timestamp, not the first one after a cutoff const messageIndex = currentCline.clineMessages.findIndex((msg: ClineMessage) => msg.ts === messageTs) const apiConversationHistoryIndex = currentCline.apiConversationHistory.findIndex( (msg: ApiMessage) => msg.ts === messageTs, ) return { messageIndex, apiConversationHistoryIndex } } /** * Fallback: find first API history index at or after a timestamp. * Used when the exact user message isn't present in apiConversationHistory (e.g., after condense). */ const findFirstApiIndexAtOrAfter = (ts: number, currentCline: any) => { if (typeof ts !== "number") return -1 return currentCline.apiConversationHistory.findIndex( (msg: ApiMessage) => typeof msg?.ts === "number" && (msg.ts as number) >= ts, ) } /** * Removes the target message and all subsequent messages */ const removeMessagesThisAndSubsequent = async ( currentCline: any, messageIndex: number, apiConversationHistoryIndex: number, ) => { // Delete this message and all that follow await currentCline.overwriteClineMessages(currentCline.clineMessages.slice(0, messageIndex)) if (apiConversationHistoryIndex !== -1) { await currentCline.overwriteApiConversationHistory( currentCline.apiConversationHistory.slice(0, apiConversationHistoryIndex), ) } } /** * Handles message deletion operations with user confirmation */ const handleDeleteOperation = async (messageTs: number): Promise => { // Check if there's a checkpoint before this message const currentCline = provider.getCurrentTask() let hasCheckpoint = false if (!currentCline) { await vscode.window.showErrorMessage(t("common:errors.message.no_active_task_to_delete")) return } const { messageIndex } = findMessageIndices(messageTs, currentCline) if (messageIndex !== -1) { // Find the last checkpoint before this message const checkpoints = currentCline.clineMessages.filter( (msg) => msg.say === "checkpoint_saved" && msg.ts > messageTs, ) hasCheckpoint = checkpoints.length > 0 } // Send message to webview to show delete confirmation dialog await provider.postMessageToWebview({ type: "showDeleteMessageDialog", messageTs, hasCheckpoint, }) } /** * Handles confirmed message deletion from webview dialog */ const handleDeleteMessageConfirm = async (messageTs: number, restoreCheckpoint?: boolean): Promise => { const currentCline = provider.getCurrentTask() if (!currentCline) { console.error("[handleDeleteMessageConfirm] No current cline available") return } const { messageIndex, apiConversationHistoryIndex } = findMessageIndices(messageTs, currentCline) // Determine API truncation index with timestamp fallback if exact match not found let apiIndexToUse = apiConversationHistoryIndex const tsThreshold = currentCline.clineMessages[messageIndex]?.ts if (apiIndexToUse === -1 && typeof tsThreshold === "number") { apiIndexToUse = findFirstApiIndexAtOrAfter(tsThreshold, currentCline) } if (messageIndex === -1) { await vscode.window.showErrorMessage(t("common:errors.message.message_not_found", { messageTs })) return } try { const targetMessage = currentCline.clineMessages[messageIndex] // If checkpoint restoration is requested, find and restore to the last checkpoint before this message if (restoreCheckpoint) { // Find the last checkpoint before this message const checkpoints = currentCline.clineMessages.filter( (msg) => msg.say === "checkpoint_saved" && msg.ts > messageTs, ) const nextCheckpoint = checkpoints[0] if (nextCheckpoint && nextCheckpoint.text) { await handleCheckpointRestoreOperation({ provider, currentCline, messageTs: targetMessage.ts!, messageIndex, checkpoint: { hash: nextCheckpoint.text }, operation: "delete", }) } else { // No checkpoint found before this message console.log("[handleDeleteMessageConfirm] No checkpoint found before message") vscode.window.showWarningMessage("No checkpoint found before this message") } } else { // For non-checkpoint deletes, preserve checkpoint associations for remaining messages // Store checkpoints from messages that will be preserved const preservedCheckpoints = new Map() for (let i = 0; i < messageIndex; i++) { const msg = currentCline.clineMessages[i] if (msg?.checkpoint && msg.ts) { preservedCheckpoints.set(msg.ts, msg.checkpoint) } } // Delete this message and all subsequent messages await removeMessagesThisAndSubsequent(currentCline, messageIndex, apiIndexToUse) // Restore checkpoint associations for preserved messages for (const [ts, checkpoint] of preservedCheckpoints) { const msgIndex = currentCline.clineMessages.findIndex((msg) => msg.ts === ts) if (msgIndex !== -1) { currentCline.clineMessages[msgIndex].checkpoint = checkpoint } } // Save the updated messages with restored checkpoints await saveTaskMessages({ messages: currentCline.clineMessages, taskId: currentCline.taskId, globalStoragePath: provider.contextProxy.globalStorageUri.fsPath, }) // Update the UI to reflect the deletion await provider.postStateToWebview() } } catch (error) { console.error("Error in delete message:", error) vscode.window.showErrorMessage( t("common:errors.message.error_deleting_message", { error: error instanceof Error ? error.message : String(error), }), ) } } /** * Handles message editing operations with user confirmation */ const handleEditOperation = async (messageTs: number, editedContent: string, images?: string[]): Promise => { // Check if there's a checkpoint before this message const currentCline = provider.getCurrentTask() let hasCheckpoint = false if (currentCline) { const { messageIndex } = findMessageIndices(messageTs, currentCline) if (messageIndex !== -1) { // Find the last checkpoint before this message const checkpoints = currentCline.clineMessages.filter( (msg) => msg.say === "checkpoint_saved" && msg.ts > messageTs, ) hasCheckpoint = checkpoints.length > 0 } else { console.log("[webviewMessageHandler] Edit - Message not found in clineMessages!") } } else { console.log("[webviewMessageHandler] Edit - No currentCline available!") } // Send message to webview to show edit confirmation dialog await provider.postMessageToWebview({ type: "showEditMessageDialog", messageTs, text: editedContent, hasCheckpoint, images, }) } /** * Handles confirmed message editing from webview dialog */ const handleEditMessageConfirm = async ( messageTs: number, editedContent: string, restoreCheckpoint?: boolean, images?: string[], ): Promise => { const currentCline = provider.getCurrentTask() if (!currentCline) { console.error("[handleEditMessageConfirm] No current cline available") return } // Use findMessageIndices to find messages based on timestamp const { messageIndex, apiConversationHistoryIndex } = findMessageIndices(messageTs, currentCline) if (messageIndex === -1) { const errorMessage = t("common:errors.message.message_not_found", { messageTs }) console.error("[handleEditMessageConfirm]", errorMessage) await vscode.window.showErrorMessage(errorMessage) return } try { const targetMessage = currentCline.clineMessages[messageIndex] // If checkpoint restoration is requested, find and restore to the last checkpoint before this message if (restoreCheckpoint) { // Find the last checkpoint before this message const checkpoints = currentCline.clineMessages.filter( (msg) => msg.say === "checkpoint_saved" && msg.ts > messageTs, ) const nextCheckpoint = checkpoints[0] if (nextCheckpoint && nextCheckpoint.text) { await handleCheckpointRestoreOperation({ provider, currentCline, messageTs: targetMessage.ts!, messageIndex, checkpoint: { hash: nextCheckpoint.text }, operation: "edit", editData: { editedContent, images, apiConversationHistoryIndex, }, }) // The task will be cancelled and reinitialized by checkpointRestore // The pending edit will be processed in the reinitialized task return } else { // No checkpoint found before this message console.log("[handleEditMessageConfirm] No checkpoint found before message") vscode.window.showWarningMessage("No checkpoint found before this message") // Continue with non-checkpoint edit } } // For non-checkpoint edits, remove the ORIGINAL user message being edited and all subsequent messages // Determine the correct starting index to delete from (prefer the last preceding user_feedback message) let deleteFromMessageIndex = messageIndex let deleteFromApiIndex = apiConversationHistoryIndex // Find the nearest preceding user message to ensure we replace the original, not just the assistant reply for (let i = messageIndex; i >= 0; i--) { const m = currentCline.clineMessages[i] if (m?.say === "user_feedback") { deleteFromMessageIndex = i // Align API history truncation to the same user message timestamp if present const userTs = m.ts if (typeof userTs === "number") { const apiIdx = currentCline.apiConversationHistory.findIndex( (am: ApiMessage) => am.ts === userTs, ) if (apiIdx !== -1) { deleteFromApiIndex = apiIdx } } break } } // Timestamp fallback for API history when exact user message isn't present if (deleteFromApiIndex === -1) { const tsThresholdForEdit = currentCline.clineMessages[deleteFromMessageIndex]?.ts if (typeof tsThresholdForEdit === "number") { deleteFromApiIndex = findFirstApiIndexAtOrAfter(tsThresholdForEdit, currentCline) } } // Store checkpoints from messages that will be preserved const preservedCheckpoints = new Map() for (let i = 0; i < deleteFromMessageIndex; i++) { const msg = currentCline.clineMessages[i] if (msg?.checkpoint && msg.ts) { preservedCheckpoints.set(msg.ts, msg.checkpoint) } } // Delete the original (user) message and all subsequent messages await removeMessagesThisAndSubsequent(currentCline, deleteFromMessageIndex, deleteFromApiIndex) // Restore checkpoint associations for preserved messages for (const [ts, checkpoint] of preservedCheckpoints) { const msgIndex = currentCline.clineMessages.findIndex((msg) => msg.ts === ts) if (msgIndex !== -1) { currentCline.clineMessages[msgIndex].checkpoint = checkpoint } } // Save the updated messages with restored checkpoints await saveTaskMessages({ messages: currentCline.clineMessages, taskId: currentCline.taskId, globalStoragePath: provider.contextProxy.globalStorageUri.fsPath, }) // Update the UI to reflect the deletion await provider.postStateToWebview() await currentCline.submitUserMessage(editedContent, images) } catch (error) { console.error("Error in edit message:", error) vscode.window.showErrorMessage( t("common:errors.message.error_editing_message", { error: error instanceof Error ? error.message : String(error), }), ) } } /** * Handles message modification operations (delete or edit) with confirmation dialog * @param messageTs Timestamp of the message to operate on * @param operation Type of operation ('delete' or 'edit') * @param editedContent New content for edit operations * @returns Promise */ const handleMessageModificationsOperation = async ( messageTs: number, operation: "delete" | "edit", editedContent?: string, images?: string[], ): Promise => { if (operation === "delete") { await handleDeleteOperation(messageTs) } else if (operation === "edit" && editedContent) { await handleEditOperation(messageTs, editedContent, images) } } switch (message.type) { case "webviewDidLaunch": // Load custom modes first const customModes = await provider.customModesManager.getCustomModes() await updateGlobalState("customModes", customModes) // kilocode_change start: Fetch organization modes on startup // Fetch organization modes on startup if an organization is selected await fetchAndRefreshOrganizationModesOnStartup(provider, updateGlobalState) // kilocode_change end // Refresh workflow toggles const { refreshWorkflowToggles } = await import("../context/instructions/workflows") // kilocode_change await refreshWorkflowToggles(provider.context, provider.cwd) // kilocode_change provider.postStateToWebview() provider.postRulesDataToWebview() // kilocode_change: send workflows and rules immediately provider.workspaceTracker?.initializeFilePaths() // Don't await. getTheme().then((theme) => provider.postMessageToWebview({ type: "theme", text: JSON.stringify(theme) })) // If MCP Hub is already initialized, update the webview with // current server list. const mcpHub = provider.getMcpHub() if (mcpHub) { provider.postMessageToWebview({ type: "mcpServers", mcpServers: mcpHub.getAllServers() }) } provider.providerSettingsManager .listConfig() .then(async (listApiConfig) => { if (!listApiConfig) { return } if (listApiConfig.length === 1) { // Check if first time init then sync with exist config. if (!checkExistKey(listApiConfig[0])) { const { apiConfiguration } = await provider.getState() await provider.providerSettingsManager.saveConfig( listApiConfig[0].name ?? "default", apiConfiguration, ) listApiConfig[0].apiProvider = apiConfiguration.apiProvider } } const currentConfigName = getGlobalState("currentApiConfigName") if (currentConfigName) { if (!(await provider.providerSettingsManager.hasConfig(currentConfigName))) { // Current config name not valid, get first config in list. const name = listApiConfig[0]?.name await updateGlobalState("currentApiConfigName", name) if (name) { await provider.activateProviderProfile({ name }) return } } } await Promise.all([ await updateGlobalState("listApiConfigMeta", listApiConfig), await provider.postMessageToWebview({ type: "listApiConfig", listApiConfig }), ]) }) .catch((error) => provider.log( `Error list api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, ), ) // If user already opted in to telemetry, enable telemetry service provider.getStateToPostToWebview().then(async (/*kilocode_change*/ state) => { const { telemetrySetting } = state const isOptedIn = telemetrySetting !== "disabled" TelemetryService.instance.updateTelemetryState(isOptedIn) await TelemetryService.instance.updateIdentity(state.apiConfiguration.kilocodeToken ?? "") // kilocode_change }) provider.isViewLaunched = true break case "newTask": // 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. try { await provider.createTask(message.text, message.images) // Task created successfully - notify the UI to reset await provider.postMessageToWebview({ type: "invoke", invoke: "newChat", }) } catch (error) { // For all errors, reset the UI and show error await provider.postMessageToWebview({ type: "invoke", invoke: "newChat", }) // Show error to user vscode.window.showErrorMessage( `Failed to create task: ${error instanceof Error ? error.message : String(error)}`, ) } break // kilocode_change start case "condense": provider.getCurrentTask()?.handleWebviewAskResponse("yesButtonClicked") break // kilocode_change end case "customInstructions": await provider.updateCustomInstructions(message.text) break case "alwaysAllowReadOnly": await updateGlobalState("alwaysAllowReadOnly", message.bool ?? undefined) await provider.postStateToWebview() break case "alwaysAllowReadOnlyOutsideWorkspace": await updateGlobalState("alwaysAllowReadOnlyOutsideWorkspace", message.bool ?? undefined) await provider.postStateToWebview() break case "alwaysAllowWrite": await updateGlobalState("alwaysAllowWrite", message.bool ?? undefined) await provider.postStateToWebview() break case "alwaysAllowWriteOutsideWorkspace": await updateGlobalState("alwaysAllowWriteOutsideWorkspace", message.bool ?? undefined) await provider.postStateToWebview() break case "alwaysAllowWriteProtected": await updateGlobalState("alwaysAllowWriteProtected", message.bool ?? undefined) await provider.postStateToWebview() break case "alwaysAllowExecute": await updateGlobalState("alwaysAllowExecute", message.bool ?? undefined) await provider.postStateToWebview() break case "alwaysAllowBrowser": await updateGlobalState("alwaysAllowBrowser", message.bool ?? undefined) await provider.postStateToWebview() break case "alwaysAllowMcp": await updateGlobalState("alwaysAllowMcp", message.bool) await provider.postStateToWebview() break case "alwaysAllowModeSwitch": await updateGlobalState("alwaysAllowModeSwitch", message.bool) await provider.postStateToWebview() break case "allowedMaxRequests": await updateGlobalState("allowedMaxRequests", message.value) await provider.postStateToWebview() break case "allowedMaxCost": await updateGlobalState("allowedMaxCost", message.value) await provider.postStateToWebview() break case "alwaysAllowSubtasks": await updateGlobalState("alwaysAllowSubtasks", message.bool) await provider.postStateToWebview() break case "alwaysAllowUpdateTodoList": await updateGlobalState("alwaysAllowUpdateTodoList", message.bool) await provider.postStateToWebview() break case "askResponse": provider.getCurrentTask()?.handleWebviewAskResponse(message.askResponse!, message.text, message.images) break case "autoCondenseContext": await updateGlobalState("autoCondenseContext", message.bool) await provider.postStateToWebview() break case "autoCondenseContextPercent": await updateGlobalState("autoCondenseContextPercent", message.value) await provider.postStateToWebview() break case "terminalOperation": if (message.terminalOperation) { provider.getCurrentTask()?.handleTerminalOperation(message.terminalOperation) } break case "clearTask": // Clear task resets the current session and allows for a new task // to be started, if this session is a subtask - it allows the // parent task to be resumed. // Check if the current task actually has a parent task. const currentTask = provider.getCurrentTask() if (currentTask && currentTask.parentTask) { await provider.finishSubTask(t("common:tasks.canceled")) } else { // Regular task - just clear it await provider.clearTask() } await provider.postStateToWebview() break case "didShowAnnouncement": await updateGlobalState("lastShownAnnouncementId", provider.latestAnnouncementId) await provider.postStateToWebview() break case "selectImages": const images = await selectImages() await provider.postMessageToWebview({ type: "selectedImages", images, context: message.context, messageTs: message.messageTs, }) break case "exportCurrentTask": const currentTaskId = provider.getCurrentTask()?.taskId if (currentTaskId) { provider.exportTaskWithId(currentTaskId) } break case "shareCurrentTask": const shareTaskId = provider.getCurrentTask()?.taskId const clineMessages = provider.getCurrentTask()?.clineMessages if (!shareTaskId) { vscode.window.showErrorMessage(t("common:errors.share_no_active_task")) break } try { const visibility = message.visibility || "organization" const result = await CloudService.instance.shareTask(shareTaskId, visibility) if (result.success && result.shareUrl) { // Show success notification const messageKey = visibility === "public" ? "common:info.public_share_link_copied" : "common:info.organization_share_link_copied" vscode.window.showInformationMessage(t(messageKey)) // Send success feedback to webview for inline display await provider.postMessageToWebview({ type: "shareTaskSuccess", visibility, text: result.shareUrl, }) } else { // Handle error const errorMessage = result.error || "Failed to create share link" if (errorMessage.includes("Authentication")) { vscode.window.showErrorMessage(t("common:errors.share_auth_required")) } else if (errorMessage.includes("sharing is not enabled")) { vscode.window.showErrorMessage(t("common:errors.share_not_enabled")) } else if (errorMessage.includes("not found")) { vscode.window.showErrorMessage(t("common:errors.share_task_not_found")) } else { vscode.window.showErrorMessage(errorMessage) } } } catch (error) { provider.log(`[shareCurrentTask] Unexpected error: ${error}`) vscode.window.showErrorMessage(t("common:errors.share_task_failed")) } break case "showTaskWithId": provider.showTaskWithId(message.text!) break case "condenseTaskContextRequest": provider.condenseTaskContext(message.text!) break case "deleteTaskWithId": provider.deleteTaskWithId(message.text!) break case "deleteMultipleTasksWithIds": { const ids = message.ids if (Array.isArray(ids)) { // Process in batches of 20 (or another reasonable number) const batchSize = 20 const results = [] // Only log start and end of the operation console.log(`Batch deletion started: ${ids.length} tasks total`) for (let i = 0; i < ids.length; i += batchSize) { const batch = ids.slice(i, i + batchSize) const batchPromises = batch.map(async (id) => { try { await provider.deleteTaskWithId(id) return { id, success: true } } catch (error) { // Keep error logging for debugging purposes console.log( `Failed to delete task ${id}: ${error instanceof Error ? error.message : String(error)}`, ) return { id, success: false } } }) // Process each batch in parallel but wait for completion before starting the next batch const batchResults = await Promise.all(batchPromises) results.push(...batchResults) // Update the UI after each batch to show progress await provider.postStateToWebview() } // Log final results const successCount = results.filter((r) => r.success).length const failCount = results.length - successCount console.log( `Batch deletion completed: ${successCount}/${ids.length} tasks successful, ${failCount} tasks failed`, ) } break } case "exportTaskWithId": provider.exportTaskWithId(message.text!) break case "importSettings": { await importSettingsWithFeedback({ providerSettingsManager: provider.providerSettingsManager, contextProxy: provider.contextProxy, customModesManager: provider.customModesManager, provider: provider, }) break } case "exportSettings": await exportSettings({ providerSettingsManager: provider.providerSettingsManager, contextProxy: provider.contextProxy, }) break case "resetState": await provider.resetState() break case "flushRouterModels": const routerNameFlush: RouterName = toRouterName(message.text) await flushModels(routerNameFlush) break case "requestRouterModels": const { apiConfiguration } = await provider.getState() const routerModels: Record = { openrouter: {}, gemini: {}, // kilocode_change "vercel-ai-gateway": {}, huggingface: {}, litellm: {}, "kilocode-openrouter": {}, // kilocode_change deepinfra: {}, "io-intelligence": {}, requesty: {}, unbound: {}, glama: {}, chutes: {}, // kilocode_change ollama: {}, lmstudio: {}, ovhcloud: {}, // kilocode_change } const safeGetModels = async (options: GetModelsOptions): Promise => { try { return await getModels(options) } catch (error) { console.error( `Failed to fetch models in webviewMessageHandler requestRouterModels for ${options.provider}:`, error, ) throw error // Re-throw to be caught by Promise.allSettled. } } // kilocode_change start: openrouter auth, kilocode provider const openRouterApiKey = apiConfiguration.openRouterApiKey || message?.values?.openRouterApiKey const openRouterBaseUrl = apiConfiguration.openRouterBaseUrl || message?.values?.openRouterBaseUrl const modelFetchPromises: Array<{ key: RouterName; options: GetModelsOptions }> = [ { key: "openrouter", options: { provider: "openrouter", apiKey: openRouterApiKey, baseUrl: openRouterBaseUrl }, }, // kilocode_change start { key: "gemini", options: { provider: "gemini", apiKey: apiConfiguration.geminiApiKey, baseUrl: apiConfiguration.googleGeminiBaseUrl, }, }, // kilocode_change end { key: "requesty", options: { provider: "requesty", apiKey: apiConfiguration.requestyApiKey, baseUrl: apiConfiguration.requestyBaseUrl, }, }, { key: "glama", options: { provider: "glama" } }, { key: "unbound", options: { provider: "unbound", apiKey: apiConfiguration.unboundApiKey } }, { key: "chutes", options: { provider: "chutes", apiKey: apiConfiguration.chutesApiKey } }, // kilocode_change { key: "kilocode-openrouter", options: { provider: "kilocode-openrouter", kilocodeToken: apiConfiguration.kilocodeToken, kilocodeOrganizationId: apiConfiguration.kilocodeOrganizationId, }, }, { key: "ollama", options: { provider: "ollama", baseUrl: apiConfiguration.ollamaBaseUrl } }, { key: "vercel-ai-gateway", options: { provider: "vercel-ai-gateway" } }, { key: "deepinfra", options: { provider: "deepinfra", apiKey: apiConfiguration.deepInfraApiKey, baseUrl: apiConfiguration.deepInfraBaseUrl, }, }, // kilocode_change start { key: "ovhcloud", options: { provider: "ovhcloud", apiKey: apiConfiguration.ovhCloudAiEndpointsApiKey, baseUrl: apiConfiguration.ovhCloudAiEndpointsBaseUrl, }, }, // kilocode_change end ] // kilocode_change end // Add IO Intelligence if API key is provided. const ioIntelligenceApiKey = apiConfiguration.ioIntelligenceApiKey if (ioIntelligenceApiKey) { modelFetchPromises.push({ key: "io-intelligence", options: { provider: "io-intelligence", apiKey: ioIntelligenceApiKey }, }) } // Don't fetch Ollama and LM Studio models by default anymore. // They have their own specific handlers: requestOllamaModels and requestLmStudioModels. const litellmApiKey = apiConfiguration.litellmApiKey || message?.values?.litellmApiKey const litellmBaseUrl = apiConfiguration.litellmBaseUrl || message?.values?.litellmBaseUrl if (litellmApiKey && litellmBaseUrl) { modelFetchPromises.push({ key: "litellm", options: { provider: "litellm", apiKey: litellmApiKey, baseUrl: litellmBaseUrl }, }) } const results = await Promise.allSettled( modelFetchPromises.map(async ({ key, options }) => { const models = await safeGetModels(options) return { key, models } // The key is `ProviderName` here. }), ) results.forEach((result, index) => { const routerName = modelFetchPromises[index].key if (result.status === "fulfilled") { routerModels[routerName] = result.value.models // Ollama and LM Studio settings pages still need these events. if (routerName === "ollama" && Object.keys(result.value.models).length > 0) { provider.postMessageToWebview({ type: "ollamaModels", ollamaModels: result.value.models, }) } else if (routerName === "lmstudio" && Object.keys(result.value.models).length > 0) { provider.postMessageToWebview({ type: "lmStudioModels", lmStudioModels: result.value.models, }) } } else { // Handle rejection: Post a specific error message for this provider. const errorMessage = result.reason instanceof Error ? result.reason.message : String(result.reason) console.error(`Error fetching models for ${routerName}:`, result.reason) routerModels[routerName] = {} // Ensure it's an empty object in the main routerModels message. provider.postMessageToWebview({ type: "singleRouterModelFetchResponse", success: false, error: errorMessage, values: { provider: routerName }, }) } }) provider.postMessageToWebview({ type: "routerModels", routerModels }) break case "requestOllamaModels": { // Specific handler for Ollama models only. const { apiConfiguration: ollamaApiConfig } = await provider.getState() try { // Flush cache first to ensure fresh models. await flushModels("ollama") const ollamaModels = await getModels({ provider: "ollama", baseUrl: ollamaApiConfig.ollamaBaseUrl, apiKey: ollamaApiConfig.ollamaApiKey, numCtx: ollamaApiConfig.ollamaNumCtx, // kilocode_change }) if (Object.keys(ollamaModels).length > 0) { provider.postMessageToWebview({ type: "ollamaModels", ollamaModels: ollamaModels }) } } catch (error) { // Silently fail - user hasn't configured Ollama yet console.debug("Ollama models fetch failed:", error) } break } case "requestLmStudioModels": { // Specific handler for LM Studio models only. const { apiConfiguration: lmStudioApiConfig } = await provider.getState() try { // Flush cache first to ensure fresh models. await flushModels("lmstudio") const lmStudioModels = await getModels({ provider: "lmstudio", baseUrl: lmStudioApiConfig.lmStudioBaseUrl, }) if (Object.keys(lmStudioModels).length > 0) { provider.postMessageToWebview({ type: "lmStudioModels", lmStudioModels: lmStudioModels, }) } } catch (error) { // Silently fail - user hasn't configured LM Studio yet. console.debug("LM Studio models fetch failed:", error) } break } case "requestOpenAiModels": if (message?.values?.baseUrl && message?.values?.apiKey) { const openAiModels = await getOpenAiModels( message?.values?.baseUrl, message?.values?.apiKey, message?.values?.openAiHeaders, ) provider.postMessageToWebview({ type: "openAiModels", openAiModels }) } break case "requestVsCodeLmModels": const vsCodeLmModels = await getVsCodeLmModels() // TODO: Cache like we do for OpenRouter, etc? provider.postMessageToWebview({ type: "vsCodeLmModels", vsCodeLmModels }) break case "requestHuggingFaceModels": // TODO: Why isn't this handled by `requestRouterModels` above? try { const { getHuggingFaceModelsWithMetadata } = await import("../../api/providers/fetchers/huggingface") const huggingFaceModelsResponse = await getHuggingFaceModelsWithMetadata() provider.postMessageToWebview({ type: "huggingFaceModels", huggingFaceModels: huggingFaceModelsResponse.models, }) } catch (error) { console.error("Failed to fetch Hugging Face models:", error) provider.postMessageToWebview({ type: "huggingFaceModels", huggingFaceModels: [] }) } break case "openImage": openImage(message.text!, { values: message.values }) break case "saveImage": saveImage(message.dataUri!) break case "openFile": let filePath: string = message.text! if (!path.isAbsolute(filePath)) { filePath = path.join(getCurrentCwd(), filePath) } openFile(filePath, message.values as { create?: boolean; content?: string; line?: number }) break case "openMention": openMention(getCurrentCwd(), message.text) break case "openExternal": if (message.url) { vscode.env.openExternal(vscode.Uri.parse(message.url)) } break case "checkpointDiff": const result = checkoutDiffPayloadSchema.safeParse(message.payload) if (result.success) { await provider.getCurrentTask()?.checkpointDiff(result.data) } break // kilocode_change start case "seeNewChanges": const task = provider.getCurrentTask() if (task && message.payload && message.payload) { await seeNewChanges(task, (message.payload as SeeNewChangesPayload).commitRange) } break case "tasksByIdRequest": { const request = message.payload as TasksByIdRequestPayload await provider.postMessageToWebview({ type: "tasksByIdResponse", payload: { requestId: request.requestId, tasks: provider.getTaskHistory().filter((task) => request.taskIds.includes(task.id)), }, }) break } case "taskHistoryRequest": { await provider.postMessageToWebview({ type: "taskHistoryResponse", payload: getTaskHistory( provider.getTaskHistory(), provider.cwd, message.payload as TaskHistoryRequestPayload, ), }) break } // kilocode_change end case "checkpointRestore": { const result = checkoutRestorePayloadSchema.safeParse(message.payload) if (result.success) { await provider.cancelTask() try { await pWaitFor(() => provider.getCurrentTask()?.isInitialized === true, { timeout: 3_000 }) } catch (error) { vscode.window.showErrorMessage(t("common:errors.checkpoint_timeout")) } try { await provider.getCurrentTask()?.checkpointRestore(result.data) } catch (error) { vscode.window.showErrorMessage(t("common:errors.checkpoint_failed")) } } break } case "cancelTask": await provider.cancelTask() break case "allowedCommands": { // Validate and sanitize the commands array const commands = message.commands ?? [] const validCommands = Array.isArray(commands) ? commands.filter((cmd) => typeof cmd === "string" && cmd.trim().length > 0) : [] await updateGlobalState("allowedCommands", validCommands) // Also update workspace settings. await vscode.workspace .getConfiguration(Package.name) .update("allowedCommands", validCommands, vscode.ConfigurationTarget.Global) break } case "deniedCommands": { // Validate and sanitize the commands array const commands = message.commands ?? [] const validCommands = Array.isArray(commands) ? commands.filter((cmd) => typeof cmd === "string" && cmd.trim().length > 0) : [] await updateGlobalState("deniedCommands", validCommands) // Also update workspace settings. await vscode.workspace .getConfiguration(Package.name) .update("deniedCommands", validCommands, vscode.ConfigurationTarget.Global) break } case "openCustomModesSettings": { const customModesFilePath = await provider.customModesManager.getCustomModesFilePath() if (customModesFilePath) { openFile(customModesFilePath) } break } case "openKeyboardShortcuts": { // Open VSCode keyboard shortcuts settings and optionally filter to show the Roo Code commands const searchQuery = message.text || "" if (searchQuery) { // Open with a search query pre-filled await vscode.commands.executeCommand("workbench.action.openGlobalKeybindings", searchQuery) } else { // Just open the keyboard shortcuts settings await vscode.commands.executeCommand("workbench.action.openGlobalKeybindings") } break } case "openMcpSettings": { const mcpSettingsFilePath = await provider.getMcpHub()?.getMcpSettingsFilePath() if (mcpSettingsFilePath) { openFile(mcpSettingsFilePath) } break } case "openProjectMcpSettings": { if (!vscode.workspace.workspaceFolders?.length) { vscode.window.showErrorMessage(t("common:errors.no_workspace")) return } const workspaceFolder = getCurrentCwd() const rooDir = path.join(workspaceFolder, ".kilocode") const mcpPath = path.join(rooDir, "mcp.json") try { await fs.mkdir(rooDir, { recursive: true }) const exists = await fileExistsAtPath(mcpPath) if (!exists) { await safeWriteJson(mcpPath, { mcpServers: {} }) } openFile(mcpPath) } catch (error) { vscode.window.showErrorMessage(t("mcp:errors.create_json", { error: `${error}` })) } break } case "deleteMcpServer": { if (!message.serverName) { break } try { provider.log(`Attempting to delete MCP server: ${message.serverName}`) await provider.getMcpHub()?.deleteServer(message.serverName, message.source as "global" | "project") provider.log(`Successfully deleted MCP server: ${message.serverName}`) // Refresh the webview state await provider.postStateToWebview() } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) provider.log(`Failed to delete MCP server: ${errorMessage}`) // Error messages are already handled by McpHub.deleteServer } break } case "restartMcpServer": { try { await provider.getMcpHub()?.restartConnection(message.text!, message.source as "global" | "project") } catch (error) { provider.log( `Failed to retry connection for ${message.text}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, ) } break } case "toggleToolAlwaysAllow": { try { await provider .getMcpHub() ?.toggleToolAlwaysAllow( message.serverName!, message.source as "global" | "project", message.toolName!, Boolean(message.alwaysAllow), ) } catch (error) { provider.log( `Failed to toggle auto-approve for tool ${message.toolName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, ) } break } case "toggleToolEnabledForPrompt": { try { await provider .getMcpHub() ?.toggleToolEnabledForPrompt( message.serverName!, message.source as "global" | "project", message.toolName!, Boolean(message.isEnabled), ) } catch (error) { provider.log( `Failed to toggle enabled for prompt for tool ${message.toolName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, ) } break } case "toggleMcpServer": { try { await provider .getMcpHub() ?.toggleServerDisabled( message.serverName!, message.disabled!, message.source as "global" | "project", ) } catch (error) { provider.log( `Failed to toggle MCP server ${message.serverName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, ) } break } case "mcpEnabled": const mcpEnabled = message.bool ?? true await updateGlobalState("mcpEnabled", mcpEnabled) const mcpHubInstance = provider.getMcpHub() if (mcpHubInstance) { await mcpHubInstance.handleMcpEnabledChange(mcpEnabled) } await provider.postStateToWebview() break case "enableMcpServerCreation": await updateGlobalState("enableMcpServerCreation", message.bool ?? true) await provider.postStateToWebview() break // kilocode_change begin case "openGlobalKeybindings": vscode.commands.executeCommand("workbench.action.openGlobalKeybindings", message.text ?? "kilo-code.") break case "showSystemNotification": const isSystemNotificationsEnabled = getGlobalState("systemNotificationsEnabled") ?? true if (!isSystemNotificationsEnabled && !message.alwaysAllow) { break } if (message.notificationOptions) { showSystemNotification(message.notificationOptions) } break case "systemNotificationsEnabled": const systemNotificationsEnabled = message.bool ?? true await updateGlobalState("systemNotificationsEnabled", systemNotificationsEnabled) await provider.postStateToWebview() break case "openInBrowser": if (message.url) { vscode.env.openExternal(vscode.Uri.parse(message.url)) } break // kilocode_change end case "remoteControlEnabled": try { await CloudService.instance.updateUserSettings({ extensionBridgeEnabled: message.bool ?? false }) } catch (error) { provider.log( `CloudService#updateUserSettings failed: ${error instanceof Error ? error.message : String(error)}`, ) } break case "taskSyncEnabled": const enabled = message.bool ?? false const updatedSettings: Partial = { taskSyncEnabled: enabled, } // If disabling task sync, also disable remote control if (!enabled) { updatedSettings.extensionBridgeEnabled = false } try { await CloudService.instance.updateUserSettings(updatedSettings) } catch (error) { provider.log(`Failed to update cloud settings for task sync: ${error}`) } break case "refreshAllMcpServers": { const mcpHub = provider.getMcpHub() if (mcpHub) { await mcpHub.refreshAllConnections() } break } case "soundEnabled": const soundEnabled = message.bool ?? true await updateGlobalState("soundEnabled", soundEnabled) await provider.postStateToWebview() break case "soundVolume": const soundVolume = message.value ?? 0.5 await updateGlobalState("soundVolume", soundVolume) await provider.postStateToWebview() break case "ttsEnabled": const ttsEnabled = message.bool ?? true await updateGlobalState("ttsEnabled", ttsEnabled) setTtsEnabled(ttsEnabled) await provider.postStateToWebview() break case "ttsSpeed": const ttsSpeed = message.value ?? 1.0 await updateGlobalState("ttsSpeed", ttsSpeed) setTtsSpeed(ttsSpeed) await provider.postStateToWebview() break case "playTts": if (message.text) { playTts(message.text, { onStart: () => provider.postMessageToWebview({ type: "ttsStart", text: message.text }), onStop: () => provider.postMessageToWebview({ type: "ttsStop", text: message.text }), }) } break case "stopTts": stopTts() break case "diffEnabled": const diffEnabled = message.bool ?? true await updateGlobalState("diffEnabled", diffEnabled) await provider.postStateToWebview() break case "enableCheckpoints": const enableCheckpoints = message.bool ?? true await updateGlobalState("enableCheckpoints", enableCheckpoints) await provider.postStateToWebview() break case "browserViewportSize": const browserViewportSize = message.text ?? "900x600" await updateGlobalState("browserViewportSize", browserViewportSize) await provider.postStateToWebview() break case "remoteBrowserHost": await updateGlobalState("remoteBrowserHost", message.text) await provider.postStateToWebview() break case "remoteBrowserEnabled": // Store the preference in global state // remoteBrowserEnabled now means "enable remote browser connection" await updateGlobalState("remoteBrowserEnabled", message.bool ?? false) // If disabling remote browser connection, clear the remoteBrowserHost if (!message.bool) { await updateGlobalState("remoteBrowserHost", undefined) } await provider.postStateToWebview() break case "testBrowserConnection": // If no text is provided, try auto-discovery if (!message.text) { // Use testBrowserConnection for auto-discovery const chromeHostUrl = await discoverChromeHostUrl() if (chromeHostUrl) { // Send the result back to the webview await provider.postMessageToWebview({ type: "browserConnectionResult", success: !!chromeHostUrl, text: `Auto-discovered and tested connection to Chrome: ${chromeHostUrl}`, values: { endpoint: chromeHostUrl }, }) } else { await provider.postMessageToWebview({ type: "browserConnectionResult", success: false, text: "No Chrome instances found on the network. Make sure Chrome is running with remote debugging enabled (--remote-debugging-port=9222).", }) } } else { // Test the provided URL const customHostUrl = message.text const hostIsValid = await tryChromeHostUrl(message.text) // Send the result back to the webview await provider.postMessageToWebview({ type: "browserConnectionResult", success: hostIsValid, text: hostIsValid ? `Successfully connected to Chrome: ${customHostUrl}` : "Failed to connect to Chrome", }) } break case "fuzzyMatchThreshold": await updateGlobalState("fuzzyMatchThreshold", message.value) await provider.postStateToWebview() break // kilocode_change start case "morphApiKey": await updateGlobalState("morphApiKey", message.text) await provider.postStateToWebview() break case "fastApplyModel": { const nextModel = fastApplyModelSchema.safeParse(message.text).data ?? "auto" await updateGlobalState("fastApplyModel", nextModel) await provider.postStateToWebview() break } // kilocode_change end case "updateVSCodeSetting": { const { setting, value } = message if (setting !== undefined && value !== undefined) { if (ALLOWED_VSCODE_SETTINGS.has(setting)) { await vscode.workspace.getConfiguration().update(setting, value, true) } else { vscode.window.showErrorMessage(`Cannot update restricted VSCode setting: ${setting}`) } } break } case "getVSCodeSetting": const { setting } = message if (setting) { try { await provider.postMessageToWebview({ type: "vsCodeSetting", setting, value: vscode.workspace.getConfiguration().get(setting), }) } catch (error) { console.error(`Failed to get VSCode setting ${message.setting}:`, error) await provider.postMessageToWebview({ type: "vsCodeSetting", setting, error: `Failed to get setting: ${error.message}`, value: undefined, }) } } break case "alwaysApproveResubmit": await updateGlobalState("alwaysApproveResubmit", message.bool ?? false) await provider.postStateToWebview() break case "requestDelaySeconds": await updateGlobalState("requestDelaySeconds", message.value ?? 5) await provider.postStateToWebview() break case "writeDelayMs": await updateGlobalState("writeDelayMs", message.value) await provider.postStateToWebview() break case "diagnosticsEnabled": await updateGlobalState("diagnosticsEnabled", message.bool ?? true) await provider.postStateToWebview() break case "terminalOutputLineLimit": // Validate that the line limit is a positive number const lineLimit = message.value if (typeof lineLimit === "number" && lineLimit > 0) { await updateGlobalState("terminalOutputLineLimit", lineLimit) await provider.postStateToWebview() } else { vscode.window.showErrorMessage( t("common:errors.invalid_line_limit") || "Terminal output line limit must be a positive number", ) } break case "terminalOutputCharacterLimit": // Validate that the character limit is a positive number const charLimit = message.value if (typeof charLimit === "number" && charLimit > 0) { await updateGlobalState("terminalOutputCharacterLimit", charLimit) await provider.postStateToWebview() } else { vscode.window.showErrorMessage( t("common:errors.invalid_character_limit") || "Terminal output character limit must be a positive number", ) } break case "terminalShellIntegrationTimeout": await updateGlobalState("terminalShellIntegrationTimeout", message.value) await provider.postStateToWebview() if (message.value !== undefined) { Terminal.setShellIntegrationTimeout(message.value) } break case "terminalShellIntegrationDisabled": await updateGlobalState("terminalShellIntegrationDisabled", message.bool) await provider.postStateToWebview() if (message.bool !== undefined) { Terminal.setShellIntegrationDisabled(message.bool) } break case "terminalCommandDelay": await updateGlobalState("terminalCommandDelay", message.value) await provider.postStateToWebview() if (message.value !== undefined) { Terminal.setCommandDelay(message.value) } break case "terminalPowershellCounter": await updateGlobalState("terminalPowershellCounter", message.bool) await provider.postStateToWebview() if (message.bool !== undefined) { Terminal.setPowershellCounter(message.bool) } break case "terminalZshClearEolMark": await updateGlobalState("terminalZshClearEolMark", message.bool) await provider.postStateToWebview() if (message.bool !== undefined) { Terminal.setTerminalZshClearEolMark(message.bool) } break case "terminalZshOhMy": await updateGlobalState("terminalZshOhMy", message.bool) await provider.postStateToWebview() if (message.bool !== undefined) { Terminal.setTerminalZshOhMy(message.bool) } break case "terminalZshP10k": await updateGlobalState("terminalZshP10k", message.bool) await provider.postStateToWebview() if (message.bool !== undefined) { Terminal.setTerminalZshP10k(message.bool) } break case "terminalZdotdir": await updateGlobalState("terminalZdotdir", message.bool) await provider.postStateToWebview() if (message.bool !== undefined) { Terminal.setTerminalZdotdir(message.bool) } break case "terminalCompressProgressBar": await updateGlobalState("terminalCompressProgressBar", message.bool) await provider.postStateToWebview() if (message.bool !== undefined) { Terminal.setCompressProgressBar(message.bool) } break case "mode": await provider.handleModeSwitch(message.text as Mode) break case "updateSupportPrompt": try { if (!message?.values) { return } // Replace all prompts with the new values from the cached state await updateGlobalState("customSupportPrompts", message.values) await provider.postStateToWebview() } catch (error) { provider.log( `Error update support prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, ) vscode.window.showErrorMessage(t("common:errors.update_support_prompt")) } break case "updatePrompt": if (message.promptMode && message.customPrompt !== undefined) { const existingPrompts = getGlobalState("customModePrompts") ?? {} const updatedPrompts = { ...existingPrompts, [message.promptMode]: message.customPrompt } await updateGlobalState("customModePrompts", updatedPrompts) const currentState = await provider.getStateToPostToWebview() const stateWithPrompts = { ...currentState, customModePrompts: updatedPrompts, hasOpenedModeSelector: currentState.hasOpenedModeSelector ?? false, } provider.postMessageToWebview({ type: "state", state: stateWithPrompts }) if (TelemetryService.hasInstance()) { // Determine which setting was changed by comparing objects const oldPrompt = existingPrompts[message.promptMode] || {} const newPrompt = message.customPrompt const changedSettings = Object.keys(newPrompt).filter( (key) => JSON.stringify((oldPrompt as Record)[key]) !== JSON.stringify((newPrompt as Record)[key]), ) if (changedSettings.length > 0) { TelemetryService.instance.captureModeSettingChanged(changedSettings[0]) } } } break case "deleteMessage": { if (!provider.getCurrentTask()) { await vscode.window.showErrorMessage(t("common:errors.message.no_active_task_to_delete")) break } if (typeof message.value !== "number" || !message.value) { await vscode.window.showErrorMessage(t("common:errors.message.invalid_timestamp_for_deletion")) break } await handleMessageModificationsOperation(message.value, "delete") break } case "submitEditedMessage": { if ( provider.getCurrentTask() && typeof message.value === "number" && message.value && message.editedMessageContent ) { await handleMessageModificationsOperation( message.value, "edit", message.editedMessageContent, message.images, ) } break } case "screenshotQuality": await updateGlobalState("screenshotQuality", message.value) await provider.postStateToWebview() break case "maxOpenTabsContext": const tabCount = Math.min(Math.max(0, message.value ?? 20), 500) await updateGlobalState("maxOpenTabsContext", tabCount) await provider.postStateToWebview() break case "maxWorkspaceFiles": const fileCount = Math.min(Math.max(0, message.value ?? 200), 500) await updateGlobalState("maxWorkspaceFiles", fileCount) await provider.postStateToWebview() break case "alwaysAllowFollowupQuestions": await updateGlobalState("alwaysAllowFollowupQuestions", message.bool ?? false) await provider.postStateToWebview() break case "followupAutoApproveTimeoutMs": await updateGlobalState("followupAutoApproveTimeoutMs", message.value) await provider.postStateToWebview() break case "browserToolEnabled": await updateGlobalState("browserToolEnabled", message.bool ?? true) await provider.postStateToWebview() break case "language": changeLanguage(message.text ?? "en") await updateGlobalState("language", message.text as Language) await provider.postStateToWebview() break case "openRouterImageApiKey": await provider.contextProxy.setValue("openRouterImageApiKey", message.text) await provider.postStateToWebview() break case "openRouterImageGenerationSelectedModel": await provider.contextProxy.setValue("openRouterImageGenerationSelectedModel", message.text) await provider.postStateToWebview() break case "showRooIgnoredFiles": await updateGlobalState("showRooIgnoredFiles", message.bool ?? false) await provider.postStateToWebview() break case "hasOpenedModeSelector": await updateGlobalState("hasOpenedModeSelector", message.bool ?? true) await provider.postStateToWebview() break case "maxReadFileLine": await updateGlobalState("maxReadFileLine", message.value) await provider.postStateToWebview() break // kilocode_change start case "kiloCodeImageApiKey": await provider.contextProxy.setValue("kiloCodeImageApiKey", message.text) await provider.postStateToWebview() break case "showAutoApproveMenu": await updateGlobalState("showAutoApproveMenu", message.bool ?? true) await provider.postStateToWebview() break case "showTaskTimeline": await updateGlobalState("showTaskTimeline", message.bool ?? false) await provider.postStateToWebview() break // kilocode_change start case "sendMessageOnEnter": await updateGlobalState("sendMessageOnEnter", message.bool ?? false) await provider.postStateToWebview() break // kilocode_change end case "showTimestamps": await updateGlobalState("showTimestamps", message.bool ?? false) await provider.postStateToWebview() break case "hideCostBelowThreshold": await updateGlobalState("hideCostBelowThreshold", message.value) await provider.postStateToWebview() break case "allowVeryLargeReads": await updateGlobalState("allowVeryLargeReads", message.bool ?? false) await provider.postStateToWebview() break // kilocode_change end case "maxImageFileSize": await updateGlobalState("maxImageFileSize", message.value) await provider.postStateToWebview() break case "maxTotalImageSize": await updateGlobalState("maxTotalImageSize", message.value) await provider.postStateToWebview() break case "maxConcurrentFileReads": const valueToSave = message.value // Capture the value intended for saving await updateGlobalState("maxConcurrentFileReads", valueToSave) await provider.postStateToWebview() break case "includeDiagnosticMessages": // Only apply default if the value is truly undefined (not false) const includeValue = message.bool !== undefined ? message.bool : true await updateGlobalState("includeDiagnosticMessages", includeValue) await provider.postStateToWebview() break case "maxDiagnosticMessages": await updateGlobalState("maxDiagnosticMessages", message.value ?? 50) await provider.postStateToWebview() break case "setHistoryPreviewCollapsed": // Add the new case handler await updateGlobalState("historyPreviewCollapsed", message.bool ?? false) // No need to call postStateToWebview here as the UI already updated optimistically break case "setReasoningBlockCollapsed": await updateGlobalState("reasoningBlockCollapsed", message.bool ?? true) // No need to call postStateToWebview here as the UI already updated optimistically break case "toggleApiConfigPin": if (message.text) { const currentPinned = getGlobalState("pinnedApiConfigs") ?? {} const updatedPinned: Record = { ...currentPinned } if (currentPinned[message.text]) { delete updatedPinned[message.text] } else { updatedPinned[message.text] = true } await updateGlobalState("pinnedApiConfigs", updatedPinned) await provider.postStateToWebview() } break case "enhancementApiConfigId": await updateGlobalState("enhancementApiConfigId", message.text) await provider.postStateToWebview() break // kilocode_change start - commitMessageApiConfigId case "commitMessageApiConfigId": await updateGlobalState("commitMessageApiConfigId", message.text) await provider.postStateToWebview() break // kilocode_change end - commitMessageApiConfigId // kilocode_change start - terminalCommandApiConfigId case "terminalCommandApiConfigId": await updateGlobalState("terminalCommandApiConfigId", message.text) await provider.postStateToWebview() break // kilocode_change end - terminalCommandApiConfigId // kilocode_change start - ghostServiceSettings case "ghostServiceSettings": if (!message.values) { return } // Validate ghostServiceSettings structure const ghostServiceSettings = ghostServiceSettingsSchema.parse(message.values) await updateGlobalState("ghostServiceSettings", ghostServiceSettings) await provider.postStateToWebview() vscode.commands.executeCommand("kilo-code.ghost.reload") break // kilocode_change end case "includeTaskHistoryInEnhance": await updateGlobalState("includeTaskHistoryInEnhance", message.bool ?? true) await provider.postStateToWebview() break case "condensingApiConfigId": await updateGlobalState("condensingApiConfigId", message.text) await provider.postStateToWebview() break case "updateCondensingPrompt": // Store the condensing prompt in customSupportPrompts["CONDENSE"] instead of customCondensingPrompt const currentSupportPrompts = getGlobalState("customSupportPrompts") ?? {} const updatedSupportPrompts = { ...currentSupportPrompts, CONDENSE: message.text } await updateGlobalState("customSupportPrompts", updatedSupportPrompts) // Also update the old field for backward compatibility during migration await updateGlobalState("customCondensingPrompt", message.text) await provider.postStateToWebview() break case "profileThresholds": await updateGlobalState("profileThresholds", message.values) await provider.postStateToWebview() break case "autoApprovalEnabled": await updateGlobalState("autoApprovalEnabled", message.bool ?? false) await provider.postStateToWebview() break case "enhancePrompt": if (message.text) { try { const state = await provider.getState() const { apiConfiguration, customSupportPrompts, listApiConfigMeta = [], enhancementApiConfigId, includeTaskHistoryInEnhance, } = state const currentCline = provider.getCurrentTask() const result = await MessageEnhancer.enhanceMessage({ text: message.text, apiConfiguration, customSupportPrompts, listApiConfigMeta, enhancementApiConfigId, includeTaskHistoryInEnhance, currentClineMessages: currentCline?.clineMessages, providerSettingsManager: provider.providerSettingsManager, }) if (result.success && result.enhancedText) { // Capture telemetry for prompt enhancement MessageEnhancer.captureTelemetry(currentCline?.taskId, includeTaskHistoryInEnhance) await provider.postMessageToWebview({ type: "enhancedPrompt", text: result.enhancedText }) } else { throw new Error(result.error || "Unknown error") } } catch (error) { provider.log( `Error enhancing prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, ) TelemetryService.instance.captureException(error, { context: "enhance_prompt" }) // kilocode_change vscode.window.showErrorMessage(t("common:errors.enhance_prompt")) await provider.postMessageToWebview({ type: "enhancedPrompt" }) } } break case "getSystemPrompt": try { const systemPrompt = await generateSystemPrompt(provider, message) await provider.postMessageToWebview({ type: "systemPrompt", text: systemPrompt, mode: message.mode, }) } catch (error) { provider.log( `Error getting system prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, ) vscode.window.showErrorMessage(t("common:errors.get_system_prompt")) } break case "copySystemPrompt": try { const systemPrompt = await generateSystemPrompt(provider, message) await vscode.env.clipboard.writeText(systemPrompt) await vscode.window.showInformationMessage(t("common:info.clipboard_copy")) } catch (error) { provider.log( `Error getting system prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, ) vscode.window.showErrorMessage(t("common:errors.get_system_prompt")) } break case "searchCommits": { const cwd = getCurrentCwd() if (cwd) { try { const commits = await searchCommits(message.query || "", cwd) await provider.postMessageToWebview({ type: "commitSearchResults", commits, }) } catch (error) { provider.log( `Error searching commits: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, ) vscode.window.showErrorMessage(t("common:errors.search_commits")) } } break } // kilocode_change start case "showFeedbackOptions": { const githubIssuesText = t("common:feedback.githubIssues") const discordText = t("common:feedback.discord") const customerSupport = t("common:feedback.customerSupport") const answer = await vscode.window.showInformationMessage( t("common:feedback.description"), { modal: true }, githubIssuesText, discordText, customerSupport, ) if (answer === githubIssuesText) { await vscode.env.openExternal(vscode.Uri.parse("https://github.com/Kilo-Org/kilocode/issues")) } else if (answer === discordText) { await vscode.env.openExternal(vscode.Uri.parse("https://discord.gg/fxrhCFGhkP")) } else if (answer === customerSupport) { await vscode.env.openExternal(vscode.Uri.parse("https://kilocode.ai/support")) } break } // kilocode_change end case "searchFiles": { const workspacePath = getCurrentCwd() if (!workspacePath) { // Handle case where workspace path is not available await provider.postMessageToWebview({ type: "fileSearchResults", results: [], requestId: message.requestId, error: "No workspace path available", }) break } try { // Call file search service with query from message const results = await searchWorkspaceFiles( message.query || "", workspacePath, 20, // Use default limit, as filtering is now done in the backend ) // Send results back to webview await provider.postMessageToWebview({ type: "fileSearchResults", results, requestId: message.requestId, }) } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) // Send error response to webview await provider.postMessageToWebview({ type: "fileSearchResults", results: [], error: errorMessage, requestId: message.requestId, }) } break } case "updateTodoList": { const payload = message.payload as { todos?: any[] } const todos = payload?.todos if (Array.isArray(todos)) { await setPendingTodoList(todos) } break } case "saveApiConfiguration": if (message.text && message.apiConfiguration) { try { await provider.providerSettingsManager.saveConfig(message.text, message.apiConfiguration) const listApiConfig = await provider.providerSettingsManager.listConfig() await updateGlobalState("listApiConfigMeta", listApiConfig) vscode.commands.executeCommand("kilo-code.ghost.reload") // kilocode_change: Reload ghost model when API provider settings change } catch (error) { provider.log( `Error save api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, ) vscode.window.showErrorMessage(t("common:errors.save_api_config")) } } break case "upsertApiConfiguration": // kilocode_change start: check for kilocodeToken change to remove organizationId and fetch organization modes if (message.text && message.apiConfiguration) { let configToSave = message.apiConfiguration let organizationChanged = false try { const { ...currentConfig } = await provider.providerSettingsManager.getProfile({ name: message.text, }) // Only clear organization ID if we actually had a kilocode token before and it's different now const hadPreviousToken = currentConfig.kilocodeToken !== undefined const hasNewToken = message.apiConfiguration.kilocodeToken !== undefined const tokensAreDifferent = currentConfig.kilocodeToken !== message.apiConfiguration.kilocodeToken if (hadPreviousToken && hasNewToken && tokensAreDifferent) { configToSave = { ...message.apiConfiguration, kilocodeOrganizationId: undefined } await updateGlobalState("hasPerformedOrganizationAutoSwitch", undefined) } organizationChanged = currentConfig.kilocodeOrganizationId !== message.apiConfiguration.kilocodeOrganizationId if (organizationChanged) { // Fetch organization-specific custom modes await refreshOrganizationModes(message, provider, updateGlobalState) // Flush and refetch models await flushModels("kilocode-openrouter") const models = await getModels({ provider: "kilocode-openrouter", kilocodeOrganizationId: message.apiConfiguration.kilocodeOrganizationId, kilocodeToken: message.apiConfiguration.kilocodeToken, }) provider.postMessageToWebview({ type: "routerModels", routerModels: { "kilocode-openrouter": models } as Record, }) } } catch (error) { // Config might not exist yet, that's fine } await provider.upsertProviderProfile(message.text, configToSave) // Ensure state is posted to webview after profile update to reflect organization mode changes if (organizationChanged) { await provider.postStateToWebview() } } // kilocode_change end: check for kilocodeToken change to remove organizationId and fetch organization modes break case "renameApiConfiguration": if (message.values && message.apiConfiguration) { try { const { oldName, newName } = message.values if (oldName === newName) { break } // Load the old configuration to get its ID. const { id } = await provider.providerSettingsManager.getProfile({ name: oldName }) // Create a new configuration with the new name and old ID. await provider.providerSettingsManager.saveConfig(newName, { ...message.apiConfiguration, id }) // Delete the old configuration. await provider.providerSettingsManager.deleteConfig(oldName) // Re-activate to update the global settings related to the // currently activated provider profile. await provider.activateProviderProfile({ name: newName }) } catch (error) { provider.log( `Error rename api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, ) vscode.window.showErrorMessage(t("common:errors.rename_api_config")) } } break case "loadApiConfiguration": if (message.text) { try { await provider.activateProviderProfile({ name: message.text }) } catch (error) { provider.log( `Error load api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, ) vscode.window.showErrorMessage(t("common:errors.load_api_config")) } } break case "loadApiConfigurationById": if (message.text) { try { await provider.activateProviderProfile({ id: message.text }) } catch (error) { provider.log( `Error load api configuration by ID: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, ) vscode.window.showErrorMessage(t("common:errors.load_api_config")) } } break case "deleteApiConfiguration": if (message.text) { const answer = await vscode.window.showInformationMessage( t("common:confirmation.delete_config_profile"), { modal: true }, t("common:answers.yes"), ) if (answer !== t("common:answers.yes")) { break } const oldName = message.text const newName = (await provider.providerSettingsManager.listConfig()).filter( (c) => c.name !== oldName, )[0]?.name if (!newName) { vscode.window.showErrorMessage(t("common:errors.delete_api_config")) return } try { await provider.providerSettingsManager.deleteConfig(oldName) await provider.activateProviderProfile({ name: newName }) } catch (error) { provider.log( `Error delete api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, ) vscode.window.showErrorMessage(t("common:errors.delete_api_config")) } } break case "deleteMessageConfirm": if (!message.messageTs) { await vscode.window.showErrorMessage(t("common:errors.message.cannot_delete_missing_timestamp")) break } if (typeof message.messageTs !== "number") { await vscode.window.showErrorMessage(t("common:errors.message.cannot_delete_invalid_timestamp")) break } await handleDeleteMessageConfirm(message.messageTs, message.restoreCheckpoint) break case "editMessageConfirm": if (message.messageTs && message.text) { await handleEditMessageConfirm( message.messageTs, message.text, message.restoreCheckpoint, message.images, ) } break case "getListApiConfiguration": try { const listApiConfig = await provider.providerSettingsManager.listConfig() await updateGlobalState("listApiConfigMeta", listApiConfig) provider.postMessageToWebview({ type: "listApiConfig", listApiConfig }) } catch (error) { provider.log( `Error get list api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, ) vscode.window.showErrorMessage(t("common:errors.list_api_config")) } break case "updateExperimental": { if (!message.values) { break } const updatedExperiments = { ...(getGlobalState("experiments") ?? experimentDefault), ...message.values, } await updateGlobalState("experiments", updatedExperiments) await provider.postStateToWebview() break } case "updateMcpTimeout": if (message.serverName && typeof message.timeout === "number") { try { await provider .getMcpHub() ?.updateServerTimeout( message.serverName, message.timeout, message.source as "global" | "project", ) } catch (error) { provider.log( `Failed to update timeout for ${message.serverName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, ) vscode.window.showErrorMessage(t("common:errors.update_server_timeout")) } } break case "updateCustomMode": if (message.modeConfig) { // Check if this is a new mode or an update to an existing mode const existingModes = await provider.customModesManager.getCustomModes() const isNewMode = !existingModes.some((mode) => mode.slug === message.modeConfig?.slug) await provider.customModesManager.updateCustomMode(message.modeConfig.slug, message.modeConfig) // Update state after saving the mode const customModes = await provider.customModesManager.getCustomModes() await updateGlobalState("customModes", customModes) await updateGlobalState("mode", message.modeConfig.slug) await provider.postStateToWebview() // Track telemetry for custom mode creation or update if (TelemetryService.hasInstance()) { if (isNewMode) { // This is a new custom mode TelemetryService.instance.captureCustomModeCreated( message.modeConfig.slug, message.modeConfig.name, ) } else { // Determine which setting was changed by comparing objects const existingMode = existingModes.find((mode) => mode.slug === message.modeConfig?.slug) const changedSettings = existingMode ? Object.keys(message.modeConfig).filter( (key) => JSON.stringify((existingMode as Record)[key]) !== JSON.stringify((message.modeConfig as Record)[key]), ) : [] if (changedSettings.length > 0) { TelemetryService.instance.captureModeSettingChanged(changedSettings[0]) } } } } break case "deleteCustomMode": if (message.slug) { // Get the mode details to determine source and rules folder path const customModes = await provider.customModesManager.getCustomModes() const modeToDelete = customModes.find((mode) => mode.slug === message.slug) if (!modeToDelete) { break } // Determine the scope based on source (project or global) const scope = modeToDelete.source || "global" // Determine the rules folder path let rulesFolderPath: string if (scope === "project") { const workspacePath = getWorkspacePath() if (workspacePath) { rulesFolderPath = path.join(workspacePath, ".roo", `rules-${message.slug}`) } else { rulesFolderPath = path.join(".roo", `rules-${message.slug}`) } } else { // Global scope - use OS home directory const homeDir = os.homedir() rulesFolderPath = path.join(homeDir, ".roo", `rules-${message.slug}`) } // Check if the rules folder exists const rulesFolderExists = await fileExistsAtPath(rulesFolderPath) // If this is a check request, send back the folder info if (message.checkOnly) { await provider.postMessageToWebview({ type: "deleteCustomModeCheck", slug: message.slug, rulesFolderPath: rulesFolderExists ? rulesFolderPath : undefined, }) break } // Delete the mode await provider.customModesManager.deleteCustomMode(message.slug) // Delete the rules folder if it exists if (rulesFolderExists) { try { await fs.rm(rulesFolderPath, { recursive: true, force: true }) provider.log(`Deleted rules folder for mode ${message.slug}: ${rulesFolderPath}`) } catch (error) { provider.log(`Failed to delete rules folder for mode ${message.slug}: ${error}`) // Notify the user about the failure vscode.window.showErrorMessage( t("common:errors.delete_rules_folder_failed", { rulesFolderPath, error: error instanceof Error ? error.message : String(error), }), ) // Continue with mode deletion even if folder deletion fails } } // Switch back to default mode after deletion await updateGlobalState("mode", defaultModeSlug) await provider.postStateToWebview() } break case "exportMode": if (message.slug) { try { // Get custom mode prompts to check if built-in mode has been customized const customModePrompts = getGlobalState("customModePrompts") || {} const customPrompt = customModePrompts[message.slug] // Export the mode with any customizations merged directly const result = await provider.customModesManager.exportModeWithRules(message.slug, customPrompt) if (result.success && result.yaml) { // Get last used directory for export const lastExportPath = getGlobalState("lastModeExportPath") let defaultUri: vscode.Uri if (lastExportPath) { // Use the directory from the last export const lastDir = path.dirname(lastExportPath) defaultUri = vscode.Uri.file(path.join(lastDir, `${message.slug}-export.yaml`)) } else { // Default to workspace or home directory const workspaceFolders = vscode.workspace.workspaceFolders if (workspaceFolders && workspaceFolders.length > 0) { defaultUri = vscode.Uri.file( path.join(workspaceFolders[0].uri.fsPath, `${message.slug}-export.yaml`), ) } else { defaultUri = vscode.Uri.file(`${message.slug}-export.yaml`) } } // Show save dialog const saveUri = await vscode.window.showSaveDialog({ defaultUri, filters: { "YAML files": ["yaml", "yml"], }, title: "Save mode export", }) if (saveUri && result.yaml) { // Save the directory for next time await updateGlobalState("lastModeExportPath", saveUri.fsPath) // Write the file to the selected location await fs.writeFile(saveUri.fsPath, result.yaml, "utf-8") // Send success message to webview provider.postMessageToWebview({ type: "exportModeResult", success: true, slug: message.slug, }) // Show info message vscode.window.showInformationMessage(t("common:info.mode_exported", { mode: message.slug })) } else { // User cancelled the save dialog provider.postMessageToWebview({ type: "exportModeResult", success: false, error: "Export cancelled", slug: message.slug, }) } } else { // Send error message to webview provider.postMessageToWebview({ type: "exportModeResult", success: false, error: result.error, slug: message.slug, }) } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) provider.log(`Failed to export mode ${message.slug}: ${errorMessage}`) // Send error message to webview provider.postMessageToWebview({ type: "exportModeResult", success: false, error: errorMessage, slug: message.slug, }) } } break case "importMode": try { // Get last used directory for import const lastImportPath = getGlobalState("lastModeImportPath") let defaultUri: vscode.Uri | undefined if (lastImportPath) { // Use the directory from the last import const lastDir = path.dirname(lastImportPath) defaultUri = vscode.Uri.file(lastDir) } else { // Default to workspace or home directory const workspaceFolders = vscode.workspace.workspaceFolders if (workspaceFolders && workspaceFolders.length > 0) { defaultUri = vscode.Uri.file(workspaceFolders[0].uri.fsPath) } } // Show file picker to select YAML file const fileUri = await vscode.window.showOpenDialog({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false, defaultUri, filters: { "YAML files": ["yaml", "yml"], }, title: "Select mode export file to import", }) if (fileUri && fileUri[0]) { // Save the directory for next time await updateGlobalState("lastModeImportPath", fileUri[0].fsPath) // Read the file content const yamlContent = await fs.readFile(fileUri[0].fsPath, "utf-8") // Import the mode with the specified source level const result = await provider.customModesManager.importModeWithRules( yamlContent, message.source || "project", // Default to project if not specified ) if (result.success) { // Update state after importing const customModes = await provider.customModesManager.getCustomModes() await updateGlobalState("customModes", customModes) await provider.postStateToWebview() // Send success message to webview provider.postMessageToWebview({ type: "importModeResult", success: true, }) // Show success message vscode.window.showInformationMessage(t("common:info.mode_imported")) } else { // Send error message to webview provider.postMessageToWebview({ type: "importModeResult", success: false, error: result.error, }) // Show error message vscode.window.showErrorMessage(t("common:errors.mode_import_failed", { error: result.error })) } } else { // User cancelled the file dialog - reset the importing state provider.postMessageToWebview({ type: "importModeResult", success: false, error: "cancelled", }) } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) provider.log(`Failed to import mode: ${errorMessage}`) // Send error message to webview provider.postMessageToWebview({ type: "importModeResult", success: false, error: errorMessage, }) // Show error message vscode.window.showErrorMessage(t("common:errors.mode_import_failed", { error: errorMessage })) } break case "checkRulesDirectory": if (message.slug) { const hasContent = await provider.customModesManager.checkRulesDirectoryHasContent(message.slug) provider.postMessageToWebview({ type: "checkRulesDirectoryResult", slug: message.slug, hasContent: hasContent, }) } break case "humanRelayResponse": if (message.requestId && message.text) { vscode.commands.executeCommand(getCommand("handleHumanRelayResponse"), { requestId: message.requestId, text: message.text, cancelled: false, }) } break case "humanRelayCancel": if (message.requestId) { vscode.commands.executeCommand(getCommand("handleHumanRelayResponse"), { requestId: message.requestId, cancelled: true, }) } break // kilocode_change_start case "fetchProfileDataRequest": try { const { apiConfiguration, currentApiConfigName } = await provider.getState() const kilocodeToken = apiConfiguration?.kilocodeToken if (!kilocodeToken) { provider.log("KiloCode token not found in extension state.") provider.postMessageToWebview({ type: "profileDataResponse", payload: { success: false, error: "KiloCode API token not configured." }, }) break } // Changed to /api/profile const headers: Record = { Authorization: `Bearer ${kilocodeToken}`, "Content-Type": "application/json", } // Add X-KILOCODE-TESTER: SUPPRESS header if the setting is enabled if ( apiConfiguration.kilocodeTesterWarningsDisabledUntil && apiConfiguration.kilocodeTesterWarningsDisabledUntil > Date.now() ) { headers["X-KILOCODE-TESTER"] = "SUPPRESS" } const response = await axios.get>( `${getKiloBaseUriFromToken(kilocodeToken)}/api/profile`, { headers, }, ) // Go back to Personal when no longer part of the current set organization const organizationExists = (response.data.organizations ?? []).some( ({ id }) => id === apiConfiguration?.kilocodeOrganizationId, ) if (apiConfiguration?.kilocodeOrganizationId && !organizationExists) { provider.upsertProviderProfile(currentApiConfigName ?? "default", { ...apiConfiguration, kilocodeOrganizationId: undefined, }) } try { const shouldAutoSwitch = response.data.organizations && response.data.organizations.length > 0 && !apiConfiguration.kilocodeOrganizationId && !getGlobalState("hasPerformedOrganizationAutoSwitch") if (shouldAutoSwitch) { const firstOrg = response.data.organizations![0] provider.log( `[Auto-switch] Performing automatic organization switch to: ${firstOrg.name} (${firstOrg.id})`, ) const upsertMessage: WebviewMessage = { type: "upsertApiConfiguration", text: currentApiConfigName ?? "default", apiConfiguration: { ...apiConfiguration, kilocodeOrganizationId: firstOrg.id, }, } await webviewMessageHandler(provider, upsertMessage) await updateGlobalState("hasPerformedOrganizationAutoSwitch", true) vscode.window.showInformationMessage(`Automatically switched to organization: ${firstOrg.name}`) provider.log(`[Auto-switch] Successfully switched to organization: ${firstOrg.name}`) } } catch (error) { provider.log( `[Auto-switch] Error during automatic organization switch: ${error instanceof Error ? error.message : String(error)}`, ) } provider.postMessageToWebview({ type: "profileDataResponse", payload: { success: true, data: { kilocodeToken, ...response.data } }, }) } catch (error: any) { const errorMessage = error.response?.data?.message || error.message || "Failed to fetch general profile data from backend." provider.log(`Error fetching general profile data: ${errorMessage}`) provider.postMessageToWebview({ type: "profileDataResponse", payload: { success: false, error: errorMessage }, }) } break case "fetchBalanceDataRequest": // New handler try { const { apiConfiguration } = await provider.getState() const { kilocodeToken, kilocodeOrganizationId } = apiConfiguration ?? {} if (!kilocodeToken) { provider.log("KiloCode token not found in extension state for balance data.") provider.postMessageToWebview({ type: "balanceDataResponse", // New response type payload: { success: false, error: "KiloCode API token not configured." }, }) break } const headers: Record = { Authorization: `Bearer ${kilocodeToken}`, "Content-Type": "application/json", } if (kilocodeOrganizationId) { headers["X-KiloCode-OrganizationId"] = kilocodeOrganizationId } // Add X-KILOCODE-TESTER: SUPPRESS header if the setting is enabled if ( apiConfiguration.kilocodeTesterWarningsDisabledUntil && apiConfiguration.kilocodeTesterWarningsDisabledUntil > Date.now() ) { headers["X-KILOCODE-TESTER"] = "SUPPRESS" } const response = await axios.get(`${getKiloBaseUriFromToken(kilocodeToken)}/api/profile/balance`, { // Original path for balance headers, }) provider.postMessageToWebview({ type: "balanceDataResponse", // New response type payload: { success: true, data: response.data }, }) } catch (error: any) { const errorMessage = error.response?.data?.message || error.message || "Failed to fetch balance data from backend." provider.log(`Error fetching balance data: ${errorMessage}`) provider.postMessageToWebview({ type: "balanceDataResponse", // New response type payload: { success: false, error: errorMessage }, }) } break case "shopBuyCredits": // New handler try { const { apiConfiguration } = await provider.getState() const kilocodeToken = apiConfiguration?.kilocodeToken if (!kilocodeToken) { provider.log("KiloCode token not found in extension state for buy credits.") break } const credits = message.values?.credits || 50 const uriScheme = message.values?.uriScheme || "vscode" const uiKind = message.values?.uiKind || "Desktop" const source = uiKind === "Web" ? "web" : uriScheme const baseUrl = getKiloBaseUriFromToken(kilocodeToken) const response = await axios.post( `${baseUrl}/payments/topup?origin=extension&source=${source}&amount=${credits}`, {}, { headers: { Authorization: `Bearer ${kilocodeToken}`, "Content-Type": "application/json", }, maxRedirects: 0, // Prevent axios from following redirects automatically validateStatus: (status) => status < 400, // Accept 3xx status codes }, ) if (response.status !== 303 || !response.headers.location) { return } await vscode.env.openExternal(vscode.Uri.parse(response.headers.location)) } catch (error: any) { const errorMessage = error?.message || "Unknown error" const errorStack = error?.stack ? ` Stack: ${error.stack}` : "" provider.log(`Error redirecting to payment page: ${errorMessage}.${errorStack}`) provider.postMessageToWebview({ type: "updateProfileData", }) } break case "fetchMcpMarketplace": { await provider.fetchMcpMarketplace(message.bool) break } case "downloadMcp": { if (message.mcpId) { await provider.downloadMcp(message.mcpId) } break } case "silentlyRefreshMcpMarketplace": { await provider.silentlyRefreshMcpMarketplace() break } case "toggleWorkflow": { if (message.workflowPath && typeof message.enabled === "boolean" && typeof message.isGlobal === "boolean") { await toggleWorkflow( message.workflowPath, message.enabled, message.isGlobal, provider.contextProxy, provider.context, ) await provider.postRulesDataToWebview() } break } case "refreshRules": { await provider.postRulesDataToWebview() break } case "toggleRule": { if (message.rulePath && typeof message.enabled === "boolean" && typeof message.isGlobal === "boolean") { await toggleRule( message.rulePath, message.enabled, message.isGlobal, provider.contextProxy, provider.context, ) await provider.postRulesDataToWebview() } break } case "createRuleFile": { if ( message.filename && typeof message.isGlobal === "boolean" && (message.ruleType === "rule" || message.ruleType === "workflow") ) { try { await createRuleFile(message.filename, message.isGlobal, message.ruleType) } catch (error) { console.error("Error creating rule file:", error) vscode.window.showErrorMessage(t("kilocode:rules.errors.failedToCreateRuleFile")) } await provider.postRulesDataToWebview() } break } case "deleteRuleFile": { if (message.rulePath) { try { await deleteRuleFile(message.rulePath) } catch (error) { console.error("Error deleting rule file:", error) vscode.window.showErrorMessage(t("kilocode:rules.errors.failedToDeleteRuleFile")) } await provider.postRulesDataToWebview() } break } case "reportBug": provider.getCurrentTask()?.handleWebviewAskResponse("yesButtonClicked") break // end kilocode_change case "telemetrySetting": { const telemetrySetting = message.text as TelemetrySetting await updateGlobalState("telemetrySetting", telemetrySetting) const isOptedIn = telemetrySetting === "enabled" TelemetryService.instance.updateTelemetryState(isOptedIn) await provider.postStateToWebview() break } case "cloudButtonClicked": { // Navigate to the cloud tab. provider.postMessageToWebview({ type: "action", action: "cloudButtonClicked" }) break } case "rooCloudSignIn": { try { TelemetryService.instance.captureEvent(TelemetryEventName.AUTHENTICATION_INITIATED) await CloudService.instance.login() } catch (error) { provider.log(`AuthService#login failed: ${error}`) vscode.window.showErrorMessage("Sign in failed.") } break } case "cloudLandingPageSignIn": { try { const landingPageSlug = message.text || "supernova" TelemetryService.instance.captureEvent(TelemetryEventName.AUTHENTICATION_INITIATED) await CloudService.instance.login(landingPageSlug) } catch (error) { provider.log(`CloudService#login failed: ${error}`) vscode.window.showErrorMessage("Sign in failed.") } break } case "rooCloudSignOut": { try { await CloudService.instance.logout() await provider.postStateToWebview() provider.postMessageToWebview({ type: "authenticatedUser", userInfo: undefined }) } catch (error) { provider.log(`AuthService#logout failed: ${error}`) vscode.window.showErrorMessage("Sign out failed.") } break } case "rooCloudManualUrl": { try { if (!message.text) { vscode.window.showErrorMessage(t("common:errors.manual_url_empty")) break } // Parse the callback URL to extract parameters const callbackUrl = message.text.trim() const uri = vscode.Uri.parse(callbackUrl) if (!uri.query) { throw new Error(t("common:errors.manual_url_no_query")) } const query = new URLSearchParams(uri.query) const code = query.get("code") const state = query.get("state") const organizationId = query.get("organizationId") if (!code || !state) { throw new Error(t("common:errors.manual_url_missing_params")) } // Reuse the existing authentication flow await CloudService.instance.handleAuthCallback( code, state, organizationId === "null" ? null : organizationId, ) await provider.postStateToWebview() } catch (error) { provider.log(`ManualUrl#handleAuthCallback failed: ${error}`) const errorMessage = error instanceof Error ? error.message : t("common:errors.manual_url_auth_failed") // Show error message through VS Code UI vscode.window.showErrorMessage(`${t("common:errors.manual_url_auth_error")}: ${errorMessage}`) } break } case "switchOrganization": { try { const organizationId = message.organizationId ?? null // Switch to the new organization context await CloudService.instance.switchOrganization(organizationId) // Refresh the state to update UI await provider.postStateToWebview() // Send success response back to webview await provider.postMessageToWebview({ type: "organizationSwitchResult", success: true, organizationId: organizationId, }) } catch (error) { provider.log(`Organization switch failed: ${error}`) const errorMessage = error instanceof Error ? error.message : String(error) // Send error response back to webview await provider.postMessageToWebview({ type: "organizationSwitchResult", success: false, error: errorMessage, organizationId: message.organizationId ?? null, }) vscode.window.showErrorMessage(`Failed to switch organization: ${errorMessage}`) } break } case "saveCodeIndexSettingsAtomic": { if (!message.codeIndexSettings) { break } const settings = message.codeIndexSettings try { // Check if embedder provider has changed const currentConfig = getGlobalState("codebaseIndexConfig") || {} const embedderProviderChanged = currentConfig.codebaseIndexEmbedderProvider !== settings.codebaseIndexEmbedderProvider // Save global state settings atomically const globalStateConfig = { ...currentConfig, codebaseIndexEnabled: settings.codebaseIndexEnabled, codebaseIndexQdrantUrl: settings.codebaseIndexQdrantUrl, codebaseIndexEmbedderProvider: settings.codebaseIndexEmbedderProvider, codebaseIndexEmbedderBaseUrl: settings.codebaseIndexEmbedderBaseUrl, codebaseIndexEmbedderModelId: settings.codebaseIndexEmbedderModelId, codebaseIndexEmbedderModelDimension: settings.codebaseIndexEmbedderModelDimension, // Generic dimension codebaseIndexOpenAiCompatibleBaseUrl: settings.codebaseIndexOpenAiCompatibleBaseUrl, codebaseIndexSearchMaxResults: settings.codebaseIndexSearchMaxResults, codebaseIndexSearchMinScore: settings.codebaseIndexSearchMinScore, } // Save global state first await updateGlobalState("codebaseIndexConfig", globalStateConfig) // Save secrets directly using context proxy if (settings.codeIndexOpenAiKey !== undefined) { await provider.contextProxy.storeSecret("codeIndexOpenAiKey", settings.codeIndexOpenAiKey) } if (settings.codeIndexQdrantApiKey !== undefined) { await provider.contextProxy.storeSecret("codeIndexQdrantApiKey", settings.codeIndexQdrantApiKey) } if (settings.codebaseIndexOpenAiCompatibleApiKey !== undefined) { await provider.contextProxy.storeSecret( "codebaseIndexOpenAiCompatibleApiKey", settings.codebaseIndexOpenAiCompatibleApiKey, ) } if (settings.codebaseIndexGeminiApiKey !== undefined) { await provider.contextProxy.storeSecret( "codebaseIndexGeminiApiKey", settings.codebaseIndexGeminiApiKey, ) } if (settings.codebaseIndexMistralApiKey !== undefined) { await provider.contextProxy.storeSecret( "codebaseIndexMistralApiKey", settings.codebaseIndexMistralApiKey, ) } if (settings.codebaseIndexVercelAiGatewayApiKey !== undefined) { await provider.contextProxy.storeSecret( "codebaseIndexVercelAiGatewayApiKey", settings.codebaseIndexVercelAiGatewayApiKey, ) } // Send success response first - settings are saved regardless of validation await provider.postMessageToWebview({ type: "codeIndexSettingsSaved", success: true, settings: globalStateConfig, }) // Update webview state await provider.postStateToWebview() // Then handle validation and initialization for the current workspace const currentCodeIndexManager = provider.getCurrentWorkspaceCodeIndexManager() if (currentCodeIndexManager) { // If embedder provider changed, perform proactive validation if (embedderProviderChanged) { try { // Force handleSettingsChange which will trigger validation await currentCodeIndexManager.handleSettingsChange() } catch (error) { // Validation failed - the error state is already set by handleSettingsChange provider.log( `Embedder validation failed after provider change: ${error instanceof Error ? error.message : String(error)}`, ) // Send validation error to webview await provider.postMessageToWebview({ type: "indexingStatusUpdate", values: currentCodeIndexManager.getCurrentStatus(), }) // Exit early - don't try to start indexing with invalid configuration break } } else { // No provider change, just handle settings normally try { await currentCodeIndexManager.handleSettingsChange() } catch (error) { // Log but don't fail - settings are saved provider.log( `Settings change handling error: ${error instanceof Error ? error.message : String(error)}`, ) } } // Wait a bit more to ensure everything is ready await new Promise((resolve) => setTimeout(resolve, 200)) // Auto-start indexing if now enabled and configured if (currentCodeIndexManager.isFeatureEnabled && currentCodeIndexManager.isFeatureConfigured) { if (!currentCodeIndexManager.isInitialized) { try { await currentCodeIndexManager.initialize(provider.contextProxy) provider.log(`Code index manager initialized after settings save`) } catch (error) { provider.log( `Code index initialization failed: ${error instanceof Error ? error.message : String(error)}`, ) // Send error status to webview await provider.postMessageToWebview({ type: "indexingStatusUpdate", values: currentCodeIndexManager.getCurrentStatus(), }) } } } } else { // No workspace open - send error status provider.log("Cannot save code index settings: No workspace folder open") await provider.postMessageToWebview({ type: "indexingStatusUpdate", values: { systemStatus: "Error", message: t("embeddings:orchestrator.indexingRequiresWorkspace"), processedItems: 0, totalItems: 0, currentItemUnit: "items", }, }) } } catch (error) { provider.log(`Error saving code index settings: ${error.message || error}`) await provider.postMessageToWebview({ type: "codeIndexSettingsSaved", success: false, error: error.message || "Failed to save settings", }) } break } case "requestIndexingStatus": { const manager = provider.getCurrentWorkspaceCodeIndexManager() if (!manager) { // No workspace open - send error status provider.postMessageToWebview({ type: "indexingStatusUpdate", values: { systemStatus: "Error", message: t("embeddings:orchestrator.indexingRequiresWorkspace"), processedItems: 0, totalItems: 0, currentItemUnit: "items", workerspacePath: undefined, }, }) return } const status = manager ? manager.getCurrentStatus() : { systemStatus: "Standby", message: "No workspace folder open", processedItems: 0, totalItems: 0, currentItemUnit: "items", workspacePath: undefined, } provider.postMessageToWebview({ type: "indexingStatusUpdate", values: status, }) break } case "requestCodeIndexSecretStatus": { // Check if secrets are set using the VSCode context directly for async access const hasOpenAiKey = !!(await provider.context.secrets.get("codeIndexOpenAiKey")) const hasQdrantApiKey = !!(await provider.context.secrets.get("codeIndexQdrantApiKey")) const hasOpenAiCompatibleApiKey = !!(await provider.context.secrets.get( "codebaseIndexOpenAiCompatibleApiKey", )) const hasGeminiApiKey = !!(await provider.context.secrets.get("codebaseIndexGeminiApiKey")) const hasMistralApiKey = !!(await provider.context.secrets.get("codebaseIndexMistralApiKey")) const hasVercelAiGatewayApiKey = !!(await provider.context.secrets.get( "codebaseIndexVercelAiGatewayApiKey", )) provider.postMessageToWebview({ type: "codeIndexSecretStatus", values: { hasOpenAiKey, hasQdrantApiKey, hasOpenAiCompatibleApiKey, hasGeminiApiKey, hasMistralApiKey, hasVercelAiGatewayApiKey, }, }) break } case "startIndexing": { try { const manager = provider.getCurrentWorkspaceCodeIndexManager() if (!manager) { // No workspace open - send error status provider.postMessageToWebview({ type: "indexingStatusUpdate", values: { systemStatus: "Error", message: t("embeddings:orchestrator.indexingRequiresWorkspace"), processedItems: 0, totalItems: 0, currentItemUnit: "items", }, }) provider.log("Cannot start indexing: No workspace folder open") return } if (manager.isFeatureEnabled && manager.isFeatureConfigured) { if (!manager.isInitialized) { await manager.initialize(provider.contextProxy) } // startIndexing now handles error recovery internally manager.startIndexing() // If startIndexing recovered from error, we need to reinitialize if (!manager.isInitialized) { await manager.initialize(provider.contextProxy) // Try starting again after initialization manager.startIndexing() } } } catch (error) { provider.log(`Error starting indexing: ${error instanceof Error ? error.message : String(error)}`) } break } // kilocode_change start case "cancelIndexing": { try { const manager = provider.getCurrentWorkspaceCodeIndexManager() if (!manager) { provider.postMessageToWebview({ type: "indexingStatusUpdate", values: { systemStatus: "Error", message: t("embeddings:orchestrator.indexingRequiresWorkspace"), processedItems: 0, totalItems: 0, currentItemUnit: "items", }, }) provider.log("Cannot cancel indexing: No workspace folder open") return } if (manager.isFeatureEnabled && manager.isFeatureConfigured) { manager.cancelIndexing() // Immediately reflect updated status to UI provider.postMessageToWebview({ type: "indexingStatusUpdate", values: manager.getCurrentStatus(), }) } } catch (error) { provider.log(`Error canceling indexing: ${error instanceof Error ? error.message : String(error)}`) } break } // kilocode_change end case "clearIndexData": { try { const manager = provider.getCurrentWorkspaceCodeIndexManager() if (!manager) { provider.log("Cannot clear index data: No workspace folder open") provider.postMessageToWebview({ type: "indexCleared", values: { success: false, error: t("embeddings:orchestrator.indexingRequiresWorkspace"), }, }) return } await manager.clearIndexData() provider.postMessageToWebview({ type: "indexCleared", values: { success: true } }) } catch (error) { provider.log(`Error clearing index data: ${error instanceof Error ? error.message : String(error)}`) provider.postMessageToWebview({ type: "indexCleared", values: { success: false, error: error instanceof Error ? error.message : String(error), }, }) } break } // kilocode_change start - add clearUsageData case "clearUsageData": { try { const usageTracker = UsageTracker.getInstance() await usageTracker.clearAllUsageData() vscode.window.showInformationMessage("Usage data has been successfully cleared.") } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) provider.log(`Error clearing usage data: ${errorMessage}`) vscode.window.showErrorMessage(`Failed to clear usage data: ${errorMessage}`) } break } // kilocode_change start - add getUsageData case "getUsageData": { if (message.text) { try { const usageTracker = UsageTracker.getInstance() const usageData = usageTracker.getAllUsage(message.text) await provider.postMessageToWebview({ type: "usageDataResponse", text: message.text, values: usageData, }) } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) provider.log(`Error getting usage data: ${errorMessage}`) } } break } // kilocode_change end - add getUsageData // kilocode_change start - add toggleTaskFavorite case "toggleTaskFavorite": if (message.text) { await provider.toggleTaskFavorite(message.text) } break // kilocode_change start - add fixMermaidSyntax case "fixMermaidSyntax": if (message.text && message.requestId) { try { const { apiConfiguration } = await provider.getState() const prompt = mermaidFixPrompt(message.values?.error || "Unknown syntax error", message.text) const fixedCode = await singleCompletionHandler(apiConfiguration, prompt) provider.postMessageToWebview({ type: "mermaidFixResponse", requestId: message.requestId, success: true, fixedCode: fixedCode?.trim() || null, }) } catch (error) { const errorMessage = error instanceof Error ? error.message : "Failed to fix Mermaid syntax" provider.log(`Error fixing Mermaid syntax: ${errorMessage}`) provider.postMessageToWebview({ type: "mermaidFixResponse", requestId: message.requestId, success: false, error: errorMessage, }) } } break // kilocode_change end case "focusPanelRequest": { // Execute the focusPanel command to focus the WebView await vscode.commands.executeCommand(getCommand("focusPanel")) break } case "filterMarketplaceItems": { if (marketplaceManager && message.filters) { try { await marketplaceManager.updateWithFilteredItems({ type: message.filters.type as MarketplaceItemType | undefined, search: message.filters.search, tags: message.filters.tags, }) await provider.postStateToWebview() } catch (error) { console.error("Marketplace: Error filtering items:", error) vscode.window.showErrorMessage("Failed to filter marketplace items") } } break } case "fetchMarketplaceData": { // Fetch marketplace data on demand await provider.fetchMarketplaceData() break } case "installMarketplaceItem": { if (marketplaceManager && message.mpItem && message.mpInstallOptions) { try { const configFilePath = await marketplaceManager.installMarketplaceItem( message.mpItem, message.mpInstallOptions, ) await provider.postStateToWebview() console.log(`Marketplace item installed and config file opened: ${configFilePath}`) // Send success message to webview provider.postMessageToWebview({ type: "marketplaceInstallResult", success: true, slug: message.mpItem.id, }) } catch (error) { console.error(`Error installing marketplace item: ${error}`) // Send error message to webview provider.postMessageToWebview({ type: "marketplaceInstallResult", success: false, error: error instanceof Error ? error.message : String(error), slug: message.mpItem.id, }) } } break } case "removeInstalledMarketplaceItem": { if (marketplaceManager && message.mpItem && message.mpInstallOptions) { try { await marketplaceManager.removeInstalledMarketplaceItem(message.mpItem, message.mpInstallOptions) await provider.postStateToWebview() // Send success message to webview provider.postMessageToWebview({ type: "marketplaceRemoveResult", success: true, slug: message.mpItem.id, }) } catch (error) { console.error(`Error removing marketplace item: ${error}`) // Show error message to user vscode.window.showErrorMessage( `Failed to remove marketplace item: ${error instanceof Error ? error.message : String(error)}`, ) // Send error message to webview provider.postMessageToWebview({ type: "marketplaceRemoveResult", success: false, error: error instanceof Error ? error.message : String(error), slug: message.mpItem.id, }) } } else { // MarketplaceManager not available or missing required parameters const errorMessage = !marketplaceManager ? "Marketplace manager is not available" : "Missing required parameters for marketplace item removal" console.error(errorMessage) vscode.window.showErrorMessage(errorMessage) if (message.mpItem?.id) { provider.postMessageToWebview({ type: "marketplaceRemoveResult", success: false, error: errorMessage, slug: message.mpItem.id, }) } } break } case "installMarketplaceItemWithParameters": { if (marketplaceManager && message.payload && "item" in message.payload && "parameters" in message.payload) { try { const configFilePath = await marketplaceManager.installMarketplaceItem(message.payload.item, { parameters: message.payload.parameters, }) await provider.postStateToWebview() console.log(`Marketplace item with parameters installed and config file opened: ${configFilePath}`) } catch (error) { console.error(`Error installing marketplace item with parameters: ${error}`) vscode.window.showErrorMessage( `Failed to install marketplace item: ${error instanceof Error ? error.message : String(error)}`, ) } } break } case "switchTab": { if (message.tab) { // Capture tab shown event for all switchTab messages (which are user-initiated) if (TelemetryService.hasInstance()) { TelemetryService.instance.captureTabShown(message.tab) } await provider.postMessageToWebview({ type: "action", action: "switchTab", tab: message.tab, values: message.values, }) } break } // kilocode_change start case "editMessage": { await editMessageHandler(provider, message) break } case "fetchKilocodeNotifications": { await fetchKilocodeNotificationsHandler(provider) break } case "dismissNotificationId": { if (!message.notificationId) { break } const dismissedNotificationIds = getGlobalState("dismissedNotificationIds") || [] await updateGlobalState("dismissedNotificationIds", [...dismissedNotificationIds, message.notificationId]) await provider.postStateToWebview() break } // kilocode_change end // kilocode_change start: Type-safe global state handler case "updateGlobalState": { const { stateKey, stateValue } = message as UpdateGlobalStateMessage if (stateKey !== undefined && stateValue !== undefined && isGlobalStateKey(stateKey)) { await updateGlobalState(stateKey, stateValue) await provider.postStateToWebview() } break } // kilocode_change end: Type-safe global state handler case "insertTextToChatArea": provider.postMessageToWebview({ type: "insertTextToChatArea", text: message.text }) break case "requestCommands": { try { const { getCommands } = await import("../../services/command/commands") const commands = await getCommands(getCurrentCwd()) // Convert to the format expected by the frontend const commandList = commands.map((command) => ({ name: command.name, source: command.source, filePath: command.filePath, description: command.description, argumentHint: command.argumentHint, })) await provider.postMessageToWebview({ type: "commands", commands: commandList, }) } catch (error) { provider.log(`Error fetching commands: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`) // Send empty array on error await provider.postMessageToWebview({ type: "commands", commands: [], }) } break } case "getKeybindings": { try { const { getKeybindingsForCommands } = await import("../../utils/keybindings") const keybindings = await getKeybindingsForCommands(message.commandIds ?? []) await provider.postMessageToWebview({ type: "keybindingsResponse", keybindings }) } catch (error) { await provider.postMessageToWebview({ type: "keybindingsResponse", keybindings: {} }) } break } case "openCommandFile": { try { if (message.text) { const { getCommand } = await import("../../services/command/commands") const command = await getCommand(getCurrentCwd(), message.text) if (command && command.filePath) { openFile(command.filePath) } else { vscode.window.showErrorMessage(t("common:errors.command_not_found", { name: message.text })) } } } catch (error) { provider.log( `Error opening command file: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, ) vscode.window.showErrorMessage(t("common:errors.open_command_file")) } break } case "deleteCommand": { try { if (message.text && message.values?.source) { const { getCommand } = await import("../../services/command/commands") const command = await getCommand(getCurrentCwd(), message.text) if (command && command.filePath) { // Delete the command file await fs.unlink(command.filePath) provider.log(`Deleted command file: ${command.filePath}`) } else { vscode.window.showErrorMessage(t("common:errors.command_not_found", { name: message.text })) } } } catch (error) { provider.log(`Error deleting command: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`) vscode.window.showErrorMessage(t("common:errors.delete_command")) } break } case "createCommand": { try { const source = message.values?.source as "global" | "project" const fileName = message.text // Custom filename from user input if (!source) { provider.log("Missing source for createCommand") break } // Determine the commands directory based on source let commandsDir: string if (source === "global") { const globalConfigDir = path.join(os.homedir(), ".roo") commandsDir = path.join(globalConfigDir, "commands") } else { if (!vscode.workspace.workspaceFolders?.length) { vscode.window.showErrorMessage(t("common:errors.no_workspace")) return } // Project commands const workspaceRoot = getCurrentCwd() if (!workspaceRoot) { vscode.window.showErrorMessage(t("common:errors.no_workspace_for_project_command")) break } commandsDir = path.join(workspaceRoot, ".roo", "commands") } // Ensure the commands directory exists await fs.mkdir(commandsDir, { recursive: true }) // Use provided filename or generate a unique one let commandName: string if (fileName && fileName.trim()) { let cleanFileName = fileName.trim() // Strip leading slash if present if (cleanFileName.startsWith("/")) { cleanFileName = cleanFileName.substring(1) } // Remove .md extension if present BEFORE slugification if (cleanFileName.toLowerCase().endsWith(".md")) { cleanFileName = cleanFileName.slice(0, -3) } // Slugify the command name: lowercase, replace spaces with dashes, remove special characters commandName = cleanFileName .toLowerCase() .replace(/\s+/g, "-") // Replace spaces with dashes .replace(/[^a-z0-9-]/g, "") // Remove special characters except dashes .replace(/-+/g, "-") // Replace multiple dashes with single dash .replace(/^-|-$/g, "") // Remove leading/trailing dashes // Ensure we have a valid command name if (!commandName || commandName.length === 0) { commandName = "new-command" } } else { // Generate a unique command name commandName = "new-command" let counter = 1 let filePath = path.join(commandsDir, `${commandName}.md`) while ( await fs .access(filePath) .then(() => true) .catch(() => false) ) { commandName = `new-command-${counter}` filePath = path.join(commandsDir, `${commandName}.md`) counter++ } } const filePath = path.join(commandsDir, `${commandName}.md`) // Check if file already exists if ( await fs .access(filePath) .then(() => true) .catch(() => false) ) { vscode.window.showErrorMessage(t("common:errors.command_already_exists", { commandName })) break } // Create the command file with template content const templateContent = t("common:errors.command_template_content") await fs.writeFile(filePath, templateContent, "utf8") provider.log(`Created new command file: ${filePath}`) // Open the new file in the editor openFile(filePath) // Refresh commands list const { getCommands } = await import("../../services/command/commands") const commands = await getCommands(getCurrentCwd() || "") const commandList = commands.map((command) => ({ name: command.name, source: command.source, filePath: command.filePath, description: command.description, argumentHint: command.argumentHint, })) await provider.postMessageToWebview({ type: "commands", commands: commandList, }) } catch (error) { provider.log(`Error creating command: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`) vscode.window.showErrorMessage(t("common:errors.create_command_failed")) } break } case "insertTextIntoTextarea": { const text = message.text if (text) { // Send message to insert text into the chat textarea await provider.postMessageToWebview({ type: "insertTextIntoTextarea", text: text, }) } break } case "showMdmAuthRequiredNotification": { // Show notification that organization requires authentication vscode.window.showWarningMessage(t("common:mdm.info.organization_requires_auth")) break } /** * Chat Message Queue */ case "queueMessage": { provider.getCurrentTask()?.messageQueueService.addMessage(message.text ?? "", message.images) break } case "removeQueuedMessage": { provider.getCurrentTask()?.messageQueueService.removeMessage(message.text ?? "") break } case "editQueuedMessage": { if (message.payload) { const { id, text, images } = message.payload as EditQueuedMessagePayload provider.getCurrentTask()?.messageQueueService.updateMessage(id, text, images) } break } case "dismissUpsell": { if (message.upsellId) { try { // Get current list of dismissed upsells const dismissedUpsells = getGlobalState("dismissedUpsells") || [] // Add the new upsell ID if not already present let updatedList = dismissedUpsells if (!dismissedUpsells.includes(message.upsellId)) { updatedList = [...dismissedUpsells, message.upsellId] await updateGlobalState("dismissedUpsells", updatedList) } // Send updated list back to webview (use the already computed updatedList) await provider.postMessageToWebview({ type: "dismissedUpsells", list: updatedList, }) } catch (error) { // Fail silently as per Bruno's comment - it's OK to fail silently in this case provider.log(`Failed to dismiss upsell: ${error instanceof Error ? error.message : String(error)}`) } } break } case "getDismissedUpsells": { // Send the current list of dismissed upsells to the webview const dismissedUpsells = getGlobalState("dismissedUpsells") || [] await provider.postMessageToWebview({ type: "dismissedUpsells", list: dismissedUpsells, }) break } } }