mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-05 08:10:14 +00:00
- Add manual token input UI to CloudView component with expandable section - Add new WebviewMessage type 'rooCloudManualToken' with token field - Implement handleManualToken method in WebAuthService to process manual tokens - Add CloudService.handleManualToken method to route manual token auth - Update webviewMessageHandler to handle manual token submission - Add translation strings for manual token UI elements This allows users in Firebase Studio and other IDEs that don't support automatic authentication redirects to manually paste the token from the authentication page. Fixes #7723
2924 lines
94 KiB
TypeScript
2924 lines
94 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,
|
|
} 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 { RouterName, toRouterName, ModelRecord } 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 }
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
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] Message not found! Looking for ts:", messageTs)
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
|
|
if (messageIndex === -1) {
|
|
const errorMessage = `Message with timestamp ${messageTs} not found`
|
|
console.error("[handleDeleteMessageConfirm]", 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: "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, apiConversationHistoryIndex)
|
|
|
|
// 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,
|
|
})
|
|
}
|
|
} catch (error) {
|
|
console.error("Error in delete message:", error)
|
|
vscode.window.showErrorMessage(
|
|
`Error deleting message: ${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 = `Message with timestamp ${messageTs} not found`
|
|
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, 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)
|
|
}
|
|
}
|
|
|
|
// Edit this message and delete subsequent
|
|
await removeMessagesThisAndSubsequent(currentCline, messageIndex, apiConversationHistoryIndex)
|
|
|
|
// 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,
|
|
})
|
|
|
|
// Process the edited message as a regular user message
|
|
webviewMessageHandler(provider, {
|
|
type: "askResponse",
|
|
askResponse: "messageResponse",
|
|
text: editedContent,
|
|
images,
|
|
})
|
|
|
|
// Don't initialize with history item for edit operations
|
|
// The webviewMessageHandler will handle the conversation state
|
|
} catch (error) {
|
|
console.error("Error in edit message:", error)
|
|
vscode.window.showErrorMessage(
|
|
`Error editing message: ${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)}`,
|
|
),
|
|
)
|
|
|
|
// If user already opted in to telemetry, enable telemetry service
|
|
provider.getStateToPostToWebview().then((state) => {
|
|
const { telemetrySetting } = state
|
|
const isOptedIn = telemetrySetting === "enabled"
|
|
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: Partial<Record<RouterName, ModelRecord>> = {
|
|
openrouter: {},
|
|
requesty: {},
|
|
glama: {},
|
|
unbound: {},
|
|
litellm: {},
|
|
ollama: {},
|
|
lmstudio: {},
|
|
deepinfra: {},
|
|
}
|
|
|
|
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: Array<{ 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 } // key is RouterName here
|
|
}),
|
|
)
|
|
|
|
const fetchedRouterModels: Partial<Record<RouterName, ModelRecord>> = {
|
|
...routerModels,
|
|
// Initialize ollama and lmstudio with empty objects since they use separate handlers
|
|
ollama: {},
|
|
lmstudio: {},
|
|
}
|
|
|
|
results.forEach((result, index) => {
|
|
const routerName = modelFetchPromises[index].key // Get RouterName using index
|
|
|
|
if (result.status === "fulfilled") {
|
|
fetchedRouterModels[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: Object.keys(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)
|
|
|
|
fetchedRouterModels[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: fetchedRouterModels as Record<RouterName, ModelRecord>,
|
|
})
|
|
|
|
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,
|
|
})
|
|
|
|
if (Object.keys(ollamaModels).length > 0) {
|
|
provider.postMessageToWebview({
|
|
type: "ollamaModels",
|
|
ollamaModels: Object.keys(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":
|
|
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 "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)}`,
|
|
)
|
|
}
|
|
|
|
try {
|
|
await provider.remoteControlEnabled(message.bool ?? false)
|
|
} catch (error) {
|
|
provider.log(
|
|
`ClineProvider#remoteControlEnabled failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
)
|
|
}
|
|
|
|
await provider.postStateToWebview()
|
|
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() && typeof message.value === "number" && message.value) {
|
|
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 "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 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
|
|
await updateGlobalState("telemetrySetting", telemetrySetting)
|
|
const isOptedIn = telemetrySetting === "enabled"
|
|
TelemetryService.instance.updateTelemetryState(isOptedIn)
|
|
await provider.postStateToWebview()
|
|
break
|
|
}
|
|
case "cloudButtonClicked": {
|
|
// Navigate to the cloud tab.
|
|
provider.postMessageToWebview({ type: "action", action: "cloudButtonClicked" })
|
|
break
|
|
}
|
|
case "rooCloudSignIn": {
|
|
try {
|
|
TelemetryService.instance.captureEvent(TelemetryEventName.AUTHENTICATION_INITIATED)
|
|
await CloudService.instance.login()
|
|
} catch (error) {
|
|
provider.log(`AuthService#login failed: ${error}`)
|
|
vscode.window.showErrorMessage("Sign in failed.")
|
|
}
|
|
|
|
break
|
|
}
|
|
case "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 "rooCloudManualToken": {
|
|
if (message.token) {
|
|
try {
|
|
// Extract the actual token from the URL if a full URL is pasted
|
|
let token = message.token.trim()
|
|
const tokenMatch = token.match(/[?&]token=([^&]+)/)
|
|
if (tokenMatch) {
|
|
token = tokenMatch[1]
|
|
}
|
|
|
|
// Call the manual token authentication method
|
|
await CloudService.instance.handleManualToken(token)
|
|
await provider.postStateToWebview()
|
|
provider.postMessageToWebview({
|
|
type: "authenticatedUser",
|
|
userInfo: CloudService.instance.getUserInfo(),
|
|
})
|
|
} catch (error) {
|
|
provider.log(
|
|
`Manual token authentication failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
)
|
|
vscode.window.showErrorMessage(
|
|
t("common:errors.manual_token_auth_failed") ||
|
|
"Manual token authentication failed. Please check the token and try again.",
|
|
)
|
|
}
|
|
}
|
|
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 })
|
|
}
|
|
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
|
|
}
|
|
}
|
|
}
|