webviewMessageHandler.ts 102 KB

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