webviewMessageHandler.ts 104 KB

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