webviewMessageHandler.ts 117 KB

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