webviewMessageHandler.ts 115 KB

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