webviewMessageHandler.ts 68 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079
  1. import { safeWriteJson } from "../../utils/safeWriteJson"
  2. import * as path from "path"
  3. import * as os from "os"
  4. import * as fs from "fs/promises"
  5. import pWaitFor from "p-wait-for"
  6. import * as vscode from "vscode"
  7. import * as yaml from "yaml"
  8. import { type Language, type ProviderSettings, type GlobalState, TelemetryEventName } from "@roo-code/types"
  9. import { CloudService } from "@roo-code/cloud"
  10. import { TelemetryService } from "@roo-code/telemetry"
  11. import { ClineProvider } from "./ClineProvider"
  12. import { changeLanguage, t } from "../../i18n"
  13. import { Package } from "../../shared/package"
  14. import { RouterName, toRouterName, ModelRecord } from "../../shared/api"
  15. import { supportPrompt } from "../../shared/support-prompt"
  16. import { checkoutDiffPayloadSchema, checkoutRestorePayloadSchema, WebviewMessage } from "../../shared/WebviewMessage"
  17. import { checkExistKey } from "../../shared/checkExistApiConfig"
  18. import { experimentDefault } from "../../shared/experiments"
  19. import { Terminal } from "../../integrations/terminal/Terminal"
  20. import { openFile } from "../../integrations/misc/open-file"
  21. import { openImage, saveImage } from "../../integrations/misc/image-handler"
  22. import { selectImages } from "../../integrations/misc/process-images"
  23. import { getTheme } from "../../integrations/theme/getTheme"
  24. import { discoverChromeHostUrl, tryChromeHostUrl } from "../../services/browser/browserDiscovery"
  25. import { searchWorkspaceFiles } from "../../services/search/file-search"
  26. import { fileExistsAtPath } from "../../utils/fs"
  27. import { playTts, setTtsEnabled, setTtsSpeed, stopTts } from "../../utils/tts"
  28. import { singleCompletionHandler } from "../../utils/single-completion-handler"
  29. import { searchCommits } from "../../utils/git"
  30. import { exportSettings, importSettingsWithFeedback } from "../config/importExport"
  31. import { getOpenAiModels } from "../../api/providers/openai"
  32. import { getVsCodeLmModels } from "../../api/providers/vscode-lm"
  33. import { openMention } from "../mentions"
  34. import { TelemetrySetting } from "../../shared/TelemetrySetting"
  35. import { getWorkspacePath } from "../../utils/path"
  36. import { ensureSettingsDirectoryExists } from "../../utils/globalContext"
  37. import { Mode, defaultModeSlug } from "../../shared/modes"
  38. import { getModels, flushModels } from "../../api/providers/fetchers/modelCache"
  39. import { GetModelsOptions } from "../../shared/api"
  40. import { generateSystemPrompt } from "./generateSystemPrompt"
  41. import { getCommand } from "../../utils/commands"
  42. const ALLOWED_VSCODE_SETTINGS = new Set(["terminal.integrated.inheritEnv"])
  43. import { MarketplaceManager, MarketplaceItemType } from "../../services/marketplace"
  44. import { setPendingTodoList } from "../tools/updateTodoListTool"
  45. export const webviewMessageHandler = async (
  46. provider: ClineProvider,
  47. message: WebviewMessage,
  48. marketplaceManager?: MarketplaceManager,
  49. ) => {
  50. // Utility functions provided for concise get/update of global state via contextProxy API.
  51. const getGlobalState = <K extends keyof GlobalState>(key: K) => provider.contextProxy.getValue(key)
  52. const updateGlobalState = async <K extends keyof GlobalState>(key: K, value: GlobalState[K]) =>
  53. await provider.contextProxy.setValue(key, value)
  54. switch (message.type) {
  55. case "webviewDidLaunch":
  56. // Load custom modes first
  57. const customModes = await provider.customModesManager.getCustomModes()
  58. await updateGlobalState("customModes", customModes)
  59. provider.postStateToWebview()
  60. provider.workspaceTracker?.initializeFilePaths() // Don't await.
  61. getTheme().then((theme) => provider.postMessageToWebview({ type: "theme", text: JSON.stringify(theme) }))
  62. // If MCP Hub is already initialized, update the webview with
  63. // current server list.
  64. const mcpHub = provider.getMcpHub()
  65. if (mcpHub) {
  66. provider.postMessageToWebview({ type: "mcpServers", mcpServers: mcpHub.getAllServers() })
  67. }
  68. provider.providerSettingsManager
  69. .listConfig()
  70. .then(async (listApiConfig) => {
  71. if (!listApiConfig) {
  72. return
  73. }
  74. if (listApiConfig.length === 1) {
  75. // Check if first time init then sync with exist config.
  76. if (!checkExistKey(listApiConfig[0])) {
  77. const { apiConfiguration } = await provider.getState()
  78. await provider.providerSettingsManager.saveConfig(
  79. listApiConfig[0].name ?? "default",
  80. apiConfiguration,
  81. )
  82. listApiConfig[0].apiProvider = apiConfiguration.apiProvider
  83. }
  84. }
  85. const currentConfigName = getGlobalState("currentApiConfigName")
  86. if (currentConfigName) {
  87. if (!(await provider.providerSettingsManager.hasConfig(currentConfigName))) {
  88. // Current config name not valid, get first config in list.
  89. const name = listApiConfig[0]?.name
  90. await updateGlobalState("currentApiConfigName", name)
  91. if (name) {
  92. await provider.activateProviderProfile({ name })
  93. return
  94. }
  95. }
  96. }
  97. await Promise.all([
  98. await updateGlobalState("listApiConfigMeta", listApiConfig),
  99. await provider.postMessageToWebview({ type: "listApiConfig", listApiConfig }),
  100. ])
  101. })
  102. .catch((error) =>
  103. provider.log(
  104. `Error list api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  105. ),
  106. )
  107. // If user already opted in to telemetry, enable telemetry service
  108. provider.getStateToPostToWebview().then((state) => {
  109. const { telemetrySetting } = state
  110. const isOptedIn = telemetrySetting === "enabled"
  111. TelemetryService.instance.updateTelemetryState(isOptedIn)
  112. })
  113. provider.isViewLaunched = true
  114. break
  115. case "newTask":
  116. // Initializing new instance of Cline will make sure that any
  117. // agentically running promises in old instance don't affect our new
  118. // task. This essentially creates a fresh slate for the new task.
  119. await provider.initClineWithTask(message.text, message.images)
  120. break
  121. case "customInstructions":
  122. await provider.updateCustomInstructions(message.text)
  123. break
  124. case "alwaysAllowReadOnly":
  125. await updateGlobalState("alwaysAllowReadOnly", message.bool ?? undefined)
  126. await provider.postStateToWebview()
  127. break
  128. case "alwaysAllowReadOnlyOutsideWorkspace":
  129. await updateGlobalState("alwaysAllowReadOnlyOutsideWorkspace", message.bool ?? undefined)
  130. await provider.postStateToWebview()
  131. break
  132. case "alwaysAllowWrite":
  133. await updateGlobalState("alwaysAllowWrite", message.bool ?? undefined)
  134. await provider.postStateToWebview()
  135. break
  136. case "alwaysAllowWriteOutsideWorkspace":
  137. await updateGlobalState("alwaysAllowWriteOutsideWorkspace", message.bool ?? undefined)
  138. await provider.postStateToWebview()
  139. break
  140. case "alwaysAllowWriteProtected":
  141. await updateGlobalState("alwaysAllowWriteProtected", message.bool ?? undefined)
  142. await provider.postStateToWebview()
  143. break
  144. case "alwaysAllowExecute":
  145. await updateGlobalState("alwaysAllowExecute", message.bool ?? undefined)
  146. await provider.postStateToWebview()
  147. break
  148. case "alwaysAllowBrowser":
  149. await updateGlobalState("alwaysAllowBrowser", message.bool ?? undefined)
  150. await provider.postStateToWebview()
  151. break
  152. case "alwaysAllowMcp":
  153. await updateGlobalState("alwaysAllowMcp", message.bool)
  154. await provider.postStateToWebview()
  155. break
  156. case "alwaysAllowModeSwitch":
  157. await updateGlobalState("alwaysAllowModeSwitch", message.bool)
  158. await provider.postStateToWebview()
  159. break
  160. case "allowedMaxRequests":
  161. await updateGlobalState("allowedMaxRequests", message.value)
  162. await provider.postStateToWebview()
  163. break
  164. case "alwaysAllowSubtasks":
  165. await updateGlobalState("alwaysAllowSubtasks", message.bool)
  166. await provider.postStateToWebview()
  167. break
  168. case "alwaysAllowUpdateTodoList":
  169. await updateGlobalState("alwaysAllowUpdateTodoList", message.bool)
  170. await provider.postStateToWebview()
  171. break
  172. case "askResponse":
  173. provider.getCurrentCline()?.handleWebviewAskResponse(message.askResponse!, message.text, message.images)
  174. break
  175. case "autoCondenseContext":
  176. await updateGlobalState("autoCondenseContext", message.bool)
  177. await provider.postStateToWebview()
  178. break
  179. case "autoCondenseContextPercent":
  180. await updateGlobalState("autoCondenseContextPercent", message.value)
  181. await provider.postStateToWebview()
  182. break
  183. case "terminalOperation":
  184. if (message.terminalOperation) {
  185. provider.getCurrentCline()?.handleTerminalOperation(message.terminalOperation)
  186. }
  187. break
  188. case "clearTask":
  189. // 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
  190. // Check if the current task actually has a parent task
  191. const currentTask = provider.getCurrentCline()
  192. if (currentTask && currentTask.parentTask) {
  193. await provider.finishSubTask(t("common:tasks.canceled"))
  194. } else {
  195. // Regular task - just clear it
  196. await provider.clearTask()
  197. }
  198. await provider.postStateToWebview()
  199. break
  200. case "didShowAnnouncement":
  201. await updateGlobalState("lastShownAnnouncementId", provider.latestAnnouncementId)
  202. await provider.postStateToWebview()
  203. break
  204. case "selectImages":
  205. const images = await selectImages()
  206. await provider.postMessageToWebview({ type: "selectedImages", images })
  207. break
  208. case "exportCurrentTask":
  209. const currentTaskId = provider.getCurrentCline()?.taskId
  210. if (currentTaskId) {
  211. provider.exportTaskWithId(currentTaskId)
  212. }
  213. break
  214. case "shareCurrentTask":
  215. const shareTaskId = provider.getCurrentCline()?.taskId
  216. const clineMessages = provider.getCurrentCline()?.clineMessages
  217. if (!shareTaskId) {
  218. vscode.window.showErrorMessage(t("common:errors.share_no_active_task"))
  219. break
  220. }
  221. try {
  222. const visibility = message.visibility || "organization"
  223. const result = await CloudService.instance.shareTask(shareTaskId, visibility, clineMessages)
  224. if (result.success && result.shareUrl) {
  225. // Show success notification
  226. const messageKey =
  227. visibility === "public"
  228. ? "common:info.public_share_link_copied"
  229. : "common:info.organization_share_link_copied"
  230. vscode.window.showInformationMessage(t(messageKey))
  231. // Send success feedback to webview for inline display
  232. await provider.postMessageToWebview({
  233. type: "shareTaskSuccess",
  234. visibility,
  235. text: result.shareUrl,
  236. })
  237. } else {
  238. // Handle error
  239. const errorMessage = result.error || "Failed to create share link"
  240. if (errorMessage.includes("Authentication")) {
  241. vscode.window.showErrorMessage(t("common:errors.share_auth_required"))
  242. } else if (errorMessage.includes("sharing is not enabled")) {
  243. vscode.window.showErrorMessage(t("common:errors.share_not_enabled"))
  244. } else if (errorMessage.includes("not found")) {
  245. vscode.window.showErrorMessage(t("common:errors.share_task_not_found"))
  246. } else {
  247. vscode.window.showErrorMessage(errorMessage)
  248. }
  249. }
  250. } catch (error) {
  251. provider.log(`[shareCurrentTask] Unexpected error: ${error}`)
  252. vscode.window.showErrorMessage(t("common:errors.share_task_failed"))
  253. }
  254. break
  255. case "showTaskWithId":
  256. provider.showTaskWithId(message.text!)
  257. break
  258. case "condenseTaskContextRequest":
  259. provider.condenseTaskContext(message.text!)
  260. break
  261. case "deleteTaskWithId":
  262. provider.deleteTaskWithId(message.text!)
  263. break
  264. case "deleteMultipleTasksWithIds": {
  265. const ids = message.ids
  266. if (Array.isArray(ids)) {
  267. // Process in batches of 20 (or another reasonable number)
  268. const batchSize = 20
  269. const results = []
  270. // Only log start and end of the operation
  271. console.log(`Batch deletion started: ${ids.length} tasks total`)
  272. for (let i = 0; i < ids.length; i += batchSize) {
  273. const batch = ids.slice(i, i + batchSize)
  274. const batchPromises = batch.map(async (id) => {
  275. try {
  276. await provider.deleteTaskWithId(id)
  277. return { id, success: true }
  278. } catch (error) {
  279. // Keep error logging for debugging purposes
  280. console.log(
  281. `Failed to delete task ${id}: ${error instanceof Error ? error.message : String(error)}`,
  282. )
  283. return { id, success: false }
  284. }
  285. })
  286. // Process each batch in parallel but wait for completion before starting the next batch
  287. const batchResults = await Promise.all(batchPromises)
  288. results.push(...batchResults)
  289. // Update the UI after each batch to show progress
  290. await provider.postStateToWebview()
  291. }
  292. // Log final results
  293. const successCount = results.filter((r) => r.success).length
  294. const failCount = results.length - successCount
  295. console.log(
  296. `Batch deletion completed: ${successCount}/${ids.length} tasks successful, ${failCount} tasks failed`,
  297. )
  298. }
  299. break
  300. }
  301. case "exportTaskWithId":
  302. provider.exportTaskWithId(message.text!)
  303. break
  304. case "importSettings": {
  305. await importSettingsWithFeedback({
  306. providerSettingsManager: provider.providerSettingsManager,
  307. contextProxy: provider.contextProxy,
  308. customModesManager: provider.customModesManager,
  309. provider: provider,
  310. })
  311. break
  312. }
  313. case "exportSettings":
  314. await exportSettings({
  315. providerSettingsManager: provider.providerSettingsManager,
  316. contextProxy: provider.contextProxy,
  317. })
  318. break
  319. case "resetState":
  320. await provider.resetState()
  321. break
  322. case "flushRouterModels":
  323. const routerNameFlush: RouterName = toRouterName(message.text)
  324. await flushModels(routerNameFlush)
  325. break
  326. case "requestRouterModels":
  327. const { apiConfiguration } = await provider.getState()
  328. const routerModels: Partial<Record<RouterName, ModelRecord>> = {
  329. openrouter: {},
  330. requesty: {},
  331. glama: {},
  332. unbound: {},
  333. litellm: {},
  334. ollama: {},
  335. lmstudio: {},
  336. }
  337. const safeGetModels = async (options: GetModelsOptions): Promise<ModelRecord> => {
  338. try {
  339. return await getModels(options)
  340. } catch (error) {
  341. console.error(
  342. `Failed to fetch models in webviewMessageHandler requestRouterModels for ${options.provider}:`,
  343. error,
  344. )
  345. throw error // Re-throw to be caught by Promise.allSettled
  346. }
  347. }
  348. const modelFetchPromises: Array<{ key: RouterName; options: GetModelsOptions }> = [
  349. { key: "openrouter", options: { provider: "openrouter" } },
  350. { key: "requesty", options: { provider: "requesty", apiKey: apiConfiguration.requestyApiKey } },
  351. { key: "glama", options: { provider: "glama" } },
  352. { key: "unbound", options: { provider: "unbound", apiKey: apiConfiguration.unboundApiKey } },
  353. ]
  354. // Don't fetch Ollama and LM Studio models by default anymore
  355. // They have their own specific handlers: requestOllamaModels and requestLmStudioModels
  356. const litellmApiKey = apiConfiguration.litellmApiKey || message?.values?.litellmApiKey
  357. const litellmBaseUrl = apiConfiguration.litellmBaseUrl || message?.values?.litellmBaseUrl
  358. if (litellmApiKey && litellmBaseUrl) {
  359. modelFetchPromises.push({
  360. key: "litellm",
  361. options: { provider: "litellm", apiKey: litellmApiKey, baseUrl: litellmBaseUrl },
  362. })
  363. }
  364. const results = await Promise.allSettled(
  365. modelFetchPromises.map(async ({ key, options }) => {
  366. const models = await safeGetModels(options)
  367. return { key, models } // key is RouterName here
  368. }),
  369. )
  370. const fetchedRouterModels: Partial<Record<RouterName, ModelRecord>> = {
  371. ...routerModels,
  372. // Initialize ollama and lmstudio with empty objects since they use separate handlers
  373. ollama: {},
  374. lmstudio: {},
  375. }
  376. results.forEach((result, index) => {
  377. const routerName = modelFetchPromises[index].key // Get RouterName using index
  378. if (result.status === "fulfilled") {
  379. fetchedRouterModels[routerName] = result.value.models
  380. // Ollama and LM Studio settings pages still need these events
  381. if (routerName === "ollama" && Object.keys(result.value.models).length > 0) {
  382. provider.postMessageToWebview({
  383. type: "ollamaModels",
  384. ollamaModels: Object.keys(result.value.models),
  385. })
  386. } else if (routerName === "lmstudio" && Object.keys(result.value.models).length > 0) {
  387. provider.postMessageToWebview({
  388. type: "lmStudioModels",
  389. lmStudioModels: Object.keys(result.value.models),
  390. })
  391. }
  392. } else {
  393. // Handle rejection: Post a specific error message for this provider
  394. const errorMessage = result.reason instanceof Error ? result.reason.message : String(result.reason)
  395. console.error(`Error fetching models for ${routerName}:`, result.reason)
  396. fetchedRouterModels[routerName] = {} // Ensure it's an empty object in the main routerModels message
  397. provider.postMessageToWebview({
  398. type: "singleRouterModelFetchResponse",
  399. success: false,
  400. error: errorMessage,
  401. values: { provider: routerName },
  402. })
  403. }
  404. })
  405. provider.postMessageToWebview({
  406. type: "routerModels",
  407. routerModels: fetchedRouterModels as Record<RouterName, ModelRecord>,
  408. })
  409. break
  410. case "requestOllamaModels": {
  411. // Specific handler for Ollama models only
  412. const { apiConfiguration: ollamaApiConfig } = await provider.getState()
  413. try {
  414. // Flush cache first to ensure fresh models
  415. await flushModels("ollama")
  416. const ollamaModels = await getModels({
  417. provider: "ollama",
  418. baseUrl: ollamaApiConfig.ollamaBaseUrl,
  419. })
  420. if (Object.keys(ollamaModels).length > 0) {
  421. provider.postMessageToWebview({
  422. type: "ollamaModels",
  423. ollamaModels: Object.keys(ollamaModels),
  424. })
  425. }
  426. } catch (error) {
  427. // Silently fail - user hasn't configured Ollama yet
  428. console.debug("Ollama models fetch failed:", error)
  429. }
  430. break
  431. }
  432. case "requestLmStudioModels": {
  433. // Specific handler for LM Studio models only
  434. const { apiConfiguration: lmStudioApiConfig } = await provider.getState()
  435. try {
  436. // Flush cache first to ensure fresh models
  437. await flushModels("lmstudio")
  438. const lmStudioModels = await getModels({
  439. provider: "lmstudio",
  440. baseUrl: lmStudioApiConfig.lmStudioBaseUrl,
  441. })
  442. if (Object.keys(lmStudioModels).length > 0) {
  443. provider.postMessageToWebview({
  444. type: "lmStudioModels",
  445. lmStudioModels: Object.keys(lmStudioModels),
  446. })
  447. }
  448. } catch (error) {
  449. // Silently fail - user hasn't configured LM Studio yet
  450. console.debug("LM Studio models fetch failed:", error)
  451. }
  452. break
  453. }
  454. case "requestOpenAiModels":
  455. if (message?.values?.baseUrl && message?.values?.apiKey) {
  456. const openAiModels = await getOpenAiModels(
  457. message?.values?.baseUrl,
  458. message?.values?.apiKey,
  459. message?.values?.openAiHeaders,
  460. )
  461. provider.postMessageToWebview({ type: "openAiModels", openAiModels })
  462. }
  463. break
  464. case "requestVsCodeLmModels":
  465. const vsCodeLmModels = await getVsCodeLmModels()
  466. // TODO: Cache like we do for OpenRouter, etc?
  467. provider.postMessageToWebview({ type: "vsCodeLmModels", vsCodeLmModels })
  468. break
  469. case "openImage":
  470. openImage(message.text!, { values: message.values })
  471. break
  472. case "saveImage":
  473. saveImage(message.dataUri!)
  474. break
  475. case "openFile":
  476. openFile(message.text!, message.values as { create?: boolean; content?: string; line?: number })
  477. break
  478. case "openMention":
  479. openMention(message.text)
  480. break
  481. case "openExternal":
  482. if (message.url) {
  483. vscode.env.openExternal(vscode.Uri.parse(message.url))
  484. }
  485. break
  486. case "checkpointDiff":
  487. const result = checkoutDiffPayloadSchema.safeParse(message.payload)
  488. if (result.success) {
  489. await provider.getCurrentCline()?.checkpointDiff(result.data)
  490. }
  491. break
  492. case "checkpointRestore": {
  493. const result = checkoutRestorePayloadSchema.safeParse(message.payload)
  494. if (result.success) {
  495. await provider.cancelTask()
  496. try {
  497. await pWaitFor(() => provider.getCurrentCline()?.isInitialized === true, { timeout: 3_000 })
  498. } catch (error) {
  499. vscode.window.showErrorMessage(t("common:errors.checkpoint_timeout"))
  500. }
  501. try {
  502. await provider.getCurrentCline()?.checkpointRestore(result.data)
  503. } catch (error) {
  504. vscode.window.showErrorMessage(t("common:errors.checkpoint_failed"))
  505. }
  506. }
  507. break
  508. }
  509. case "cancelTask":
  510. await provider.cancelTask()
  511. break
  512. case "allowedCommands": {
  513. // Validate and sanitize the commands array
  514. const commands = message.commands ?? []
  515. const validCommands = Array.isArray(commands)
  516. ? commands.filter((cmd) => typeof cmd === "string" && cmd.trim().length > 0)
  517. : []
  518. await updateGlobalState("allowedCommands", validCommands)
  519. // Also update workspace settings.
  520. await vscode.workspace
  521. .getConfiguration(Package.name)
  522. .update("allowedCommands", validCommands, vscode.ConfigurationTarget.Global)
  523. break
  524. }
  525. case "openCustomModesSettings": {
  526. const customModesFilePath = await provider.customModesManager.getCustomModesFilePath()
  527. if (customModesFilePath) {
  528. openFile(customModesFilePath)
  529. }
  530. break
  531. }
  532. case "openMcpSettings": {
  533. const mcpSettingsFilePath = await provider.getMcpHub()?.getMcpSettingsFilePath()
  534. if (mcpSettingsFilePath) {
  535. openFile(mcpSettingsFilePath)
  536. }
  537. break
  538. }
  539. case "openProjectMcpSettings": {
  540. if (!vscode.workspace.workspaceFolders?.length) {
  541. vscode.window.showErrorMessage(t("common:errors.no_workspace"))
  542. return
  543. }
  544. const workspaceFolder = vscode.workspace.workspaceFolders[0]
  545. const rooDir = path.join(workspaceFolder.uri.fsPath, ".roo")
  546. const mcpPath = path.join(rooDir, "mcp.json")
  547. try {
  548. await fs.mkdir(rooDir, { recursive: true })
  549. const exists = await fileExistsAtPath(mcpPath)
  550. if (!exists) {
  551. await safeWriteJson(mcpPath, { mcpServers: {} })
  552. }
  553. await openFile(mcpPath)
  554. } catch (error) {
  555. vscode.window.showErrorMessage(t("mcp:errors.create_json", { error: `${error}` }))
  556. }
  557. break
  558. }
  559. case "deleteMcpServer": {
  560. if (!message.serverName) {
  561. break
  562. }
  563. try {
  564. provider.log(`Attempting to delete MCP server: ${message.serverName}`)
  565. await provider.getMcpHub()?.deleteServer(message.serverName, message.source as "global" | "project")
  566. provider.log(`Successfully deleted MCP server: ${message.serverName}`)
  567. // Refresh the webview state
  568. await provider.postStateToWebview()
  569. } catch (error) {
  570. const errorMessage = error instanceof Error ? error.message : String(error)
  571. provider.log(`Failed to delete MCP server: ${errorMessage}`)
  572. // Error messages are already handled by McpHub.deleteServer
  573. }
  574. break
  575. }
  576. case "restartMcpServer": {
  577. try {
  578. await provider.getMcpHub()?.restartConnection(message.text!, message.source as "global" | "project")
  579. } catch (error) {
  580. provider.log(
  581. `Failed to retry connection for ${message.text}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  582. )
  583. }
  584. break
  585. }
  586. case "toggleToolAlwaysAllow": {
  587. try {
  588. await provider
  589. .getMcpHub()
  590. ?.toggleToolAlwaysAllow(
  591. message.serverName!,
  592. message.source as "global" | "project",
  593. message.toolName!,
  594. Boolean(message.alwaysAllow),
  595. )
  596. } catch (error) {
  597. provider.log(
  598. `Failed to toggle auto-approve for tool ${message.toolName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  599. )
  600. }
  601. break
  602. }
  603. case "toggleToolEnabledForPrompt": {
  604. try {
  605. await provider
  606. .getMcpHub()
  607. ?.toggleToolEnabledForPrompt(
  608. message.serverName!,
  609. message.source as "global" | "project",
  610. message.toolName!,
  611. Boolean(message.isEnabled),
  612. )
  613. } catch (error) {
  614. provider.log(
  615. `Failed to toggle enabled for prompt for tool ${message.toolName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  616. )
  617. }
  618. break
  619. }
  620. case "toggleMcpServer": {
  621. try {
  622. await provider
  623. .getMcpHub()
  624. ?.toggleServerDisabled(
  625. message.serverName!,
  626. message.disabled!,
  627. message.source as "global" | "project",
  628. )
  629. } catch (error) {
  630. provider.log(
  631. `Failed to toggle MCP server ${message.serverName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  632. )
  633. }
  634. break
  635. }
  636. case "mcpEnabled":
  637. const mcpEnabled = message.bool ?? true
  638. await updateGlobalState("mcpEnabled", mcpEnabled)
  639. await provider.postStateToWebview()
  640. break
  641. case "enableMcpServerCreation":
  642. await updateGlobalState("enableMcpServerCreation", message.bool ?? true)
  643. await provider.postStateToWebview()
  644. break
  645. case "refreshAllMcpServers": {
  646. const mcpHub = provider.getMcpHub()
  647. if (mcpHub) {
  648. await mcpHub.refreshAllConnections()
  649. }
  650. break
  651. }
  652. // playSound handler removed - now handled directly in the webview
  653. case "soundEnabled":
  654. const soundEnabled = message.bool ?? true
  655. await updateGlobalState("soundEnabled", soundEnabled)
  656. await provider.postStateToWebview()
  657. break
  658. case "soundVolume":
  659. const soundVolume = message.value ?? 0.5
  660. await updateGlobalState("soundVolume", soundVolume)
  661. await provider.postStateToWebview()
  662. break
  663. case "ttsEnabled":
  664. const ttsEnabled = message.bool ?? true
  665. await updateGlobalState("ttsEnabled", ttsEnabled)
  666. setTtsEnabled(ttsEnabled) // Add this line to update the tts utility
  667. await provider.postStateToWebview()
  668. break
  669. case "ttsSpeed":
  670. const ttsSpeed = message.value ?? 1.0
  671. await updateGlobalState("ttsSpeed", ttsSpeed)
  672. setTtsSpeed(ttsSpeed)
  673. await provider.postStateToWebview()
  674. break
  675. case "playTts":
  676. if (message.text) {
  677. playTts(message.text, {
  678. onStart: () => provider.postMessageToWebview({ type: "ttsStart", text: message.text }),
  679. onStop: () => provider.postMessageToWebview({ type: "ttsStop", text: message.text }),
  680. })
  681. }
  682. break
  683. case "stopTts":
  684. stopTts()
  685. break
  686. case "diffEnabled":
  687. const diffEnabled = message.bool ?? true
  688. await updateGlobalState("diffEnabled", diffEnabled)
  689. await provider.postStateToWebview()
  690. break
  691. case "enableCheckpoints":
  692. const enableCheckpoints = message.bool ?? true
  693. await updateGlobalState("enableCheckpoints", enableCheckpoints)
  694. await provider.postStateToWebview()
  695. break
  696. case "browserViewportSize":
  697. const browserViewportSize = message.text ?? "900x600"
  698. await updateGlobalState("browserViewportSize", browserViewportSize)
  699. await provider.postStateToWebview()
  700. break
  701. case "remoteBrowserHost":
  702. await updateGlobalState("remoteBrowserHost", message.text)
  703. await provider.postStateToWebview()
  704. break
  705. case "remoteBrowserEnabled":
  706. // Store the preference in global state
  707. // remoteBrowserEnabled now means "enable remote browser connection"
  708. await updateGlobalState("remoteBrowserEnabled", message.bool ?? false)
  709. // If disabling remote browser connection, clear the remoteBrowserHost
  710. if (!message.bool) {
  711. await updateGlobalState("remoteBrowserHost", undefined)
  712. }
  713. await provider.postStateToWebview()
  714. break
  715. case "testBrowserConnection":
  716. // If no text is provided, try auto-discovery
  717. if (!message.text) {
  718. // Use testBrowserConnection for auto-discovery
  719. const chromeHostUrl = await discoverChromeHostUrl()
  720. if (chromeHostUrl) {
  721. // Send the result back to the webview
  722. await provider.postMessageToWebview({
  723. type: "browserConnectionResult",
  724. success: !!chromeHostUrl,
  725. text: `Auto-discovered and tested connection to Chrome: ${chromeHostUrl}`,
  726. values: { endpoint: chromeHostUrl },
  727. })
  728. } else {
  729. await provider.postMessageToWebview({
  730. type: "browserConnectionResult",
  731. success: false,
  732. text: "No Chrome instances found on the network. Make sure Chrome is running with remote debugging enabled (--remote-debugging-port=9222).",
  733. })
  734. }
  735. } else {
  736. // Test the provided URL
  737. const customHostUrl = message.text
  738. const hostIsValid = await tryChromeHostUrl(message.text)
  739. // Send the result back to the webview
  740. await provider.postMessageToWebview({
  741. type: "browserConnectionResult",
  742. success: hostIsValid,
  743. text: hostIsValid
  744. ? `Successfully connected to Chrome: ${customHostUrl}`
  745. : "Failed to connect to Chrome",
  746. })
  747. }
  748. break
  749. case "fuzzyMatchThreshold":
  750. await updateGlobalState("fuzzyMatchThreshold", message.value)
  751. await provider.postStateToWebview()
  752. break
  753. case "updateVSCodeSetting": {
  754. const { setting, value } = message
  755. if (setting !== undefined && value !== undefined) {
  756. if (ALLOWED_VSCODE_SETTINGS.has(setting)) {
  757. await vscode.workspace.getConfiguration().update(setting, value, true)
  758. } else {
  759. vscode.window.showErrorMessage(`Cannot update restricted VSCode setting: ${setting}`)
  760. }
  761. }
  762. break
  763. }
  764. case "getVSCodeSetting":
  765. const { setting } = message
  766. if (setting) {
  767. try {
  768. await provider.postMessageToWebview({
  769. type: "vsCodeSetting",
  770. setting,
  771. value: vscode.workspace.getConfiguration().get(setting),
  772. })
  773. } catch (error) {
  774. console.error(`Failed to get VSCode setting ${message.setting}:`, error)
  775. await provider.postMessageToWebview({
  776. type: "vsCodeSetting",
  777. setting,
  778. error: `Failed to get setting: ${error.message}`,
  779. value: undefined,
  780. })
  781. }
  782. }
  783. break
  784. case "alwaysApproveResubmit":
  785. await updateGlobalState("alwaysApproveResubmit", message.bool ?? false)
  786. await provider.postStateToWebview()
  787. break
  788. case "requestDelaySeconds":
  789. await updateGlobalState("requestDelaySeconds", message.value ?? 5)
  790. await provider.postStateToWebview()
  791. break
  792. case "writeDelayMs":
  793. await updateGlobalState("writeDelayMs", message.value)
  794. await provider.postStateToWebview()
  795. break
  796. case "terminalOutputLineLimit":
  797. await updateGlobalState("terminalOutputLineLimit", message.value)
  798. await provider.postStateToWebview()
  799. break
  800. case "terminalShellIntegrationTimeout":
  801. await updateGlobalState("terminalShellIntegrationTimeout", message.value)
  802. await provider.postStateToWebview()
  803. if (message.value !== undefined) {
  804. Terminal.setShellIntegrationTimeout(message.value)
  805. }
  806. break
  807. case "terminalShellIntegrationDisabled":
  808. await updateGlobalState("terminalShellIntegrationDisabled", message.bool)
  809. await provider.postStateToWebview()
  810. if (message.bool !== undefined) {
  811. Terminal.setShellIntegrationDisabled(message.bool)
  812. }
  813. break
  814. case "terminalCommandDelay":
  815. await updateGlobalState("terminalCommandDelay", message.value)
  816. await provider.postStateToWebview()
  817. if (message.value !== undefined) {
  818. Terminal.setCommandDelay(message.value)
  819. }
  820. break
  821. case "terminalPowershellCounter":
  822. await updateGlobalState("terminalPowershellCounter", message.bool)
  823. await provider.postStateToWebview()
  824. if (message.bool !== undefined) {
  825. Terminal.setPowershellCounter(message.bool)
  826. }
  827. break
  828. case "terminalZshClearEolMark":
  829. await updateGlobalState("terminalZshClearEolMark", message.bool)
  830. await provider.postStateToWebview()
  831. if (message.bool !== undefined) {
  832. Terminal.setTerminalZshClearEolMark(message.bool)
  833. }
  834. break
  835. case "terminalZshOhMy":
  836. await updateGlobalState("terminalZshOhMy", message.bool)
  837. await provider.postStateToWebview()
  838. if (message.bool !== undefined) {
  839. Terminal.setTerminalZshOhMy(message.bool)
  840. }
  841. break
  842. case "terminalZshP10k":
  843. await updateGlobalState("terminalZshP10k", message.bool)
  844. await provider.postStateToWebview()
  845. if (message.bool !== undefined) {
  846. Terminal.setTerminalZshP10k(message.bool)
  847. }
  848. break
  849. case "terminalZdotdir":
  850. await updateGlobalState("terminalZdotdir", message.bool)
  851. await provider.postStateToWebview()
  852. if (message.bool !== undefined) {
  853. Terminal.setTerminalZdotdir(message.bool)
  854. }
  855. break
  856. case "terminalCompressProgressBar":
  857. await updateGlobalState("terminalCompressProgressBar", message.bool)
  858. await provider.postStateToWebview()
  859. if (message.bool !== undefined) {
  860. Terminal.setCompressProgressBar(message.bool)
  861. }
  862. break
  863. case "mode":
  864. await provider.handleModeSwitch(message.text as Mode)
  865. break
  866. case "updateSupportPrompt":
  867. try {
  868. if (!message?.values) {
  869. return
  870. }
  871. // Replace all prompts with the new values from the cached state
  872. await updateGlobalState("customSupportPrompts", message.values)
  873. await provider.postStateToWebview()
  874. } catch (error) {
  875. provider.log(
  876. `Error update support prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  877. )
  878. vscode.window.showErrorMessage(t("common:errors.update_support_prompt"))
  879. }
  880. break
  881. case "updatePrompt":
  882. if (message.promptMode && message.customPrompt !== undefined) {
  883. const existingPrompts = getGlobalState("customModePrompts") ?? {}
  884. const updatedPrompts = { ...existingPrompts, [message.promptMode]: message.customPrompt }
  885. await updateGlobalState("customModePrompts", updatedPrompts)
  886. const currentState = await provider.getStateToPostToWebview()
  887. const stateWithPrompts = {
  888. ...currentState,
  889. customModePrompts: updatedPrompts,
  890. hasOpenedModeSelector: currentState.hasOpenedModeSelector ?? false,
  891. }
  892. provider.postMessageToWebview({ type: "state", state: stateWithPrompts })
  893. if (TelemetryService.hasInstance()) {
  894. // Determine which setting was changed by comparing objects
  895. const oldPrompt = existingPrompts[message.promptMode] || {}
  896. const newPrompt = message.customPrompt
  897. const changedSettings = Object.keys(newPrompt).filter(
  898. (key) =>
  899. JSON.stringify((oldPrompt as Record<string, unknown>)[key]) !==
  900. JSON.stringify((newPrompt as Record<string, unknown>)[key]),
  901. )
  902. if (changedSettings.length > 0) {
  903. TelemetryService.instance.captureModeSettingChanged(changedSettings[0])
  904. }
  905. }
  906. }
  907. break
  908. case "deleteMessage": {
  909. const answer = await vscode.window.showInformationMessage(
  910. t("common:confirmation.delete_message"),
  911. { modal: true },
  912. t("common:confirmation.just_this_message"),
  913. t("common:confirmation.this_and_subsequent"),
  914. )
  915. if (
  916. (answer === t("common:confirmation.just_this_message") ||
  917. answer === t("common:confirmation.this_and_subsequent")) &&
  918. provider.getCurrentCline() &&
  919. typeof message.value === "number" &&
  920. message.value
  921. ) {
  922. const timeCutoff = message.value - 1000 // 1 second buffer before the message to delete
  923. const messageIndex = provider
  924. .getCurrentCline()!
  925. .clineMessages.findIndex((msg) => msg.ts && msg.ts >= timeCutoff)
  926. const apiConversationHistoryIndex = provider
  927. .getCurrentCline()
  928. ?.apiConversationHistory.findIndex((msg) => msg.ts && msg.ts >= timeCutoff)
  929. if (messageIndex !== -1) {
  930. const { historyItem } = await provider.getTaskWithId(provider.getCurrentCline()!.taskId)
  931. if (answer === t("common:confirmation.just_this_message")) {
  932. // Find the next user message first
  933. const nextUserMessage = provider
  934. .getCurrentCline()!
  935. .clineMessages.slice(messageIndex + 1)
  936. .find((msg) => msg.type === "say" && msg.say === "user_feedback")
  937. // Handle UI messages
  938. if (nextUserMessage) {
  939. // Find absolute index of next user message
  940. const nextUserMessageIndex = provider
  941. .getCurrentCline()!
  942. .clineMessages.findIndex((msg) => msg === nextUserMessage)
  943. // Keep messages before current message and after next user message
  944. await provider
  945. .getCurrentCline()!
  946. .overwriteClineMessages([
  947. ...provider.getCurrentCline()!.clineMessages.slice(0, messageIndex),
  948. ...provider.getCurrentCline()!.clineMessages.slice(nextUserMessageIndex),
  949. ])
  950. } else {
  951. // If no next user message, keep only messages before current message
  952. await provider
  953. .getCurrentCline()!
  954. .overwriteClineMessages(
  955. provider.getCurrentCline()!.clineMessages.slice(0, messageIndex),
  956. )
  957. }
  958. // Handle API messages
  959. if (apiConversationHistoryIndex !== -1) {
  960. if (nextUserMessage && nextUserMessage.ts) {
  961. // Keep messages before current API message and after next user message
  962. await provider
  963. .getCurrentCline()!
  964. .overwriteApiConversationHistory([
  965. ...provider
  966. .getCurrentCline()!
  967. .apiConversationHistory.slice(0, apiConversationHistoryIndex),
  968. ...provider
  969. .getCurrentCline()!
  970. .apiConversationHistory.filter(
  971. (msg) => msg.ts && msg.ts >= nextUserMessage.ts,
  972. ),
  973. ])
  974. } else {
  975. // If no next user message, keep only messages before current API message
  976. await provider
  977. .getCurrentCline()!
  978. .overwriteApiConversationHistory(
  979. provider
  980. .getCurrentCline()!
  981. .apiConversationHistory.slice(0, apiConversationHistoryIndex),
  982. )
  983. }
  984. }
  985. } else if (answer === t("common:confirmation.this_and_subsequent")) {
  986. // Delete this message and all that follow
  987. await provider
  988. .getCurrentCline()!
  989. .overwriteClineMessages(provider.getCurrentCline()!.clineMessages.slice(0, messageIndex))
  990. if (apiConversationHistoryIndex !== -1) {
  991. await provider
  992. .getCurrentCline()!
  993. .overwriteApiConversationHistory(
  994. provider
  995. .getCurrentCline()!
  996. .apiConversationHistory.slice(0, apiConversationHistoryIndex),
  997. )
  998. }
  999. }
  1000. await provider.initClineWithHistoryItem(historyItem)
  1001. }
  1002. }
  1003. break
  1004. }
  1005. case "screenshotQuality":
  1006. await updateGlobalState("screenshotQuality", message.value)
  1007. await provider.postStateToWebview()
  1008. break
  1009. case "maxOpenTabsContext":
  1010. const tabCount = Math.min(Math.max(0, message.value ?? 20), 500)
  1011. await updateGlobalState("maxOpenTabsContext", tabCount)
  1012. await provider.postStateToWebview()
  1013. break
  1014. case "maxWorkspaceFiles":
  1015. const fileCount = Math.min(Math.max(0, message.value ?? 200), 500)
  1016. await updateGlobalState("maxWorkspaceFiles", fileCount)
  1017. await provider.postStateToWebview()
  1018. break
  1019. case "alwaysAllowFollowupQuestions":
  1020. await updateGlobalState("alwaysAllowFollowupQuestions", message.bool ?? false)
  1021. await provider.postStateToWebview()
  1022. break
  1023. case "followupAutoApproveTimeoutMs":
  1024. await updateGlobalState("followupAutoApproveTimeoutMs", message.value)
  1025. await provider.postStateToWebview()
  1026. break
  1027. case "browserToolEnabled":
  1028. await updateGlobalState("browserToolEnabled", message.bool ?? true)
  1029. await provider.postStateToWebview()
  1030. break
  1031. case "codebaseIndexEnabled":
  1032. // Update the codebaseIndexConfig with the new enabled state
  1033. const currentCodebaseConfig = getGlobalState("codebaseIndexConfig") || {}
  1034. await updateGlobalState("codebaseIndexConfig", {
  1035. ...currentCodebaseConfig,
  1036. codebaseIndexEnabled: message.bool ?? false,
  1037. })
  1038. // Notify the code index manager about the change
  1039. if (provider.codeIndexManager) {
  1040. await provider.codeIndexManager.handleSettingsChange()
  1041. }
  1042. await provider.postStateToWebview()
  1043. break
  1044. case "language":
  1045. changeLanguage(message.text ?? "en")
  1046. await updateGlobalState("language", message.text as Language)
  1047. await provider.postStateToWebview()
  1048. break
  1049. case "showRooIgnoredFiles":
  1050. await updateGlobalState("showRooIgnoredFiles", message.bool ?? true)
  1051. await provider.postStateToWebview()
  1052. break
  1053. case "hasOpenedModeSelector":
  1054. await updateGlobalState("hasOpenedModeSelector", message.bool ?? true)
  1055. await provider.postStateToWebview()
  1056. break
  1057. case "maxReadFileLine":
  1058. await updateGlobalState("maxReadFileLine", message.value)
  1059. await provider.postStateToWebview()
  1060. break
  1061. case "maxConcurrentFileReads":
  1062. const valueToSave = message.value // Capture the value intended for saving
  1063. await updateGlobalState("maxConcurrentFileReads", valueToSave)
  1064. await provider.postStateToWebview()
  1065. break
  1066. case "setHistoryPreviewCollapsed": // Add the new case handler
  1067. await updateGlobalState("historyPreviewCollapsed", message.bool ?? false)
  1068. // No need to call postStateToWebview here as the UI already updated optimistically
  1069. break
  1070. case "toggleApiConfigPin":
  1071. if (message.text) {
  1072. const currentPinned = getGlobalState("pinnedApiConfigs") ?? {}
  1073. const updatedPinned: Record<string, boolean> = { ...currentPinned }
  1074. if (currentPinned[message.text]) {
  1075. delete updatedPinned[message.text]
  1076. } else {
  1077. updatedPinned[message.text] = true
  1078. }
  1079. await updateGlobalState("pinnedApiConfigs", updatedPinned)
  1080. await provider.postStateToWebview()
  1081. }
  1082. break
  1083. case "enhancementApiConfigId":
  1084. await updateGlobalState("enhancementApiConfigId", message.text)
  1085. await provider.postStateToWebview()
  1086. break
  1087. case "condensingApiConfigId":
  1088. await updateGlobalState("condensingApiConfigId", message.text)
  1089. await provider.postStateToWebview()
  1090. break
  1091. case "updateCondensingPrompt":
  1092. await updateGlobalState("customCondensingPrompt", message.text)
  1093. await provider.postStateToWebview()
  1094. break
  1095. case "profileThresholds":
  1096. await updateGlobalState("profileThresholds", message.values)
  1097. await provider.postStateToWebview()
  1098. break
  1099. case "autoApprovalEnabled":
  1100. await updateGlobalState("autoApprovalEnabled", message.bool ?? false)
  1101. await provider.postStateToWebview()
  1102. break
  1103. case "enhancePrompt":
  1104. if (message.text) {
  1105. try {
  1106. const { apiConfiguration, customSupportPrompts, listApiConfigMeta, enhancementApiConfigId } =
  1107. await provider.getState()
  1108. // Try to get enhancement config first, fall back to current config.
  1109. let configToUse: ProviderSettings = apiConfiguration
  1110. if (enhancementApiConfigId && !!listApiConfigMeta.find(({ id }) => id === enhancementApiConfigId)) {
  1111. const { name: _, ...providerSettings } = await provider.providerSettingsManager.getProfile({
  1112. id: enhancementApiConfigId,
  1113. })
  1114. if (providerSettings.apiProvider) {
  1115. configToUse = providerSettings
  1116. }
  1117. }
  1118. const enhancedPrompt = await singleCompletionHandler(
  1119. configToUse,
  1120. supportPrompt.create("ENHANCE", { userInput: message.text }, customSupportPrompts),
  1121. )
  1122. // Capture telemetry for prompt enhancement.
  1123. const currentCline = provider.getCurrentCline()
  1124. TelemetryService.instance.capturePromptEnhanced(currentCline?.taskId)
  1125. await provider.postMessageToWebview({ type: "enhancedPrompt", text: enhancedPrompt })
  1126. } catch (error) {
  1127. provider.log(
  1128. `Error enhancing prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1129. )
  1130. vscode.window.showErrorMessage(t("common:errors.enhance_prompt"))
  1131. await provider.postMessageToWebview({ type: "enhancedPrompt" })
  1132. }
  1133. }
  1134. break
  1135. case "getSystemPrompt":
  1136. try {
  1137. const systemPrompt = await generateSystemPrompt(provider, message)
  1138. await provider.postMessageToWebview({
  1139. type: "systemPrompt",
  1140. text: systemPrompt,
  1141. mode: message.mode,
  1142. })
  1143. } catch (error) {
  1144. provider.log(
  1145. `Error getting system prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1146. )
  1147. vscode.window.showErrorMessage(t("common:errors.get_system_prompt"))
  1148. }
  1149. break
  1150. case "copySystemPrompt":
  1151. try {
  1152. const systemPrompt = await generateSystemPrompt(provider, message)
  1153. await vscode.env.clipboard.writeText(systemPrompt)
  1154. await vscode.window.showInformationMessage(t("common:info.clipboard_copy"))
  1155. } catch (error) {
  1156. provider.log(
  1157. `Error getting system prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1158. )
  1159. vscode.window.showErrorMessage(t("common:errors.get_system_prompt"))
  1160. }
  1161. break
  1162. case "searchCommits": {
  1163. const cwd = provider.cwd
  1164. if (cwd) {
  1165. try {
  1166. const commits = await searchCommits(message.query || "", cwd)
  1167. await provider.postMessageToWebview({
  1168. type: "commitSearchResults",
  1169. commits,
  1170. })
  1171. } catch (error) {
  1172. provider.log(
  1173. `Error searching commits: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1174. )
  1175. vscode.window.showErrorMessage(t("common:errors.search_commits"))
  1176. }
  1177. }
  1178. break
  1179. }
  1180. case "searchFiles": {
  1181. const workspacePath = getWorkspacePath()
  1182. if (!workspacePath) {
  1183. // Handle case where workspace path is not available
  1184. await provider.postMessageToWebview({
  1185. type: "fileSearchResults",
  1186. results: [],
  1187. requestId: message.requestId,
  1188. error: "No workspace path available",
  1189. })
  1190. break
  1191. }
  1192. try {
  1193. // Call file search service with query from message
  1194. const results = await searchWorkspaceFiles(
  1195. message.query || "",
  1196. workspacePath,
  1197. 20, // Use default limit, as filtering is now done in the backend
  1198. )
  1199. // Send results back to webview
  1200. await provider.postMessageToWebview({
  1201. type: "fileSearchResults",
  1202. results,
  1203. requestId: message.requestId,
  1204. })
  1205. } catch (error) {
  1206. const errorMessage = error instanceof Error ? error.message : String(error)
  1207. // Send error response to webview
  1208. await provider.postMessageToWebview({
  1209. type: "fileSearchResults",
  1210. results: [],
  1211. error: errorMessage,
  1212. requestId: message.requestId,
  1213. })
  1214. }
  1215. break
  1216. }
  1217. case "updateTodoList": {
  1218. const payload = message.payload as { todos?: any[] }
  1219. const todos = payload?.todos
  1220. if (Array.isArray(todos)) {
  1221. await setPendingTodoList(todos)
  1222. }
  1223. break
  1224. }
  1225. case "saveApiConfiguration":
  1226. if (message.text && message.apiConfiguration) {
  1227. try {
  1228. await provider.providerSettingsManager.saveConfig(message.text, message.apiConfiguration)
  1229. const listApiConfig = await provider.providerSettingsManager.listConfig()
  1230. await updateGlobalState("listApiConfigMeta", listApiConfig)
  1231. } catch (error) {
  1232. provider.log(
  1233. `Error save api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1234. )
  1235. vscode.window.showErrorMessage(t("common:errors.save_api_config"))
  1236. }
  1237. }
  1238. break
  1239. case "upsertApiConfiguration":
  1240. if (message.text && message.apiConfiguration) {
  1241. await provider.upsertProviderProfile(message.text, message.apiConfiguration)
  1242. }
  1243. break
  1244. case "renameApiConfiguration":
  1245. if (message.values && message.apiConfiguration) {
  1246. try {
  1247. const { oldName, newName } = message.values
  1248. if (oldName === newName) {
  1249. break
  1250. }
  1251. // Load the old configuration to get its ID.
  1252. const { id } = await provider.providerSettingsManager.getProfile({ name: oldName })
  1253. // Create a new configuration with the new name and old ID.
  1254. await provider.providerSettingsManager.saveConfig(newName, { ...message.apiConfiguration, id })
  1255. // Delete the old configuration.
  1256. await provider.providerSettingsManager.deleteConfig(oldName)
  1257. // Re-activate to update the global settings related to the
  1258. // currently activated provider profile.
  1259. await provider.activateProviderProfile({ name: newName })
  1260. } catch (error) {
  1261. provider.log(
  1262. `Error rename api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1263. )
  1264. vscode.window.showErrorMessage(t("common:errors.rename_api_config"))
  1265. }
  1266. }
  1267. break
  1268. case "loadApiConfiguration":
  1269. if (message.text) {
  1270. try {
  1271. await provider.activateProviderProfile({ name: message.text })
  1272. } catch (error) {
  1273. provider.log(
  1274. `Error load api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1275. )
  1276. vscode.window.showErrorMessage(t("common:errors.load_api_config"))
  1277. }
  1278. }
  1279. break
  1280. case "loadApiConfigurationById":
  1281. if (message.text) {
  1282. try {
  1283. await provider.activateProviderProfile({ id: message.text })
  1284. } catch (error) {
  1285. provider.log(
  1286. `Error load api configuration by ID: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1287. )
  1288. vscode.window.showErrorMessage(t("common:errors.load_api_config"))
  1289. }
  1290. }
  1291. break
  1292. case "deleteApiConfiguration":
  1293. if (message.text) {
  1294. const answer = await vscode.window.showInformationMessage(
  1295. t("common:confirmation.delete_config_profile"),
  1296. { modal: true },
  1297. t("common:answers.yes"),
  1298. )
  1299. if (answer !== t("common:answers.yes")) {
  1300. break
  1301. }
  1302. const oldName = message.text
  1303. const newName = (await provider.providerSettingsManager.listConfig()).filter(
  1304. (c) => c.name !== oldName,
  1305. )[0]?.name
  1306. if (!newName) {
  1307. vscode.window.showErrorMessage(t("common:errors.delete_api_config"))
  1308. return
  1309. }
  1310. try {
  1311. await provider.providerSettingsManager.deleteConfig(oldName)
  1312. await provider.activateProviderProfile({ name: newName })
  1313. } catch (error) {
  1314. provider.log(
  1315. `Error delete api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1316. )
  1317. vscode.window.showErrorMessage(t("common:errors.delete_api_config"))
  1318. }
  1319. }
  1320. break
  1321. case "getListApiConfiguration":
  1322. try {
  1323. const listApiConfig = await provider.providerSettingsManager.listConfig()
  1324. await updateGlobalState("listApiConfigMeta", listApiConfig)
  1325. provider.postMessageToWebview({ type: "listApiConfig", listApiConfig })
  1326. } catch (error) {
  1327. provider.log(
  1328. `Error get list api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1329. )
  1330. vscode.window.showErrorMessage(t("common:errors.list_api_config"))
  1331. }
  1332. break
  1333. case "updateExperimental": {
  1334. if (!message.values) {
  1335. break
  1336. }
  1337. const updatedExperiments = {
  1338. ...(getGlobalState("experiments") ?? experimentDefault),
  1339. ...message.values,
  1340. }
  1341. await updateGlobalState("experiments", updatedExperiments)
  1342. await provider.postStateToWebview()
  1343. break
  1344. }
  1345. case "updateMcpTimeout":
  1346. if (message.serverName && typeof message.timeout === "number") {
  1347. try {
  1348. await provider
  1349. .getMcpHub()
  1350. ?.updateServerTimeout(
  1351. message.serverName,
  1352. message.timeout,
  1353. message.source as "global" | "project",
  1354. )
  1355. } catch (error) {
  1356. provider.log(
  1357. `Failed to update timeout for ${message.serverName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1358. )
  1359. vscode.window.showErrorMessage(t("common:errors.update_server_timeout"))
  1360. }
  1361. }
  1362. break
  1363. case "updateCustomMode":
  1364. if (message.modeConfig) {
  1365. // Check if this is a new mode or an update to an existing mode
  1366. const existingModes = await provider.customModesManager.getCustomModes()
  1367. const isNewMode = !existingModes.some((mode) => mode.slug === message.modeConfig?.slug)
  1368. await provider.customModesManager.updateCustomMode(message.modeConfig.slug, message.modeConfig)
  1369. // Update state after saving the mode
  1370. const customModes = await provider.customModesManager.getCustomModes()
  1371. await updateGlobalState("customModes", customModes)
  1372. await updateGlobalState("mode", message.modeConfig.slug)
  1373. await provider.postStateToWebview()
  1374. // Track telemetry for custom mode creation or update
  1375. if (TelemetryService.hasInstance()) {
  1376. if (isNewMode) {
  1377. // This is a new custom mode
  1378. TelemetryService.instance.captureCustomModeCreated(
  1379. message.modeConfig.slug,
  1380. message.modeConfig.name,
  1381. )
  1382. } else {
  1383. // Determine which setting was changed by comparing objects
  1384. const existingMode = existingModes.find((mode) => mode.slug === message.modeConfig?.slug)
  1385. const changedSettings = existingMode
  1386. ? Object.keys(message.modeConfig).filter(
  1387. (key) =>
  1388. JSON.stringify((existingMode as Record<string, unknown>)[key]) !==
  1389. JSON.stringify((message.modeConfig as Record<string, unknown>)[key]),
  1390. )
  1391. : []
  1392. if (changedSettings.length > 0) {
  1393. TelemetryService.instance.captureModeSettingChanged(changedSettings[0])
  1394. }
  1395. }
  1396. }
  1397. }
  1398. break
  1399. case "deleteCustomMode":
  1400. if (message.slug) {
  1401. // Get the mode details to determine source and rules folder path
  1402. const customModes = await provider.customModesManager.getCustomModes()
  1403. const modeToDelete = customModes.find((mode) => mode.slug === message.slug)
  1404. if (!modeToDelete) {
  1405. break
  1406. }
  1407. // Determine the scope based on source (project or global)
  1408. const scope = modeToDelete.source || "global"
  1409. // Determine the rules folder path
  1410. let rulesFolderPath: string
  1411. if (scope === "project") {
  1412. const workspacePath = getWorkspacePath()
  1413. if (workspacePath) {
  1414. rulesFolderPath = path.join(workspacePath, ".roo", `rules-${message.slug}`)
  1415. } else {
  1416. rulesFolderPath = path.join(".roo", `rules-${message.slug}`)
  1417. }
  1418. } else {
  1419. // Global scope - use OS home directory
  1420. const homeDir = os.homedir()
  1421. rulesFolderPath = path.join(homeDir, ".roo", `rules-${message.slug}`)
  1422. }
  1423. // Check if the rules folder exists
  1424. const rulesFolderExists = await fileExistsAtPath(rulesFolderPath)
  1425. // If this is a check request, send back the folder info
  1426. if (message.checkOnly) {
  1427. await provider.postMessageToWebview({
  1428. type: "deleteCustomModeCheck",
  1429. slug: message.slug,
  1430. rulesFolderPath: rulesFolderExists ? rulesFolderPath : undefined,
  1431. })
  1432. break
  1433. }
  1434. // Delete the mode
  1435. await provider.customModesManager.deleteCustomMode(message.slug)
  1436. // Delete the rules folder if it exists
  1437. if (rulesFolderExists) {
  1438. try {
  1439. await fs.rm(rulesFolderPath, { recursive: true, force: true })
  1440. provider.log(`Deleted rules folder for mode ${message.slug}: ${rulesFolderPath}`)
  1441. } catch (error) {
  1442. provider.log(`Failed to delete rules folder for mode ${message.slug}: ${error}`)
  1443. // Notify the user about the failure
  1444. vscode.window.showErrorMessage(
  1445. t("common:errors.delete_rules_folder_failed", {
  1446. rulesFolderPath,
  1447. error: error instanceof Error ? error.message : String(error),
  1448. }),
  1449. )
  1450. // Continue with mode deletion even if folder deletion fails
  1451. }
  1452. }
  1453. // Switch back to default mode after deletion
  1454. await updateGlobalState("mode", defaultModeSlug)
  1455. await provider.postStateToWebview()
  1456. }
  1457. break
  1458. case "exportMode":
  1459. if (message.slug) {
  1460. try {
  1461. // Get custom mode prompts to check if built-in mode has been customized
  1462. const customModePrompts = getGlobalState("customModePrompts") || {}
  1463. const customPrompt = customModePrompts[message.slug]
  1464. // Export the mode with any customizations merged directly
  1465. const result = await provider.customModesManager.exportModeWithRules(message.slug, customPrompt)
  1466. if (result.success && result.yaml) {
  1467. // Get last used directory for export
  1468. const lastExportPath = getGlobalState("lastModeExportPath")
  1469. let defaultUri: vscode.Uri
  1470. if (lastExportPath) {
  1471. // Use the directory from the last export
  1472. const lastDir = path.dirname(lastExportPath)
  1473. defaultUri = vscode.Uri.file(path.join(lastDir, `${message.slug}-export.yaml`))
  1474. } else {
  1475. // Default to workspace or home directory
  1476. const workspaceFolders = vscode.workspace.workspaceFolders
  1477. if (workspaceFolders && workspaceFolders.length > 0) {
  1478. defaultUri = vscode.Uri.file(
  1479. path.join(workspaceFolders[0].uri.fsPath, `${message.slug}-export.yaml`),
  1480. )
  1481. } else {
  1482. defaultUri = vscode.Uri.file(`${message.slug}-export.yaml`)
  1483. }
  1484. }
  1485. // Show save dialog
  1486. const saveUri = await vscode.window.showSaveDialog({
  1487. defaultUri,
  1488. filters: {
  1489. "YAML files": ["yaml", "yml"],
  1490. },
  1491. title: "Save mode export",
  1492. })
  1493. if (saveUri && result.yaml) {
  1494. // Save the directory for next time
  1495. await updateGlobalState("lastModeExportPath", saveUri.fsPath)
  1496. // Write the file to the selected location
  1497. await fs.writeFile(saveUri.fsPath, result.yaml, "utf-8")
  1498. // Send success message to webview
  1499. provider.postMessageToWebview({
  1500. type: "exportModeResult",
  1501. success: true,
  1502. slug: message.slug,
  1503. })
  1504. // Show info message
  1505. vscode.window.showInformationMessage(t("common:info.mode_exported", { mode: message.slug }))
  1506. } else {
  1507. // User cancelled the save dialog
  1508. provider.postMessageToWebview({
  1509. type: "exportModeResult",
  1510. success: false,
  1511. error: "Export cancelled",
  1512. slug: message.slug,
  1513. })
  1514. }
  1515. } else {
  1516. // Send error message to webview
  1517. provider.postMessageToWebview({
  1518. type: "exportModeResult",
  1519. success: false,
  1520. error: result.error,
  1521. slug: message.slug,
  1522. })
  1523. }
  1524. } catch (error) {
  1525. const errorMessage = error instanceof Error ? error.message : String(error)
  1526. provider.log(`Failed to export mode ${message.slug}: ${errorMessage}`)
  1527. // Send error message to webview
  1528. provider.postMessageToWebview({
  1529. type: "exportModeResult",
  1530. success: false,
  1531. error: errorMessage,
  1532. slug: message.slug,
  1533. })
  1534. }
  1535. }
  1536. break
  1537. case "importMode":
  1538. try {
  1539. // Get last used directory for import
  1540. const lastImportPath = getGlobalState("lastModeImportPath")
  1541. let defaultUri: vscode.Uri | undefined
  1542. if (lastImportPath) {
  1543. // Use the directory from the last import
  1544. const lastDir = path.dirname(lastImportPath)
  1545. defaultUri = vscode.Uri.file(lastDir)
  1546. } else {
  1547. // Default to workspace or home directory
  1548. const workspaceFolders = vscode.workspace.workspaceFolders
  1549. if (workspaceFolders && workspaceFolders.length > 0) {
  1550. defaultUri = vscode.Uri.file(workspaceFolders[0].uri.fsPath)
  1551. }
  1552. }
  1553. // Show file picker to select YAML file
  1554. const fileUri = await vscode.window.showOpenDialog({
  1555. canSelectFiles: true,
  1556. canSelectFolders: false,
  1557. canSelectMany: false,
  1558. defaultUri,
  1559. filters: {
  1560. "YAML files": ["yaml", "yml"],
  1561. },
  1562. title: "Select mode export file to import",
  1563. })
  1564. if (fileUri && fileUri[0]) {
  1565. // Save the directory for next time
  1566. await updateGlobalState("lastModeImportPath", fileUri[0].fsPath)
  1567. // Read the file content
  1568. const yamlContent = await fs.readFile(fileUri[0].fsPath, "utf-8")
  1569. // Import the mode with the specified source level
  1570. const result = await provider.customModesManager.importModeWithRules(
  1571. yamlContent,
  1572. message.source || "project", // Default to project if not specified
  1573. )
  1574. if (result.success) {
  1575. // Update state after importing
  1576. const customModes = await provider.customModesManager.getCustomModes()
  1577. await updateGlobalState("customModes", customModes)
  1578. await provider.postStateToWebview()
  1579. // Send success message to webview
  1580. provider.postMessageToWebview({
  1581. type: "importModeResult",
  1582. success: true,
  1583. })
  1584. // Show success message
  1585. vscode.window.showInformationMessage(t("common:info.mode_imported"))
  1586. } else {
  1587. // Send error message to webview
  1588. provider.postMessageToWebview({
  1589. type: "importModeResult",
  1590. success: false,
  1591. error: result.error,
  1592. })
  1593. // Show error message
  1594. vscode.window.showErrorMessage(t("common:errors.mode_import_failed", { error: result.error }))
  1595. }
  1596. } else {
  1597. // User cancelled the file dialog - reset the importing state
  1598. provider.postMessageToWebview({
  1599. type: "importModeResult",
  1600. success: false,
  1601. error: "cancelled",
  1602. })
  1603. }
  1604. } catch (error) {
  1605. const errorMessage = error instanceof Error ? error.message : String(error)
  1606. provider.log(`Failed to import mode: ${errorMessage}`)
  1607. // Send error message to webview
  1608. provider.postMessageToWebview({
  1609. type: "importModeResult",
  1610. success: false,
  1611. error: errorMessage,
  1612. })
  1613. // Show error message
  1614. vscode.window.showErrorMessage(t("common:errors.mode_import_failed", { error: errorMessage }))
  1615. }
  1616. break
  1617. case "checkRulesDirectory":
  1618. if (message.slug) {
  1619. const hasContent = await provider.customModesManager.checkRulesDirectoryHasContent(message.slug)
  1620. provider.postMessageToWebview({
  1621. type: "checkRulesDirectoryResult",
  1622. slug: message.slug,
  1623. hasContent: hasContent,
  1624. })
  1625. }
  1626. break
  1627. case "humanRelayResponse":
  1628. if (message.requestId && message.text) {
  1629. vscode.commands.executeCommand(getCommand("handleHumanRelayResponse"), {
  1630. requestId: message.requestId,
  1631. text: message.text,
  1632. cancelled: false,
  1633. })
  1634. }
  1635. break
  1636. case "humanRelayCancel":
  1637. if (message.requestId) {
  1638. vscode.commands.executeCommand(getCommand("handleHumanRelayResponse"), {
  1639. requestId: message.requestId,
  1640. cancelled: true,
  1641. })
  1642. }
  1643. break
  1644. case "telemetrySetting": {
  1645. const telemetrySetting = message.text as TelemetrySetting
  1646. await updateGlobalState("telemetrySetting", telemetrySetting)
  1647. const isOptedIn = telemetrySetting === "enabled"
  1648. TelemetryService.instance.updateTelemetryState(isOptedIn)
  1649. await provider.postStateToWebview()
  1650. break
  1651. }
  1652. case "accountButtonClicked": {
  1653. // Navigate to the account tab.
  1654. provider.postMessageToWebview({ type: "action", action: "accountButtonClicked" })
  1655. break
  1656. }
  1657. case "rooCloudSignIn": {
  1658. try {
  1659. TelemetryService.instance.captureEvent(TelemetryEventName.AUTHENTICATION_INITIATED)
  1660. await CloudService.instance.login()
  1661. } catch (error) {
  1662. provider.log(`AuthService#login failed: ${error}`)
  1663. vscode.window.showErrorMessage("Sign in failed.")
  1664. }
  1665. break
  1666. }
  1667. case "rooCloudSignOut": {
  1668. try {
  1669. await CloudService.instance.logout()
  1670. await provider.postStateToWebview()
  1671. provider.postMessageToWebview({ type: "authenticatedUser", userInfo: undefined })
  1672. } catch (error) {
  1673. provider.log(`AuthService#logout failed: ${error}`)
  1674. vscode.window.showErrorMessage("Sign out failed.")
  1675. }
  1676. break
  1677. }
  1678. case "saveCodeIndexSettingsAtomic": {
  1679. if (!message.codeIndexSettings) {
  1680. break
  1681. }
  1682. const settings = message.codeIndexSettings
  1683. try {
  1684. // Save global state settings atomically (without codebaseIndexEnabled which is now in global settings)
  1685. const currentConfig = getGlobalState("codebaseIndexConfig") || {}
  1686. const globalStateConfig = {
  1687. ...currentConfig,
  1688. codebaseIndexQdrantUrl: settings.codebaseIndexQdrantUrl,
  1689. codebaseIndexEmbedderProvider: settings.codebaseIndexEmbedderProvider,
  1690. codebaseIndexEmbedderBaseUrl: settings.codebaseIndexEmbedderBaseUrl,
  1691. codebaseIndexEmbedderModelId: settings.codebaseIndexEmbedderModelId,
  1692. codebaseIndexOpenAiCompatibleBaseUrl: settings.codebaseIndexOpenAiCompatibleBaseUrl,
  1693. codebaseIndexOpenAiCompatibleModelDimension: settings.codebaseIndexOpenAiCompatibleModelDimension,
  1694. codebaseIndexSearchMaxResults: settings.codebaseIndexSearchMaxResults,
  1695. codebaseIndexSearchMinScore: settings.codebaseIndexSearchMinScore,
  1696. }
  1697. // Save global state first
  1698. await updateGlobalState("codebaseIndexConfig", globalStateConfig)
  1699. // Save secrets directly using context proxy
  1700. if (settings.codeIndexOpenAiKey !== undefined) {
  1701. await provider.contextProxy.storeSecret("codeIndexOpenAiKey", settings.codeIndexOpenAiKey)
  1702. }
  1703. if (settings.codeIndexQdrantApiKey !== undefined) {
  1704. await provider.contextProxy.storeSecret("codeIndexQdrantApiKey", settings.codeIndexQdrantApiKey)
  1705. }
  1706. if (settings.codebaseIndexOpenAiCompatibleApiKey !== undefined) {
  1707. await provider.contextProxy.storeSecret(
  1708. "codebaseIndexOpenAiCompatibleApiKey",
  1709. settings.codebaseIndexOpenAiCompatibleApiKey,
  1710. )
  1711. }
  1712. if (settings.codebaseIndexGeminiApiKey !== undefined) {
  1713. await provider.contextProxy.storeSecret(
  1714. "codebaseIndexGeminiApiKey",
  1715. settings.codebaseIndexGeminiApiKey,
  1716. )
  1717. }
  1718. // Verify secrets are actually stored
  1719. const storedOpenAiKey = provider.contextProxy.getSecret("codeIndexOpenAiKey")
  1720. // Notify code index manager of changes
  1721. if (provider.codeIndexManager) {
  1722. await provider.codeIndexManager.handleSettingsChange()
  1723. // Auto-start indexing if now enabled and configured
  1724. if (provider.codeIndexManager.isFeatureEnabled && provider.codeIndexManager.isFeatureConfigured) {
  1725. if (!provider.codeIndexManager.isInitialized) {
  1726. await provider.codeIndexManager.initialize(provider.contextProxy)
  1727. }
  1728. provider.codeIndexManager.startIndexing()
  1729. }
  1730. }
  1731. // Send success response
  1732. await provider.postMessageToWebview({
  1733. type: "codeIndexSettingsSaved",
  1734. success: true,
  1735. settings: globalStateConfig,
  1736. })
  1737. // Update webview state
  1738. await provider.postStateToWebview()
  1739. } catch (error) {
  1740. provider.log(`Error saving code index settings: ${error.message || error}`)
  1741. await provider.postMessageToWebview({
  1742. type: "codeIndexSettingsSaved",
  1743. success: false,
  1744. error: error.message || "Failed to save settings",
  1745. })
  1746. }
  1747. break
  1748. }
  1749. case "requestIndexingStatus": {
  1750. const status = provider.codeIndexManager!.getCurrentStatus()
  1751. provider.postMessageToWebview({
  1752. type: "indexingStatusUpdate",
  1753. values: status,
  1754. })
  1755. break
  1756. }
  1757. case "requestCodeIndexSecretStatus": {
  1758. // Check if secrets are set using the VSCode context directly for async access
  1759. const hasOpenAiKey = !!(await provider.context.secrets.get("codeIndexOpenAiKey"))
  1760. const hasQdrantApiKey = !!(await provider.context.secrets.get("codeIndexQdrantApiKey"))
  1761. const hasOpenAiCompatibleApiKey = !!(await provider.context.secrets.get(
  1762. "codebaseIndexOpenAiCompatibleApiKey",
  1763. ))
  1764. const hasGeminiApiKey = !!(await provider.context.secrets.get("codebaseIndexGeminiApiKey"))
  1765. provider.postMessageToWebview({
  1766. type: "codeIndexSecretStatus",
  1767. values: {
  1768. hasOpenAiKey,
  1769. hasQdrantApiKey,
  1770. hasOpenAiCompatibleApiKey,
  1771. hasGeminiApiKey,
  1772. },
  1773. })
  1774. break
  1775. }
  1776. case "startIndexing": {
  1777. try {
  1778. const manager = provider.codeIndexManager!
  1779. if (manager.isFeatureEnabled && manager.isFeatureConfigured) {
  1780. if (!manager.isInitialized) {
  1781. await manager.initialize(provider.contextProxy)
  1782. }
  1783. manager.startIndexing()
  1784. }
  1785. } catch (error) {
  1786. provider.log(`Error starting indexing: ${error instanceof Error ? error.message : String(error)}`)
  1787. }
  1788. break
  1789. }
  1790. case "clearIndexData": {
  1791. try {
  1792. const manager = provider.codeIndexManager!
  1793. await manager.clearIndexData()
  1794. provider.postMessageToWebview({ type: "indexCleared", values: { success: true } })
  1795. } catch (error) {
  1796. provider.log(`Error clearing index data: ${error instanceof Error ? error.message : String(error)}`)
  1797. provider.postMessageToWebview({
  1798. type: "indexCleared",
  1799. values: {
  1800. success: false,
  1801. error: error instanceof Error ? error.message : String(error),
  1802. },
  1803. })
  1804. }
  1805. break
  1806. }
  1807. case "focusPanelRequest": {
  1808. // Execute the focusPanel command to focus the WebView
  1809. await vscode.commands.executeCommand(getCommand("focusPanel"))
  1810. break
  1811. }
  1812. case "filterMarketplaceItems": {
  1813. if (marketplaceManager && message.filters) {
  1814. try {
  1815. await marketplaceManager.updateWithFilteredItems({
  1816. type: message.filters.type as MarketplaceItemType | undefined,
  1817. search: message.filters.search,
  1818. tags: message.filters.tags,
  1819. })
  1820. await provider.postStateToWebview()
  1821. } catch (error) {
  1822. console.error("Marketplace: Error filtering items:", error)
  1823. vscode.window.showErrorMessage("Failed to filter marketplace items")
  1824. }
  1825. }
  1826. break
  1827. }
  1828. case "fetchMarketplaceData": {
  1829. // Fetch marketplace data on demand
  1830. await provider.fetchMarketplaceData()
  1831. break
  1832. }
  1833. case "installMarketplaceItem": {
  1834. if (marketplaceManager && message.mpItem && message.mpInstallOptions) {
  1835. try {
  1836. const configFilePath = await marketplaceManager.installMarketplaceItem(
  1837. message.mpItem,
  1838. message.mpInstallOptions,
  1839. )
  1840. await provider.postStateToWebview()
  1841. console.log(`Marketplace item installed and config file opened: ${configFilePath}`)
  1842. // Send success message to webview
  1843. provider.postMessageToWebview({
  1844. type: "marketplaceInstallResult",
  1845. success: true,
  1846. slug: message.mpItem.id,
  1847. })
  1848. } catch (error) {
  1849. console.error(`Error installing marketplace item: ${error}`)
  1850. // Send error message to webview
  1851. provider.postMessageToWebview({
  1852. type: "marketplaceInstallResult",
  1853. success: false,
  1854. error: error instanceof Error ? error.message : String(error),
  1855. slug: message.mpItem.id,
  1856. })
  1857. }
  1858. }
  1859. break
  1860. }
  1861. case "removeInstalledMarketplaceItem": {
  1862. if (marketplaceManager && message.mpItem && message.mpInstallOptions) {
  1863. try {
  1864. await marketplaceManager.removeInstalledMarketplaceItem(message.mpItem, message.mpInstallOptions)
  1865. await provider.postStateToWebview()
  1866. } catch (error) {
  1867. console.error(`Error removing marketplace item: ${error}`)
  1868. }
  1869. }
  1870. break
  1871. }
  1872. case "installMarketplaceItemWithParameters": {
  1873. if (marketplaceManager && message.payload && "item" in message.payload && "parameters" in message.payload) {
  1874. try {
  1875. const configFilePath = await marketplaceManager.installMarketplaceItem(message.payload.item, {
  1876. parameters: message.payload.parameters,
  1877. })
  1878. await provider.postStateToWebview()
  1879. console.log(`Marketplace item with parameters installed and config file opened: ${configFilePath}`)
  1880. } catch (error) {
  1881. console.error(`Error installing marketplace item with parameters: ${error}`)
  1882. vscode.window.showErrorMessage(
  1883. `Failed to install marketplace item: ${error instanceof Error ? error.message : String(error)}`,
  1884. )
  1885. }
  1886. }
  1887. break
  1888. }
  1889. case "switchTab": {
  1890. if (message.tab) {
  1891. // Capture tab shown event for all switchTab messages (which are user-initiated)
  1892. if (TelemetryService.hasInstance()) {
  1893. TelemetryService.instance.captureTabShown(message.tab)
  1894. }
  1895. await provider.postMessageToWebview({ type: "action", action: "switchTab", tab: message.tab })
  1896. }
  1897. break
  1898. }
  1899. }
  1900. }