webviewMessageHandler.ts 109 KB

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