webviewMessageHandler.ts 84 KB

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