mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
3114 lines
101 KiB
TypeScript
3114 lines
101 KiB
TypeScript
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"
|
|
|
|
import {
|
|
type Language,
|
|
type GlobalState,
|
|
type ClineMessage,
|
|
type TelemetrySetting,
|
|
TelemetryEventName,
|
|
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 { 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"
|
|
|
|
const ALLOWED_VSCODE_SETTINGS = new Set(["terminal.integrated.inheritEnv"])
|
|
|
|
import { MarketplaceManager, MarketplaceItemType } from "../../services/marketplace"
|
|
import { setPendingTodoList } from "../tools/updateTodoListTool"
|
|
|
|
export const webviewMessageHandler = async (
|
|
provider: ClineProvider,
|
|
message: WebviewMessage,
|
|
marketplaceManager?: MarketplaceManager,
|
|
) => {
|
|
// Utility functions provided for concise get/update of global state via contextProxy API.
|
|
const getGlobalState = <K extends keyof GlobalState>(key: K) => provider.contextProxy.getValue(key)
|
|
const updateGlobalState = async <K extends keyof GlobalState>(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<void> => {
|
|
// 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<void> => {
|
|
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<number, any>()
|
|
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<void> => {
|
|
// 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<void> => {
|
|
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<number, any>()
|
|
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<void>
|
|
*/
|
|
const handleMessageModificationsOperation = async (
|
|
messageTs: number,
|
|
operation: "delete" | "edit",
|
|
editedContent?: string,
|
|
images?: string[],
|
|
): Promise<void> => {
|
|
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)
|
|
|
|
provider.postStateToWebview()
|
|
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)}`,
|
|
),
|
|
)
|
|
|
|
// Enable telemetry by default (when unset) or when explicitly enabled
|
|
provider.getStateToPostToWebview().then((state) => {
|
|
const { telemetrySetting } = state
|
|
const isOptedIn = telemetrySetting !== "disabled"
|
|
TelemetryService.instance.updateTelemetryState(isOptedIn)
|
|
})
|
|
|
|
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
|
|
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, clineMessages)
|
|
|
|
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<RouterName, ModelRecord> = {
|
|
openrouter: {},
|
|
"vercel-ai-gateway": {},
|
|
huggingface: {},
|
|
litellm: {},
|
|
deepinfra: {},
|
|
"io-intelligence": {},
|
|
requesty: {},
|
|
unbound: {},
|
|
glama: {},
|
|
ollama: {},
|
|
lmstudio: {},
|
|
}
|
|
|
|
const safeGetModels = async (options: GetModelsOptions): Promise<ModelRecord> => {
|
|
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.
|
|
}
|
|
}
|
|
|
|
const modelFetchPromises: { key: RouterName; options: GetModelsOptions }[] = [
|
|
{ key: "openrouter", options: { provider: "openrouter" } },
|
|
{
|
|
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: "vercel-ai-gateway", options: { provider: "vercel-ai-gateway" } },
|
|
{
|
|
key: "deepinfra",
|
|
options: {
|
|
provider: "deepinfra",
|
|
apiKey: apiConfiguration.deepInfraApiKey,
|
|
baseUrl: apiConfiguration.deepInfraBaseUrl,
|
|
},
|
|
},
|
|
]
|
|
|
|
// 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,
|
|
})
|
|
|
|
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
|
|
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, ".roo")
|
|
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: {} })
|
|
}
|
|
|
|
await 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
|
|
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<UserSettingsConfig> = {
|
|
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
|
|
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<string, unknown>)[key]) !==
|
|
JSON.stringify((newPrompt as Record<string, unknown>)[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
|
|
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<string, boolean> = { ...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
|
|
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)}`,
|
|
)
|
|
|
|
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
|
|
}
|
|
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)
|
|
} 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":
|
|
if (message.text && message.apiConfiguration) {
|
|
await provider.upsertProviderProfile(message.text, message.apiConfiguration)
|
|
}
|
|
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<string, unknown>)[key]) !==
|
|
JSON.stringify((message.modeConfig as Record<string, unknown>)[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
|
|
|
|
case "telemetrySetting": {
|
|
const telemetrySetting = message.text as TelemetrySetting
|
|
const previousSetting = getGlobalState("telemetrySetting") || "unset"
|
|
const isOptedIn = telemetrySetting !== "disabled"
|
|
const wasPreviouslyOptedIn = previousSetting !== "disabled"
|
|
|
|
// If turning telemetry OFF, fire event BEFORE disabling
|
|
if (wasPreviouslyOptedIn && !isOptedIn && TelemetryService.hasInstance()) {
|
|
TelemetryService.instance.captureTelemetrySettingsChanged(previousSetting, telemetrySetting)
|
|
}
|
|
// Update the telemetry state
|
|
await updateGlobalState("telemetrySetting", telemetrySetting)
|
|
if (TelemetryService.hasInstance()) {
|
|
TelemetryService.instance.updateTelemetryState(isOptedIn)
|
|
}
|
|
|
|
// If turning telemetry ON, fire event AFTER enabling
|
|
if (!wasPreviouslyOptedIn && isOptedIn && TelemetryService.hasInstance()) {
|
|
TelemetryService.instance.captureTelemetrySettingsChanged(previousSetting, telemetrySetting)
|
|
}
|
|
|
|
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
|
|
}
|
|
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
|
|
}
|
|
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
|
|
}
|
|
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 "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
|
|
}
|
|
}
|
|
}
|