| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341 |
- import { safeWriteJson } from "../../utils/safeWriteJson"
- import * as path from "path"
- import * as os from "os"
- import * as fs from "fs/promises"
- import { getRooDirectoriesForCwd } from "../../services/roo-config/index.js"
- import pWaitFor from "p-wait-for"
- import * as vscode from "vscode"
- import {
- type Language,
- type GlobalState,
- type ClineMessage,
- type TelemetrySetting,
- type UserSettingsConfig,
- type ModelRecord,
- type WebviewMessage,
- type EditQueuedMessagePayload,
- TelemetryEventName,
- RooCodeSettings,
- ExperimentId,
- checkoutDiffPayloadSchema,
- checkoutRestorePayloadSchema,
- } from "@roo-code/types"
- import { customToolRegistry } from "@roo-code/core"
- import { CloudService } from "@roo-code/cloud"
- import { TelemetryService } from "@roo-code/telemetry"
- import { type ApiMessage } from "../task-persistence/apiMessages"
- import { saveTaskMessages } from "../task-persistence"
- import { ClineProvider } from "./ClineProvider"
- import { BrowserSessionPanelManager } from "./BrowserSessionPanelManager"
- import { handleCheckpointRestoreOperation } from "./checkpointRestoreHandler"
- import { generateErrorDiagnostics } from "./diagnosticsHandler"
- import { changeLanguage, t } from "../../i18n"
- import { Package } from "../../shared/package"
- import { type RouterName, toRouterName } from "../../shared/api"
- import { MessageEnhancer } from "./messageEnhancer"
- import { checkExistKey } from "../../shared/checkExistApiConfig"
- import { experimentDefault } from "../../shared/experiments"
- import { Terminal } from "../../integrations/terminal/Terminal"
- import { openFile } from "../../integrations/misc/open-file"
- import { openImage, saveImage } from "../../integrations/misc/image-handler"
- import { selectImages } from "../../integrations/misc/process-images"
- import { getTheme } from "../../integrations/theme/getTheme"
- import { discoverChromeHostUrl, tryChromeHostUrl } from "../../services/browser/browserDiscovery"
- import { searchWorkspaceFiles } from "../../services/search/file-search"
- import { fileExistsAtPath } from "../../utils/fs"
- import { playTts, setTtsEnabled, setTtsSpeed, stopTts } from "../../utils/tts"
- import { searchCommits } from "../../utils/git"
- import { exportSettings, importSettingsWithFeedback } from "../config/importExport"
- import { getOpenAiModels } from "../../api/providers/openai"
- import { getVsCodeLmModels } from "../../api/providers/vscode-lm"
- import { openMention } from "../mentions"
- import { resolveImageMentions } from "../mentions/resolveImageMentions"
- import { RooIgnoreController } from "../ignore/RooIgnoreController"
- import { getWorkspacePath } from "../../utils/path"
- import { Mode, defaultModeSlug } from "../../shared/modes"
- import { getModels, flushModels } from "../../api/providers/fetchers/modelCache"
- import { GetModelsOptions } from "../../shared/api"
- import { generateSystemPrompt } from "./generateSystemPrompt"
- import { getCommand } from "../../utils/commands"
- const ALLOWED_VSCODE_SETTINGS = new Set(["terminal.integrated.inheritEnv"])
- import { MarketplaceManager, MarketplaceItemType } from "../../services/marketplace"
- import { setPendingTodoList } from "../tools/UpdateTodoListTool"
- export const webviewMessageHandler = async (
- provider: ClineProvider,
- message: WebviewMessage,
- marketplaceManager?: MarketplaceManager,
- ) => {
- // Utility functions provided for concise get/update of global state via contextProxy API.
- const getGlobalState = <K extends keyof GlobalState>(key: K) => provider.contextProxy.getValue(key)
- const updateGlobalState = async <K extends keyof GlobalState>(key: K, value: GlobalState[K]) =>
- await provider.contextProxy.setValue(key, value)
- const getCurrentCwd = () => {
- return provider.getCurrentTask()?.cwd || provider.cwd
- }
- /**
- * Resolves image file mentions in incoming messages.
- * Matches read_file behavior: respects size limits and model capabilities.
- */
- const resolveIncomingImages = async (payload: { text?: string; images?: string[] }) => {
- const text = payload.text ?? ""
- const images = payload.images
- const currentTask = provider.getCurrentTask()
- const state = await provider.getState()
- const resolved = await resolveImageMentions({
- text,
- images,
- cwd: getCurrentCwd(),
- rooIgnoreController: currentTask?.rooIgnoreController,
- maxImageFileSize: state.maxImageFileSize,
- maxTotalImageSize: state.maxTotalImageSize,
- })
- return resolved
- }
- /**
- * Shared utility to find message indices based on timestamp.
- * When multiple messages share the same timestamp (e.g., after condense),
- * this function prefers non-summary messages to ensure user operations
- * target the intended message rather than the summary.
- */
- const findMessageIndices = (messageTs: number, currentCline: any) => {
- // Find the exact message by timestamp, not the first one after a cutoff
- const messageIndex = currentCline.clineMessages.findIndex((msg: ClineMessage) => msg.ts === messageTs)
- // Find all matching API messages by timestamp
- const allApiMatches = currentCline.apiConversationHistory
- .map((msg: ApiMessage, idx: number) => ({ msg, idx }))
- .filter(({ msg }: { msg: ApiMessage }) => msg.ts === messageTs)
- // Prefer non-summary message if multiple matches exist (handles timestamp collision after condense)
- const preferred = allApiMatches.find(({ msg }: { msg: ApiMessage }) => !msg.isSummary) || allApiMatches[0]
- const apiConversationHistoryIndex = preferred?.idx ?? -1
- return { messageIndex, apiConversationHistoryIndex }
- }
- /**
- * Fallback: find first API history index at or after a timestamp.
- * Used when the exact user message isn't present in apiConversationHistory (e.g., after condense).
- */
- const findFirstApiIndexAtOrAfter = (ts: number, currentCline: any) => {
- if (typeof ts !== "number") return -1
- return currentCline.apiConversationHistory.findIndex(
- (msg: ApiMessage) => typeof msg?.ts === "number" && (msg.ts as number) >= ts,
- )
- }
- /**
- * Handles message deletion operations with user confirmation
- */
- const handleDeleteOperation = async (messageTs: number): Promise<void> => {
- // Check if there's a checkpoint before this message
- const currentCline = provider.getCurrentTask()
- let hasCheckpoint = false
- if (!currentCline) {
- await vscode.window.showErrorMessage(t("common:errors.message.no_active_task_to_delete"))
- return
- }
- const { messageIndex } = findMessageIndices(messageTs, currentCline)
- if (messageIndex !== -1) {
- // Find the last checkpoint before this message
- const checkpoints = currentCline.clineMessages.filter(
- (msg) => msg.say === "checkpoint_saved" && msg.ts > messageTs,
- )
- hasCheckpoint = checkpoints.length > 0
- }
- // Send message to webview to show delete confirmation dialog
- await provider.postMessageToWebview({
- type: "showDeleteMessageDialog",
- messageTs,
- hasCheckpoint,
- })
- }
- /**
- * Handles confirmed message deletion from webview dialog
- */
- const handleDeleteMessageConfirm = async (messageTs: number, restoreCheckpoint?: boolean): Promise<void> => {
- const currentCline = provider.getCurrentTask()
- if (!currentCline) {
- console.error("[handleDeleteMessageConfirm] No current cline available")
- return
- }
- const { messageIndex, apiConversationHistoryIndex } = findMessageIndices(messageTs, currentCline)
- // Determine API truncation index with timestamp fallback if exact match not found
- let apiIndexToUse = apiConversationHistoryIndex
- const tsThreshold = currentCline.clineMessages[messageIndex]?.ts
- if (apiIndexToUse === -1 && typeof tsThreshold === "number") {
- apiIndexToUse = findFirstApiIndexAtOrAfter(tsThreshold, currentCline)
- }
- if (messageIndex === -1) {
- await vscode.window.showErrorMessage(t("common:errors.message.message_not_found", { messageTs }))
- return
- }
- try {
- const targetMessage = currentCline.clineMessages[messageIndex]
- // If checkpoint restoration is requested, find and restore to the last checkpoint before this message
- if (restoreCheckpoint) {
- // Find the last checkpoint before this message
- const checkpoints = currentCline.clineMessages.filter(
- (msg) => msg.say === "checkpoint_saved" && msg.ts > messageTs,
- )
- const nextCheckpoint = checkpoints[0]
- if (nextCheckpoint && nextCheckpoint.text) {
- await handleCheckpointRestoreOperation({
- provider,
- currentCline,
- messageTs: targetMessage.ts!,
- messageIndex,
- checkpoint: { hash: nextCheckpoint.text },
- operation: "delete",
- })
- } else {
- // No checkpoint found before this message
- console.log("[handleDeleteMessageConfirm] No checkpoint found before message")
- vscode.window.showWarningMessage("No checkpoint found before this message")
- }
- } else {
- // For non-checkpoint deletes, preserve checkpoint associations for remaining messages
- // Store checkpoints from messages that will be preserved
- const preservedCheckpoints = new Map<number, any>()
- for (let i = 0; i < messageIndex; i++) {
- const msg = currentCline.clineMessages[i]
- if (msg?.checkpoint && msg.ts) {
- preservedCheckpoints.set(msg.ts, msg.checkpoint)
- }
- }
- // Delete this message and all subsequent messages using MessageManager
- await currentCline.messageManager.rewindToTimestamp(targetMessage.ts!, { includeTargetMessage: false })
- // Restore checkpoint associations for preserved messages
- for (const [ts, checkpoint] of preservedCheckpoints) {
- const msgIndex = currentCline.clineMessages.findIndex((msg) => msg.ts === ts)
- if (msgIndex !== -1) {
- currentCline.clineMessages[msgIndex].checkpoint = checkpoint
- }
- }
- // Save the updated messages with restored checkpoints
- await saveTaskMessages({
- messages: currentCline.clineMessages,
- taskId: currentCline.taskId,
- globalStoragePath: provider.contextProxy.globalStorageUri.fsPath,
- })
- // Update the UI to reflect the deletion
- await provider.postStateToWebview()
- }
- } catch (error) {
- console.error("Error in delete message:", error)
- vscode.window.showErrorMessage(
- t("common:errors.message.error_deleting_message", {
- error: error instanceof Error ? error.message : String(error),
- }),
- )
- }
- }
- /**
- * Handles message editing operations with user confirmation
- */
- const handleEditOperation = async (messageTs: number, editedContent: string, images?: string[]): Promise<void> => {
- // Check if there's a checkpoint before this message
- const currentCline = provider.getCurrentTask()
- let hasCheckpoint = false
- if (currentCline) {
- const { messageIndex } = findMessageIndices(messageTs, currentCline)
- if (messageIndex !== -1) {
- // Find the last checkpoint before this message
- const checkpoints = currentCline.clineMessages.filter(
- (msg) => msg.say === "checkpoint_saved" && msg.ts > messageTs,
- )
- hasCheckpoint = checkpoints.length > 0
- } else {
- console.log("[webviewMessageHandler] Edit - Message not found in clineMessages!")
- }
- } else {
- console.log("[webviewMessageHandler] Edit - No currentCline available!")
- }
- // Send message to webview to show edit confirmation dialog
- await provider.postMessageToWebview({
- type: "showEditMessageDialog",
- messageTs,
- text: editedContent,
- hasCheckpoint,
- images,
- })
- }
- /**
- * Handles confirmed message editing from webview dialog
- */
- const handleEditMessageConfirm = async (
- messageTs: number,
- editedContent: string,
- restoreCheckpoint?: boolean,
- images?: string[],
- ): Promise<void> => {
- const currentCline = provider.getCurrentTask()
- if (!currentCline) {
- console.error("[handleEditMessageConfirm] No current cline available")
- return
- }
- // Use findMessageIndices to find messages based on timestamp
- const { messageIndex, apiConversationHistoryIndex } = findMessageIndices(messageTs, currentCline)
- if (messageIndex === -1) {
- const errorMessage = t("common:errors.message.message_not_found", { messageTs })
- console.error("[handleEditMessageConfirm]", errorMessage)
- await vscode.window.showErrorMessage(errorMessage)
- return
- }
- try {
- const targetMessage = currentCline.clineMessages[messageIndex]
- // If checkpoint restoration is requested, find and restore to the last checkpoint before this message
- if (restoreCheckpoint) {
- // Find the last checkpoint before this message
- const checkpoints = currentCline.clineMessages.filter(
- (msg) => msg.say === "checkpoint_saved" && msg.ts > messageTs,
- )
- const nextCheckpoint = checkpoints[0]
- if (nextCheckpoint && nextCheckpoint.text) {
- await handleCheckpointRestoreOperation({
- provider,
- currentCline,
- messageTs: targetMessage.ts!,
- messageIndex,
- checkpoint: { hash: nextCheckpoint.text },
- operation: "edit",
- editData: {
- editedContent,
- images,
- apiConversationHistoryIndex,
- },
- })
- // The task will be cancelled and reinitialized by checkpointRestore
- // The pending edit will be processed in the reinitialized task
- return
- } else {
- // No checkpoint found before this message
- console.log("[handleEditMessageConfirm] No checkpoint found before message")
- vscode.window.showWarningMessage("No checkpoint found before this message")
- // Continue with non-checkpoint edit
- }
- }
- // For non-checkpoint edits, remove the ORIGINAL user message being edited and all subsequent messages
- // Determine the correct starting index to delete from (prefer the last preceding user_feedback message)
- let deleteFromMessageIndex = messageIndex
- let deleteFromApiIndex = apiConversationHistoryIndex
- // Find the nearest preceding user message to ensure we replace the original, not just the assistant reply
- for (let i = messageIndex; i >= 0; i--) {
- const m = currentCline.clineMessages[i]
- if (m?.say === "user_feedback") {
- deleteFromMessageIndex = i
- // Align API history truncation to the same user message timestamp if present
- const userTs = m.ts
- if (typeof userTs === "number") {
- const apiIdx = currentCline.apiConversationHistory.findIndex(
- (am: ApiMessage) => am.ts === userTs,
- )
- if (apiIdx !== -1) {
- deleteFromApiIndex = apiIdx
- }
- }
- break
- }
- }
- // Timestamp fallback for API history when exact user message isn't present
- if (deleteFromApiIndex === -1) {
- const tsThresholdForEdit = currentCline.clineMessages[deleteFromMessageIndex]?.ts
- if (typeof tsThresholdForEdit === "number") {
- deleteFromApiIndex = findFirstApiIndexAtOrAfter(tsThresholdForEdit, currentCline)
- }
- }
- // Store checkpoints from messages that will be preserved
- const preservedCheckpoints = new Map<number, any>()
- for (let i = 0; i < deleteFromMessageIndex; i++) {
- const msg = currentCline.clineMessages[i]
- if (msg?.checkpoint && msg.ts) {
- preservedCheckpoints.set(msg.ts, msg.checkpoint)
- }
- }
- // Delete the original (user) message and all subsequent messages using MessageManager
- const rewindTs = currentCline.clineMessages[deleteFromMessageIndex]?.ts
- if (rewindTs) {
- await currentCline.messageManager.rewindToTimestamp(rewindTs, { includeTargetMessage: false })
- }
- // Restore checkpoint associations for preserved messages
- for (const [ts, checkpoint] of preservedCheckpoints) {
- const msgIndex = currentCline.clineMessages.findIndex((msg) => msg.ts === ts)
- if (msgIndex !== -1) {
- currentCline.clineMessages[msgIndex].checkpoint = checkpoint
- }
- }
- // Save the updated messages with restored checkpoints
- await saveTaskMessages({
- messages: currentCline.clineMessages,
- taskId: currentCline.taskId,
- globalStoragePath: provider.contextProxy.globalStorageUri.fsPath,
- })
- // Update the UI to reflect the deletion
- await provider.postStateToWebview()
- await currentCline.submitUserMessage(editedContent, images)
- } catch (error) {
- console.error("Error in edit message:", error)
- vscode.window.showErrorMessage(
- t("common:errors.message.error_editing_message", {
- error: error instanceof Error ? error.message : String(error),
- }),
- )
- }
- }
- /**
- * Handles message modification operations (delete or edit) with confirmation dialog
- * @param messageTs Timestamp of the message to operate on
- * @param operation Type of operation ('delete' or 'edit')
- * @param editedContent New content for edit operations
- * @returns Promise<void>
- */
- const handleMessageModificationsOperation = async (
- messageTs: number,
- operation: "delete" | "edit",
- editedContent?: string,
- images?: string[],
- ): Promise<void> => {
- if (operation === "delete") {
- await handleDeleteOperation(messageTs)
- } else if (operation === "edit" && editedContent) {
- await handleEditOperation(messageTs, editedContent, images)
- }
- }
- switch (message.type) {
- case "webviewDidLaunch":
- // Load custom modes first
- const customModes = await provider.customModesManager.getCustomModes()
- await updateGlobalState("customModes", customModes)
- provider.postStateToWebview()
- provider.workspaceTracker?.initializeFilePaths() // Don't await.
- getTheme().then((theme) => provider.postMessageToWebview({ type: "theme", text: JSON.stringify(theme) }))
- // If MCP Hub is already initialized, update the webview with
- // current server list.
- const mcpHub = provider.getMcpHub()
- if (mcpHub) {
- provider.postMessageToWebview({ type: "mcpServers", mcpServers: mcpHub.getAllServers() })
- }
- provider.providerSettingsManager
- .listConfig()
- .then(async (listApiConfig) => {
- if (!listApiConfig) {
- return
- }
- if (listApiConfig.length === 1) {
- // Check if first time init then sync with exist config.
- if (!checkExistKey(listApiConfig[0])) {
- const { apiConfiguration } = await provider.getState()
- await provider.providerSettingsManager.saveConfig(
- listApiConfig[0].name ?? "default",
- apiConfiguration,
- )
- listApiConfig[0].apiProvider = apiConfiguration.apiProvider
- }
- }
- const currentConfigName = getGlobalState("currentApiConfigName")
- if (currentConfigName) {
- if (!(await provider.providerSettingsManager.hasConfig(currentConfigName))) {
- // Current config name not valid, get first config in list.
- const name = listApiConfig[0]?.name
- await updateGlobalState("currentApiConfigName", name)
- if (name) {
- await provider.activateProviderProfile({ name })
- return
- }
- }
- }
- await Promise.all([
- await updateGlobalState("listApiConfigMeta", listApiConfig),
- await provider.postMessageToWebview({ type: "listApiConfig", listApiConfig }),
- ])
- })
- .catch((error) =>
- provider.log(
- `Error list api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
- ),
- )
- // Enable telemetry by default (when unset) or when explicitly enabled
- provider.getStateToPostToWebview().then((state) => {
- const { telemetrySetting } = state
- const isOptedIn = telemetrySetting !== "disabled"
- TelemetryService.instance.updateTelemetryState(isOptedIn)
- })
- provider.isViewLaunched = true
- break
- case "newTask":
- // Initializing new instance of Cline will make sure that any
- // agentically running promises in old instance don't affect our new
- // task. This essentially creates a fresh slate for the new task.
- try {
- const resolved = await resolveIncomingImages({ text: message.text, images: message.images })
- await provider.createTask(resolved.text, resolved.images)
- // Task created successfully - notify the UI to reset
- await provider.postMessageToWebview({ type: "invoke", invoke: "newChat" })
- } catch (error) {
- // For all errors, reset the UI and show error
- await provider.postMessageToWebview({ type: "invoke", invoke: "newChat" })
- // Show error to user
- vscode.window.showErrorMessage(
- `Failed to create task: ${error instanceof Error ? error.message : String(error)}`,
- )
- }
- break
- case "customInstructions":
- await provider.updateCustomInstructions(message.text)
- break
- case "askResponse":
- {
- const resolved = await resolveIncomingImages({ text: message.text, images: message.images })
- provider
- .getCurrentTask()
- ?.handleWebviewAskResponse(message.askResponse!, resolved.text, resolved.images)
- }
- break
- case "updateSettings":
- if (message.updatedSettings) {
- for (const [key, value] of Object.entries(message.updatedSettings)) {
- let newValue = value
- if (key === "language") {
- newValue = value ?? "en"
- changeLanguage(newValue as Language)
- } else if (key === "allowedCommands") {
- const commands = value ?? []
- newValue = Array.isArray(commands)
- ? commands.filter((cmd) => typeof cmd === "string" && cmd.trim().length > 0)
- : []
- await vscode.workspace
- .getConfiguration(Package.name)
- .update("allowedCommands", newValue, vscode.ConfigurationTarget.Global)
- } else if (key === "deniedCommands") {
- const commands = value ?? []
- newValue = Array.isArray(commands)
- ? commands.filter((cmd) => typeof cmd === "string" && cmd.trim().length > 0)
- : []
- await vscode.workspace
- .getConfiguration(Package.name)
- .update("deniedCommands", newValue, vscode.ConfigurationTarget.Global)
- } else if (key === "ttsEnabled") {
- newValue = value ?? true
- setTtsEnabled(newValue as boolean)
- } else if (key === "ttsSpeed") {
- newValue = value ?? 1.0
- setTtsSpeed(newValue as number)
- } else if (key === "terminalShellIntegrationTimeout") {
- if (value !== undefined) {
- Terminal.setShellIntegrationTimeout(value as number)
- }
- } else if (key === "terminalShellIntegrationDisabled") {
- if (value !== undefined) {
- Terminal.setShellIntegrationDisabled(value as boolean)
- }
- } else if (key === "terminalCommandDelay") {
- if (value !== undefined) {
- Terminal.setCommandDelay(value as number)
- }
- } else if (key === "terminalPowershellCounter") {
- if (value !== undefined) {
- Terminal.setPowershellCounter(value as boolean)
- }
- } else if (key === "terminalZshClearEolMark") {
- if (value !== undefined) {
- Terminal.setTerminalZshClearEolMark(value as boolean)
- }
- } else if (key === "terminalZshOhMy") {
- if (value !== undefined) {
- Terminal.setTerminalZshOhMy(value as boolean)
- }
- } else if (key === "terminalZshP10k") {
- if (value !== undefined) {
- Terminal.setTerminalZshP10k(value as boolean)
- }
- } else if (key === "terminalZdotdir") {
- if (value !== undefined) {
- Terminal.setTerminalZdotdir(value as boolean)
- }
- } else if (key === "terminalCompressProgressBar") {
- if (value !== undefined) {
- Terminal.setCompressProgressBar(value as boolean)
- }
- } else if (key === "mcpEnabled") {
- newValue = value ?? true
- const mcpHub = provider.getMcpHub()
- if (mcpHub) {
- await mcpHub.handleMcpEnabledChange(newValue as boolean)
- }
- } else if (key === "experiments") {
- if (!value) {
- continue
- }
- newValue = {
- ...(getGlobalState("experiments") ?? experimentDefault),
- ...(value as Record<ExperimentId, boolean>),
- }
- } else if (key === "customSupportPrompts") {
- if (!value) {
- continue
- }
- }
- await provider.contextProxy.setValue(key as keyof RooCodeSettings, newValue)
- }
- await provider.postStateToWebview()
- }
- break
- case "terminalOperation":
- if (message.terminalOperation) {
- provider.getCurrentTask()?.handleTerminalOperation(message.terminalOperation)
- }
- break
- case "clearTask":
- // Clear task resets the current session. Delegation flows are
- // handled via metadata; parent resumption occurs through
- // reopenParentFromDelegation, not via finishSubTask.
- await provider.clearTask()
- await provider.postStateToWebview()
- break
- case "didShowAnnouncement":
- await updateGlobalState("lastShownAnnouncementId", provider.latestAnnouncementId)
- await provider.postStateToWebview()
- break
- case "selectImages":
- const images = await selectImages()
- await provider.postMessageToWebview({
- type: "selectedImages",
- images,
- context: message.context,
- messageTs: message.messageTs,
- })
- break
- case "exportCurrentTask":
- const currentTaskId = provider.getCurrentTask()?.taskId
- if (currentTaskId) {
- provider.exportTaskWithId(currentTaskId)
- }
- break
- case "shareCurrentTask":
- const shareTaskId = provider.getCurrentTask()?.taskId
- const clineMessages = provider.getCurrentTask()?.clineMessages
- if (!shareTaskId) {
- vscode.window.showErrorMessage(t("common:errors.share_no_active_task"))
- break
- }
- try {
- const visibility = message.visibility || "organization"
- const result = await CloudService.instance.shareTask(shareTaskId, visibility, clineMessages)
- if (result.success && result.shareUrl) {
- // Show success notification
- const messageKey =
- visibility === "public"
- ? "common:info.public_share_link_copied"
- : "common:info.organization_share_link_copied"
- vscode.window.showInformationMessage(t(messageKey))
- // Send success feedback to webview for inline display
- await provider.postMessageToWebview({
- type: "shareTaskSuccess",
- visibility,
- text: result.shareUrl,
- })
- } else {
- // Handle error
- const errorMessage = result.error || "Failed to create share link"
- if (errorMessage.includes("Authentication")) {
- vscode.window.showErrorMessage(t("common:errors.share_auth_required"))
- } else if (errorMessage.includes("sharing is not enabled")) {
- vscode.window.showErrorMessage(t("common:errors.share_not_enabled"))
- } else if (errorMessage.includes("not found")) {
- vscode.window.showErrorMessage(t("common:errors.share_task_not_found"))
- } else {
- vscode.window.showErrorMessage(errorMessage)
- }
- }
- } catch (error) {
- provider.log(`[shareCurrentTask] Unexpected error: ${error}`)
- vscode.window.showErrorMessage(t("common:errors.share_task_failed"))
- }
- break
- case "showTaskWithId":
- provider.showTaskWithId(message.text!)
- break
- case "condenseTaskContextRequest":
- provider.condenseTaskContext(message.text!)
- break
- case "deleteTaskWithId":
- provider.deleteTaskWithId(message.text!)
- break
- case "deleteMultipleTasksWithIds": {
- const ids = message.ids
- if (Array.isArray(ids)) {
- // Process in batches of 20 (or another reasonable number)
- const batchSize = 20
- const results = []
- // Only log start and end of the operation
- console.log(`Batch deletion started: ${ids.length} tasks total`)
- for (let i = 0; i < ids.length; i += batchSize) {
- const batch = ids.slice(i, i + batchSize)
- const batchPromises = batch.map(async (id) => {
- try {
- await provider.deleteTaskWithId(id)
- return { id, success: true }
- } catch (error) {
- // Keep error logging for debugging purposes
- console.log(
- `Failed to delete task ${id}: ${error instanceof Error ? error.message : String(error)}`,
- )
- return { id, success: false }
- }
- })
- // Process each batch in parallel but wait for completion before starting the next batch
- const batchResults = await Promise.all(batchPromises)
- results.push(...batchResults)
- // Update the UI after each batch to show progress
- await provider.postStateToWebview()
- }
- // Log final results
- const successCount = results.filter((r) => r.success).length
- const failCount = results.length - successCount
- console.log(
- `Batch deletion completed: ${successCount}/${ids.length} tasks successful, ${failCount} tasks failed`,
- )
- }
- break
- }
- case "exportTaskWithId":
- provider.exportTaskWithId(message.text!)
- break
- case "importSettings": {
- await importSettingsWithFeedback({
- providerSettingsManager: provider.providerSettingsManager,
- contextProxy: provider.contextProxy,
- customModesManager: provider.customModesManager,
- provider: provider,
- })
- break
- }
- case "exportSettings":
- await exportSettings({
- providerSettingsManager: provider.providerSettingsManager,
- contextProxy: provider.contextProxy,
- })
- break
- case "resetState":
- await provider.resetState()
- break
- case "flushRouterModels":
- const routerNameFlush: RouterName = toRouterName(message.text)
- // Note: flushRouterModels is a generic flush without credentials
- // For providers that need credentials, use their specific handlers
- await flushModels({ provider: routerNameFlush } as GetModelsOptions, true)
- break
- case "requestRouterModels":
- const { apiConfiguration } = await provider.getState()
- // Optional single provider filter from webview
- const requestedProvider = message?.values?.provider
- const providerFilter = requestedProvider ? toRouterName(requestedProvider) : undefined
- // Optional refresh flag to flush cache before fetching (useful for providers requiring credentials)
- const shouldRefresh = message?.values?.refresh === true
- const routerModels: Record<RouterName, ModelRecord> = providerFilter
- ? ({} as Record<RouterName, ModelRecord>)
- : {
- openrouter: {},
- "vercel-ai-gateway": {},
- huggingface: {},
- litellm: {},
- deepinfra: {},
- "io-intelligence": {},
- requesty: {},
- unbound: {},
- ollama: {},
- lmstudio: {},
- roo: {},
- chutes: {},
- }
- const safeGetModels = async (options: GetModelsOptions): Promise<ModelRecord> => {
- try {
- return await getModels(options)
- } catch (error) {
- console.error(
- `Failed to fetch models in webviewMessageHandler requestRouterModels for ${options.provider}:`,
- error,
- )
- throw error // Re-throw to be caught by Promise.allSettled.
- }
- }
- // Base candidates (only those handled by this aggregate fetcher)
- const candidates: { key: RouterName; options: GetModelsOptions }[] = [
- { key: "openrouter", options: { provider: "openrouter" } },
- {
- key: "requesty",
- options: {
- provider: "requesty",
- apiKey: apiConfiguration.requestyApiKey,
- baseUrl: apiConfiguration.requestyBaseUrl,
- },
- },
- { key: "unbound", options: { provider: "unbound", apiKey: apiConfiguration.unboundApiKey } },
- { key: "vercel-ai-gateway", options: { provider: "vercel-ai-gateway" } },
- {
- key: "deepinfra",
- options: {
- provider: "deepinfra",
- apiKey: apiConfiguration.deepInfraApiKey,
- baseUrl: apiConfiguration.deepInfraBaseUrl,
- },
- },
- {
- key: "roo",
- options: {
- provider: "roo",
- baseUrl: process.env.ROO_CODE_PROVIDER_URL ?? "https://api.roocode.com/proxy",
- apiKey: CloudService.hasInstance()
- ? CloudService.instance.authService?.getSessionToken()
- : undefined,
- },
- },
- {
- key: "chutes",
- options: { provider: "chutes", apiKey: apiConfiguration.chutesApiKey },
- },
- ]
- // IO Intelligence is conditional on api key
- if (apiConfiguration.ioIntelligenceApiKey) {
- candidates.push({
- key: "io-intelligence",
- options: { provider: "io-intelligence", apiKey: apiConfiguration.ioIntelligenceApiKey },
- })
- }
- // LiteLLM is conditional on baseUrl+apiKey
- const litellmApiKey = apiConfiguration.litellmApiKey || message?.values?.litellmApiKey
- const litellmBaseUrl = apiConfiguration.litellmBaseUrl || message?.values?.litellmBaseUrl
- if (litellmApiKey && litellmBaseUrl) {
- // If explicit credentials are provided in message.values (from Refresh Models button),
- // flush the cache first to ensure we fetch fresh data with the new credentials
- if (message?.values?.litellmApiKey || message?.values?.litellmBaseUrl) {
- await flushModels({ provider: "litellm", apiKey: litellmApiKey, baseUrl: litellmBaseUrl }, true)
- }
- candidates.push({
- key: "litellm",
- options: { provider: "litellm", apiKey: litellmApiKey, baseUrl: litellmBaseUrl },
- })
- }
- // Apply single provider filter if specified
- const modelFetchPromises = providerFilter
- ? candidates.filter(({ key }) => key === providerFilter)
- : candidates
- // If refresh flag is set and we have a specific provider, flush its cache first
- if (shouldRefresh && providerFilter && modelFetchPromises.length > 0) {
- const targetCandidate = modelFetchPromises[0]
- await flushModels(targetCandidate.options, true)
- }
- const results = await Promise.allSettled(
- modelFetchPromises.map(async ({ key, options }) => {
- const models = await safeGetModels(options)
- return { key, models } // The key is `ProviderName` here.
- }),
- )
- results.forEach((result, index) => {
- const routerName = modelFetchPromises[index].key
- if (result.status === "fulfilled") {
- routerModels[routerName] = result.value.models
- // Ollama and LM Studio settings pages still need these events. They are not fetched here.
- } else {
- // Handle rejection: Post a specific error message for this provider.
- const errorMessage = result.reason instanceof Error ? result.reason.message : String(result.reason)
- console.error(`Error fetching models for ${routerName}:`, result.reason)
- routerModels[routerName] = {} // Ensure it's an empty object in the main routerModels message.
- provider.postMessageToWebview({
- type: "singleRouterModelFetchResponse",
- success: false,
- error: errorMessage,
- values: { provider: routerName },
- })
- }
- })
- provider.postMessageToWebview({
- type: "routerModels",
- routerModels,
- values: providerFilter ? { provider: requestedProvider } : undefined,
- })
- break
- case "requestOllamaModels": {
- // Specific handler for Ollama models only.
- const { apiConfiguration: ollamaApiConfig } = await provider.getState()
- try {
- const ollamaOptions = {
- provider: "ollama" as const,
- baseUrl: ollamaApiConfig.ollamaBaseUrl,
- apiKey: ollamaApiConfig.ollamaApiKey,
- }
- // Flush cache and refresh to ensure fresh models.
- await flushModels(ollamaOptions, true)
- const ollamaModels = await getModels(ollamaOptions)
- if (Object.keys(ollamaModels).length > 0) {
- provider.postMessageToWebview({ type: "ollamaModels", ollamaModels: ollamaModels })
- }
- } catch (error) {
- // Silently fail - user hasn't configured Ollama yet
- console.debug("Ollama models fetch failed:", error)
- }
- break
- }
- case "requestLmStudioModels": {
- // Specific handler for LM Studio models only.
- const { apiConfiguration: lmStudioApiConfig } = await provider.getState()
- try {
- const lmStudioOptions = {
- provider: "lmstudio" as const,
- baseUrl: lmStudioApiConfig.lmStudioBaseUrl,
- }
- // Flush cache and refresh to ensure fresh models.
- await flushModels(lmStudioOptions, true)
- const lmStudioModels = await getModels(lmStudioOptions)
- if (Object.keys(lmStudioModels).length > 0) {
- provider.postMessageToWebview({
- type: "lmStudioModels",
- lmStudioModels: lmStudioModels,
- })
- }
- } catch (error) {
- // Silently fail - user hasn't configured LM Studio yet.
- console.debug("LM Studio models fetch failed:", error)
- }
- break
- }
- case "requestRooModels": {
- // Specific handler for Roo models only - flushes cache to ensure fresh auth token is used
- try {
- const rooOptions = {
- provider: "roo" as const,
- baseUrl: process.env.ROO_CODE_PROVIDER_URL ?? "https://api.roocode.com/proxy",
- apiKey: CloudService.hasInstance()
- ? CloudService.instance.authService?.getSessionToken()
- : undefined,
- }
- // Flush cache and refresh to ensure fresh models with current auth state
- await flushModels(rooOptions, true)
- const rooModels = await getModels(rooOptions)
- // Always send a response, even if no models are returned
- provider.postMessageToWebview({
- type: "singleRouterModelFetchResponse",
- success: true,
- values: { provider: "roo", models: rooModels },
- })
- } catch (error) {
- // Send error response
- const errorMessage = error instanceof Error ? error.message : String(error)
- provider.postMessageToWebview({
- type: "singleRouterModelFetchResponse",
- success: false,
- error: errorMessage,
- values: { provider: "roo" },
- })
- }
- break
- }
- case "requestRooCreditBalance": {
- // Fetch Roo credit balance using CloudAPI
- const requestId = message.requestId
- try {
- if (!CloudService.hasInstance() || !CloudService.instance.cloudAPI) {
- throw new Error("Cloud service not available")
- }
- const balance = await CloudService.instance.cloudAPI.creditBalance()
- provider.postMessageToWebview({
- type: "rooCreditBalance",
- requestId,
- values: { balance },
- })
- } catch (error) {
- const errorMessage = error instanceof Error ? error.message : String(error)
- provider.postMessageToWebview({
- type: "rooCreditBalance",
- requestId,
- values: { error: errorMessage },
- })
- }
- break
- }
- case "requestOpenAiModels":
- if (message?.values?.baseUrl && message?.values?.apiKey) {
- const openAiModels = await getOpenAiModels(
- message?.values?.baseUrl,
- message?.values?.apiKey,
- message?.values?.openAiHeaders,
- )
- provider.postMessageToWebview({ type: "openAiModels", openAiModels })
- }
- break
- case "requestVsCodeLmModels":
- const vsCodeLmModels = await getVsCodeLmModels()
- // TODO: Cache like we do for OpenRouter, etc?
- provider.postMessageToWebview({ type: "vsCodeLmModels", vsCodeLmModels })
- break
- case "requestHuggingFaceModels":
- // TODO: Why isn't this handled by `requestRouterModels` above?
- try {
- const { getHuggingFaceModelsWithMetadata } = await import("../../api/providers/fetchers/huggingface")
- const huggingFaceModelsResponse = await getHuggingFaceModelsWithMetadata()
- provider.postMessageToWebview({
- type: "huggingFaceModels",
- huggingFaceModels: huggingFaceModelsResponse.models,
- })
- } catch (error) {
- console.error("Failed to fetch Hugging Face models:", error)
- provider.postMessageToWebview({ type: "huggingFaceModels", huggingFaceModels: [] })
- }
- break
- case "openImage":
- openImage(message.text!, { values: message.values })
- break
- case "saveImage":
- saveImage(message.dataUri!)
- break
- case "openFile":
- let filePath: string = message.text!
- if (!path.isAbsolute(filePath)) {
- filePath = path.join(getCurrentCwd(), filePath)
- }
- openFile(filePath, message.values as { create?: boolean; content?: string; line?: number })
- break
- case "openMention":
- openMention(getCurrentCwd(), message.text)
- break
- case "openExternal":
- if (message.url) {
- vscode.env.openExternal(vscode.Uri.parse(message.url))
- }
- break
- case "checkpointDiff":
- const result = checkoutDiffPayloadSchema.safeParse(message.payload)
- if (result.success) {
- await provider.getCurrentTask()?.checkpointDiff(result.data)
- }
- break
- case "checkpointRestore": {
- const result = checkoutRestorePayloadSchema.safeParse(message.payload)
- if (result.success) {
- await provider.cancelTask()
- try {
- await pWaitFor(() => provider.getCurrentTask()?.isInitialized === true, { timeout: 3_000 })
- } catch (error) {
- vscode.window.showErrorMessage(t("common:errors.checkpoint_timeout"))
- }
- try {
- await provider.getCurrentTask()?.checkpointRestore(result.data)
- } catch (error) {
- vscode.window.showErrorMessage(t("common:errors.checkpoint_failed"))
- }
- }
- break
- }
- case "cancelTask":
- await provider.cancelTask()
- break
- case "cancelAutoApproval":
- // Cancel any pending auto-approval timeout for the current task
- provider.getCurrentTask()?.cancelAutoApprovalTimeout()
- break
- case "killBrowserSession":
- {
- const task = provider.getCurrentTask()
- if (task?.browserSession) {
- await task.browserSession.closeBrowser()
- await provider.postStateToWebview()
- }
- }
- break
- case "openBrowserSessionPanel":
- {
- // Toggle the Browser Session panel (open if closed, close if open)
- const panelManager = BrowserSessionPanelManager.getInstance(provider)
- await panelManager.toggle()
- }
- break
- case "showBrowserSessionPanelAtStep":
- {
- const panelManager = BrowserSessionPanelManager.getInstance(provider)
- // If this is a launch action, reset the manual close flag
- if (message.isLaunchAction) {
- panelManager.resetManualCloseFlag()
- }
- // Show panel if:
- // 1. Manual click (forceShow) - always show
- // 2. Launch action - always show and reset flag
- // 3. Auto-open for non-launch action - only if user hasn't manually closed
- if (message.forceShow || message.isLaunchAction || panelManager.shouldAllowAutoOpen()) {
- // Ensure panel is shown and populated
- await panelManager.show()
- // Navigate to a specific step if provided
- // For launch actions: navigate to step 0
- // For manual clicks: navigate to the clicked step
- // For auto-opens of regular actions: don't navigate, let BrowserSessionRow's
- // internal auto-advance logic handle it (only advances if user is on most recent step)
- if (typeof message.stepIndex === "number" && message.stepIndex >= 0) {
- await panelManager.navigateToStep(message.stepIndex)
- }
- }
- }
- break
- case "refreshBrowserSessionPanel":
- {
- // Re-send the latest browser session snapshot to the panel
- const panelManager = BrowserSessionPanelManager.getInstance(provider)
- const task = provider.getCurrentTask()
- if (task) {
- const messages = task.clineMessages || []
- const browserSessionStartIndex = messages.findIndex(
- (m) =>
- m.ask === "browser_action_launch" ||
- (m.say === "browser_session_status" && m.text?.includes("opened")),
- )
- const browserSessionMessages =
- browserSessionStartIndex !== -1 ? messages.slice(browserSessionStartIndex) : []
- const isBrowserSessionActive = task.browserSession?.isSessionActive() ?? false
- await panelManager.updateBrowserSession(browserSessionMessages, isBrowserSessionActive)
- }
- }
- break
- case "allowedCommands": {
- // Validate and sanitize the commands array
- const commands = message.commands ?? []
- const validCommands = Array.isArray(commands)
- ? commands.filter((cmd) => typeof cmd === "string" && cmd.trim().length > 0)
- : []
- await updateGlobalState("allowedCommands", validCommands)
- // Also update workspace settings.
- await vscode.workspace
- .getConfiguration(Package.name)
- .update("allowedCommands", validCommands, vscode.ConfigurationTarget.Global)
- break
- }
- case "deniedCommands": {
- // Validate and sanitize the commands array
- const commands = message.commands ?? []
- const validCommands = Array.isArray(commands)
- ? commands.filter((cmd) => typeof cmd === "string" && cmd.trim().length > 0)
- : []
- await updateGlobalState("deniedCommands", validCommands)
- // Also update workspace settings.
- await vscode.workspace
- .getConfiguration(Package.name)
- .update("deniedCommands", validCommands, vscode.ConfigurationTarget.Global)
- break
- }
- case "openCustomModesSettings": {
- const customModesFilePath = await provider.customModesManager.getCustomModesFilePath()
- if (customModesFilePath) {
- openFile(customModesFilePath)
- }
- break
- }
- case "openKeyboardShortcuts": {
- // Open VSCode keyboard shortcuts settings and optionally filter to show the Roo Code commands
- const searchQuery = message.text || ""
- if (searchQuery) {
- // Open with a search query pre-filled
- await vscode.commands.executeCommand("workbench.action.openGlobalKeybindings", searchQuery)
- } else {
- // Just open the keyboard shortcuts settings
- await vscode.commands.executeCommand("workbench.action.openGlobalKeybindings")
- }
- break
- }
- case "openMcpSettings": {
- const mcpSettingsFilePath = await provider.getMcpHub()?.getMcpSettingsFilePath()
- if (mcpSettingsFilePath) {
- openFile(mcpSettingsFilePath)
- }
- break
- }
- case "openProjectMcpSettings": {
- if (!vscode.workspace.workspaceFolders?.length) {
- vscode.window.showErrorMessage(t("common:errors.no_workspace"))
- return
- }
- const workspaceFolder = getCurrentCwd()
- const rooDir = path.join(workspaceFolder, ".roo")
- const mcpPath = path.join(rooDir, "mcp.json")
- try {
- await fs.mkdir(rooDir, { recursive: true })
- const exists = await fileExistsAtPath(mcpPath)
- if (!exists) {
- await safeWriteJson(mcpPath, { mcpServers: {} })
- }
- await openFile(mcpPath)
- } catch (error) {
- vscode.window.showErrorMessage(t("mcp:errors.create_json", { error: `${error}` }))
- }
- break
- }
- case "deleteMcpServer": {
- if (!message.serverName) {
- break
- }
- try {
- provider.log(`Attempting to delete MCP server: ${message.serverName}`)
- await provider.getMcpHub()?.deleteServer(message.serverName, message.source as "global" | "project")
- provider.log(`Successfully deleted MCP server: ${message.serverName}`)
- // Refresh the webview state
- await provider.postStateToWebview()
- } catch (error) {
- const errorMessage = error instanceof Error ? error.message : String(error)
- provider.log(`Failed to delete MCP server: ${errorMessage}`)
- // Error messages are already handled by McpHub.deleteServer
- }
- break
- }
- case "restartMcpServer": {
- try {
- await provider.getMcpHub()?.restartConnection(message.text!, message.source as "global" | "project")
- } catch (error) {
- provider.log(
- `Failed to retry connection for ${message.text}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
- )
- }
- break
- }
- case "toggleToolAlwaysAllow": {
- try {
- await provider
- .getMcpHub()
- ?.toggleToolAlwaysAllow(
- message.serverName!,
- message.source as "global" | "project",
- message.toolName!,
- Boolean(message.alwaysAllow),
- )
- } catch (error) {
- provider.log(
- `Failed to toggle auto-approve for tool ${message.toolName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
- )
- }
- break
- }
- case "toggleToolEnabledForPrompt": {
- try {
- await provider
- .getMcpHub()
- ?.toggleToolEnabledForPrompt(
- message.serverName!,
- message.source as "global" | "project",
- message.toolName!,
- Boolean(message.isEnabled),
- )
- } catch (error) {
- provider.log(
- `Failed to toggle enabled for prompt for tool ${message.toolName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
- )
- }
- break
- }
- case "toggleMcpServer": {
- try {
- await provider
- .getMcpHub()
- ?.toggleServerDisabled(
- message.serverName!,
- message.disabled!,
- message.source as "global" | "project",
- )
- } catch (error) {
- provider.log(
- `Failed to toggle MCP server ${message.serverName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
- )
- }
- break
- }
- case "enableMcpServerCreation":
- await updateGlobalState("enableMcpServerCreation", message.bool ?? true)
- await provider.postStateToWebview()
- break
- case "remoteControlEnabled":
- try {
- await CloudService.instance.updateUserSettings({ extensionBridgeEnabled: message.bool ?? false })
- } catch (error) {
- provider.log(
- `CloudService#updateUserSettings failed: ${error instanceof Error ? error.message : String(error)}`,
- )
- }
- break
- case "taskSyncEnabled":
- const enabled = message.bool ?? false
- const updatedSettings: Partial<UserSettingsConfig> = { taskSyncEnabled: enabled }
- // If disabling task sync, also disable remote control.
- if (!enabled) {
- updatedSettings.extensionBridgeEnabled = false
- }
- try {
- await CloudService.instance.updateUserSettings(updatedSettings)
- } catch (error) {
- provider.log(`Failed to update cloud settings for task sync: ${error}`)
- }
- break
- case "refreshAllMcpServers": {
- const mcpHub = provider.getMcpHub()
- if (mcpHub) {
- await mcpHub.refreshAllConnections()
- }
- break
- }
- case "ttsEnabled":
- const ttsEnabled = message.bool ?? true
- await updateGlobalState("ttsEnabled", ttsEnabled)
- setTtsEnabled(ttsEnabled)
- await provider.postStateToWebview()
- break
- case "ttsSpeed":
- const ttsSpeed = message.value ?? 1.0
- await updateGlobalState("ttsSpeed", ttsSpeed)
- setTtsSpeed(ttsSpeed)
- await provider.postStateToWebview()
- break
- case "playTts":
- if (message.text) {
- playTts(message.text, {
- onStart: () => provider.postMessageToWebview({ type: "ttsStart", text: message.text }),
- onStop: () => provider.postMessageToWebview({ type: "ttsStop", text: message.text }),
- })
- }
- break
- case "stopTts":
- stopTts()
- break
- case "testBrowserConnection":
- // If no text is provided, try auto-discovery
- if (!message.text) {
- // Use testBrowserConnection for auto-discovery
- const chromeHostUrl = await discoverChromeHostUrl()
- if (chromeHostUrl) {
- // Send the result back to the webview
- await provider.postMessageToWebview({
- type: "browserConnectionResult",
- success: !!chromeHostUrl,
- text: `Auto-discovered and tested connection to Chrome: ${chromeHostUrl}`,
- values: { endpoint: chromeHostUrl },
- })
- } else {
- await provider.postMessageToWebview({
- type: "browserConnectionResult",
- success: false,
- text: "No Chrome instances found on the network. Make sure Chrome is running with remote debugging enabled (--remote-debugging-port=9222).",
- })
- }
- } else {
- // Test the provided URL
- const customHostUrl = message.text
- const hostIsValid = await tryChromeHostUrl(message.text)
- // Send the result back to the webview
- await provider.postMessageToWebview({
- type: "browserConnectionResult",
- success: hostIsValid,
- text: hostIsValid
- ? `Successfully connected to Chrome: ${customHostUrl}`
- : "Failed to connect to Chrome",
- })
- }
- break
- case "updateVSCodeSetting": {
- const { setting, value } = message
- if (setting !== undefined && value !== undefined) {
- if (ALLOWED_VSCODE_SETTINGS.has(setting)) {
- await vscode.workspace.getConfiguration().update(setting, value, true)
- } else {
- vscode.window.showErrorMessage(`Cannot update restricted VSCode setting: ${setting}`)
- }
- }
- break
- }
- case "getVSCodeSetting":
- const { setting } = message
- if (setting) {
- try {
- await provider.postMessageToWebview({
- type: "vsCodeSetting",
- setting,
- value: vscode.workspace.getConfiguration().get(setting),
- })
- } catch (error) {
- console.error(`Failed to get VSCode setting ${message.setting}:`, error)
- await provider.postMessageToWebview({
- type: "vsCodeSetting",
- setting,
- error: `Failed to get setting: ${error.message}`,
- value: undefined,
- })
- }
- }
- break
- case "mode":
- await provider.handleModeSwitch(message.text as Mode)
- break
- case "updatePrompt":
- if (message.promptMode && message.customPrompt !== undefined) {
- const existingPrompts = getGlobalState("customModePrompts") ?? {}
- const updatedPrompts = { ...existingPrompts, [message.promptMode]: message.customPrompt }
- await updateGlobalState("customModePrompts", updatedPrompts)
- const currentState = await provider.getStateToPostToWebview()
- const stateWithPrompts = {
- ...currentState,
- customModePrompts: updatedPrompts,
- hasOpenedModeSelector: currentState.hasOpenedModeSelector ?? false,
- }
- provider.postMessageToWebview({ type: "state", state: stateWithPrompts })
- if (TelemetryService.hasInstance()) {
- // Determine which setting was changed by comparing objects
- const oldPrompt = existingPrompts[message.promptMode] || {}
- const newPrompt = message.customPrompt
- const changedSettings = Object.keys(newPrompt).filter(
- (key) =>
- JSON.stringify((oldPrompt as Record<string, unknown>)[key]) !==
- JSON.stringify((newPrompt as Record<string, unknown>)[key]),
- )
- if (changedSettings.length > 0) {
- TelemetryService.instance.captureModeSettingChanged(changedSettings[0])
- }
- }
- }
- break
- case "deleteMessage": {
- if (!provider.getCurrentTask()) {
- await vscode.window.showErrorMessage(t("common:errors.message.no_active_task_to_delete"))
- break
- }
- if (typeof message.value !== "number" || !message.value) {
- await vscode.window.showErrorMessage(t("common:errors.message.invalid_timestamp_for_deletion"))
- break
- }
- await handleMessageModificationsOperation(message.value, "delete")
- break
- }
- case "submitEditedMessage": {
- if (
- provider.getCurrentTask() &&
- typeof message.value === "number" &&
- message.value &&
- message.editedMessageContent
- ) {
- await handleMessageModificationsOperation(
- message.value,
- "edit",
- message.editedMessageContent,
- message.images,
- )
- }
- break
- }
- case "hasOpenedModeSelector":
- await updateGlobalState("hasOpenedModeSelector", message.bool ?? true)
- await provider.postStateToWebview()
- break
- case "toggleApiConfigPin":
- if (message.text) {
- const currentPinned = getGlobalState("pinnedApiConfigs") ?? {}
- const updatedPinned: Record<string, boolean> = { ...currentPinned }
- if (currentPinned[message.text]) {
- delete updatedPinned[message.text]
- } else {
- updatedPinned[message.text] = true
- }
- await updateGlobalState("pinnedApiConfigs", updatedPinned)
- await provider.postStateToWebview()
- }
- break
- case "enhancementApiConfigId":
- await updateGlobalState("enhancementApiConfigId", message.text)
- await provider.postStateToWebview()
- break
- case "updateCondensingPrompt":
- // Store the condensing prompt in customSupportPrompts["CONDENSE"]
- // instead of customCondensingPrompt.
- const currentSupportPrompts = getGlobalState("customSupportPrompts") ?? {}
- const updatedSupportPrompts = { ...currentSupportPrompts, CONDENSE: message.text }
- await updateGlobalState("customSupportPrompts", updatedSupportPrompts)
- // Also update the old field for backward compatibility during migration.
- await updateGlobalState("customCondensingPrompt", message.text)
- await provider.postStateToWebview()
- break
- case "autoApprovalEnabled":
- await updateGlobalState("autoApprovalEnabled", message.bool ?? false)
- await provider.postStateToWebview()
- break
- case "enhancePrompt":
- if (message.text) {
- try {
- const state = await provider.getState()
- const {
- apiConfiguration,
- customSupportPrompts,
- listApiConfigMeta = [],
- enhancementApiConfigId,
- includeTaskHistoryInEnhance,
- } = state
- const currentCline = provider.getCurrentTask()
- const result = await MessageEnhancer.enhanceMessage({
- text: message.text,
- apiConfiguration,
- customSupportPrompts,
- listApiConfigMeta,
- enhancementApiConfigId,
- includeTaskHistoryInEnhance,
- currentClineMessages: currentCline?.clineMessages,
- providerSettingsManager: provider.providerSettingsManager,
- })
- if (result.success && result.enhancedText) {
- MessageEnhancer.captureTelemetry(currentCline?.taskId, includeTaskHistoryInEnhance)
- await provider.postMessageToWebview({ type: "enhancedPrompt", text: result.enhancedText })
- } else {
- throw new Error(result.error || "Unknown error")
- }
- } catch (error) {
- provider.log(
- `Error enhancing prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
- )
- vscode.window.showErrorMessage(t("common:errors.enhance_prompt"))
- await provider.postMessageToWebview({ type: "enhancedPrompt" })
- }
- }
- break
- case "getSystemPrompt":
- try {
- const systemPrompt = await generateSystemPrompt(provider, message)
- await provider.postMessageToWebview({
- type: "systemPrompt",
- text: systemPrompt,
- mode: message.mode,
- })
- } catch (error) {
- provider.log(
- `Error getting system prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
- )
- vscode.window.showErrorMessage(t("common:errors.get_system_prompt"))
- }
- break
- case "copySystemPrompt":
- try {
- const systemPrompt = await generateSystemPrompt(provider, message)
- await vscode.env.clipboard.writeText(systemPrompt)
- await vscode.window.showInformationMessage(t("common:info.clipboard_copy"))
- } catch (error) {
- provider.log(
- `Error getting system prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
- )
- vscode.window.showErrorMessage(t("common:errors.get_system_prompt"))
- }
- break
- case "searchCommits": {
- const cwd = getCurrentCwd()
- if (cwd) {
- try {
- const commits = await searchCommits(message.query || "", cwd)
- await provider.postMessageToWebview({
- type: "commitSearchResults",
- commits,
- })
- } catch (error) {
- provider.log(
- `Error searching commits: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
- )
- vscode.window.showErrorMessage(t("common:errors.search_commits"))
- }
- }
- break
- }
- case "searchFiles": {
- const workspacePath = getCurrentCwd()
- if (!workspacePath) {
- // Handle case where workspace path is not available
- await provider.postMessageToWebview({
- type: "fileSearchResults",
- results: [],
- requestId: message.requestId,
- error: "No workspace path available",
- })
- break
- }
- try {
- // Call file search service with query from message
- const results = await searchWorkspaceFiles(
- message.query || "",
- workspacePath,
- 20, // Use default limit, as filtering is now done in the backend
- )
- // Get the RooIgnoreController from the current task, or create a new one
- const currentTask = provider.getCurrentTask()
- let rooIgnoreController = currentTask?.rooIgnoreController
- let tempController: RooIgnoreController | undefined
- // If no current task or no controller, create a temporary one
- if (!rooIgnoreController) {
- tempController = new RooIgnoreController(workspacePath)
- await tempController.initialize()
- rooIgnoreController = tempController
- }
- try {
- // Get showRooIgnoredFiles setting from state
- const { showRooIgnoredFiles = false } = (await provider.getState()) ?? {}
- // Filter results using RooIgnoreController if showRooIgnoredFiles is false
- let filteredResults = results
- if (!showRooIgnoredFiles && rooIgnoreController) {
- const allowedPaths = rooIgnoreController.filterPaths(results.map((r) => r.path))
- filteredResults = results.filter((r) => allowedPaths.includes(r.path))
- }
- // Send results back to webview
- await provider.postMessageToWebview({
- type: "fileSearchResults",
- results: filteredResults,
- requestId: message.requestId,
- })
- } finally {
- // Dispose temporary controller to prevent resource leak
- tempController?.dispose()
- }
- } catch (error) {
- const errorMessage = error instanceof Error ? error.message : String(error)
- // Send error response to webview
- await provider.postMessageToWebview({
- type: "fileSearchResults",
- results: [],
- error: errorMessage,
- requestId: message.requestId,
- })
- }
- break
- }
- case "updateTodoList": {
- const payload = message.payload as { todos?: any[] }
- const todos = payload?.todos
- if (Array.isArray(todos)) {
- await setPendingTodoList(todos)
- }
- break
- }
- case "refreshCustomTools": {
- try {
- const toolDirs = getRooDirectoriesForCwd(getCurrentCwd()).map((dir) => path.join(dir, "tools"))
- await customToolRegistry.loadFromDirectories(toolDirs)
- await provider.postMessageToWebview({
- type: "customToolsResult",
- tools: customToolRegistry.getAllSerialized(),
- })
- } catch (error) {
- await provider.postMessageToWebview({
- type: "customToolsResult",
- tools: [],
- error: error instanceof Error ? error.message : String(error),
- })
- }
- break
- }
- case "saveApiConfiguration":
- if (message.text && message.apiConfiguration) {
- try {
- await provider.providerSettingsManager.saveConfig(message.text, message.apiConfiguration)
- const listApiConfig = await provider.providerSettingsManager.listConfig()
- await updateGlobalState("listApiConfigMeta", listApiConfig)
- } catch (error) {
- provider.log(
- `Error save api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
- )
- vscode.window.showErrorMessage(t("common:errors.save_api_config"))
- }
- }
- break
- case "upsertApiConfiguration":
- if (message.text && message.apiConfiguration) {
- await provider.upsertProviderProfile(message.text, message.apiConfiguration)
- }
- break
- case "renameApiConfiguration":
- if (message.values && message.apiConfiguration) {
- try {
- const { oldName, newName } = message.values
- if (oldName === newName) {
- break
- }
- // Load the old configuration to get its ID.
- const { id } = await provider.providerSettingsManager.getProfile({ name: oldName })
- // Create a new configuration with the new name and old ID.
- await provider.providerSettingsManager.saveConfig(newName, { ...message.apiConfiguration, id })
- // Delete the old configuration.
- await provider.providerSettingsManager.deleteConfig(oldName)
- // Re-activate to update the global settings related to the
- // currently activated provider profile.
- await provider.activateProviderProfile({ name: newName })
- } catch (error) {
- provider.log(
- `Error rename api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
- )
- vscode.window.showErrorMessage(t("common:errors.rename_api_config"))
- }
- }
- break
- case "loadApiConfiguration":
- if (message.text) {
- try {
- await provider.activateProviderProfile({ name: message.text })
- } catch (error) {
- provider.log(
- `Error load api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
- )
- vscode.window.showErrorMessage(t("common:errors.load_api_config"))
- }
- }
- break
- case "loadApiConfigurationById":
- if (message.text) {
- try {
- await provider.activateProviderProfile({ id: message.text })
- } catch (error) {
- provider.log(
- `Error load api configuration by ID: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
- )
- vscode.window.showErrorMessage(t("common:errors.load_api_config"))
- }
- }
- break
- case "deleteApiConfiguration":
- if (message.text) {
- const answer = await vscode.window.showInformationMessage(
- t("common:confirmation.delete_config_profile"),
- { modal: true },
- t("common:answers.yes"),
- )
- if (answer !== t("common:answers.yes")) {
- break
- }
- const oldName = message.text
- const newName = (await provider.providerSettingsManager.listConfig()).filter(
- (c) => c.name !== oldName,
- )[0]?.name
- if (!newName) {
- vscode.window.showErrorMessage(t("common:errors.delete_api_config"))
- return
- }
- try {
- await provider.providerSettingsManager.deleteConfig(oldName)
- await provider.activateProviderProfile({ name: newName })
- } catch (error) {
- provider.log(
- `Error delete api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
- )
- vscode.window.showErrorMessage(t("common:errors.delete_api_config"))
- }
- }
- break
- case "deleteMessageConfirm":
- if (!message.messageTs) {
- await vscode.window.showErrorMessage(t("common:errors.message.cannot_delete_missing_timestamp"))
- break
- }
- if (typeof message.messageTs !== "number") {
- await vscode.window.showErrorMessage(t("common:errors.message.cannot_delete_invalid_timestamp"))
- break
- }
- await handleDeleteMessageConfirm(message.messageTs, message.restoreCheckpoint)
- break
- case "editMessageConfirm":
- if (message.messageTs && message.text) {
- const resolved = await resolveIncomingImages({ text: message.text, images: message.images })
- await handleEditMessageConfirm(
- message.messageTs,
- resolved.text,
- message.restoreCheckpoint,
- resolved.images,
- )
- }
- break
- case "getListApiConfiguration":
- try {
- const listApiConfig = await provider.providerSettingsManager.listConfig()
- await updateGlobalState("listApiConfigMeta", listApiConfig)
- provider.postMessageToWebview({ type: "listApiConfig", listApiConfig })
- } catch (error) {
- provider.log(
- `Error get list api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
- )
- vscode.window.showErrorMessage(t("common:errors.list_api_config"))
- }
- break
- case "updateMcpTimeout":
- if (message.serverName && typeof message.timeout === "number") {
- try {
- await provider
- .getMcpHub()
- ?.updateServerTimeout(
- message.serverName,
- message.timeout,
- message.source as "global" | "project",
- )
- } catch (error) {
- provider.log(
- `Failed to update timeout for ${message.serverName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
- )
- vscode.window.showErrorMessage(t("common:errors.update_server_timeout"))
- }
- }
- break
- case "updateCustomMode":
- if (message.modeConfig) {
- try {
- // Check if this is a new mode or an update to an existing mode
- const existingModes = await provider.customModesManager.getCustomModes()
- const isNewMode = !existingModes.some((mode) => mode.slug === message.modeConfig?.slug)
- await provider.customModesManager.updateCustomMode(message.modeConfig.slug, message.modeConfig)
- // Update state after saving the mode
- const customModes = await provider.customModesManager.getCustomModes()
- await updateGlobalState("customModes", customModes)
- await updateGlobalState("mode", message.modeConfig.slug)
- await provider.postStateToWebview()
- // Track telemetry for custom mode creation or update
- if (TelemetryService.hasInstance()) {
- if (isNewMode) {
- // This is a new custom mode
- TelemetryService.instance.captureCustomModeCreated(
- message.modeConfig.slug,
- message.modeConfig.name,
- )
- } else {
- // Determine which setting was changed by comparing objects
- const existingMode = existingModes.find((mode) => mode.slug === message.modeConfig?.slug)
- const changedSettings = existingMode
- ? Object.keys(message.modeConfig).filter(
- (key) =>
- JSON.stringify((existingMode as Record<string, unknown>)[key]) !==
- JSON.stringify((message.modeConfig as Record<string, unknown>)[key]),
- )
- : []
- if (changedSettings.length > 0) {
- TelemetryService.instance.captureModeSettingChanged(changedSettings[0])
- }
- }
- }
- } catch (error) {
- // Error already shown to user by updateCustomMode
- // Just prevent unhandled rejection and skip state updates
- }
- }
- break
- case "deleteCustomMode":
- if (message.slug) {
- // Get the mode details to determine source and rules folder path
- const customModes = await provider.customModesManager.getCustomModes()
- const modeToDelete = customModes.find((mode) => mode.slug === message.slug)
- if (!modeToDelete) {
- break
- }
- // Determine the scope based on source (project or global)
- const scope = modeToDelete.source || "global"
- // Determine the rules folder path
- let rulesFolderPath: string
- if (scope === "project") {
- const workspacePath = getWorkspacePath()
- if (workspacePath) {
- rulesFolderPath = path.join(workspacePath, ".roo", `rules-${message.slug}`)
- } else {
- rulesFolderPath = path.join(".roo", `rules-${message.slug}`)
- }
- } else {
- // Global scope - use OS home directory
- const homeDir = os.homedir()
- rulesFolderPath = path.join(homeDir, ".roo", `rules-${message.slug}`)
- }
- // Check if the rules folder exists
- const rulesFolderExists = await fileExistsAtPath(rulesFolderPath)
- // If this is a check request, send back the folder info
- if (message.checkOnly) {
- await provider.postMessageToWebview({
- type: "deleteCustomModeCheck",
- slug: message.slug,
- rulesFolderPath: rulesFolderExists ? rulesFolderPath : undefined,
- })
- break
- }
- // Delete the mode
- await provider.customModesManager.deleteCustomMode(message.slug)
- // Delete the rules folder if it exists
- if (rulesFolderExists) {
- try {
- await fs.rm(rulesFolderPath, { recursive: true, force: true })
- provider.log(`Deleted rules folder for mode ${message.slug}: ${rulesFolderPath}`)
- } catch (error) {
- provider.log(`Failed to delete rules folder for mode ${message.slug}: ${error}`)
- // Notify the user about the failure
- vscode.window.showErrorMessage(
- t("common:errors.delete_rules_folder_failed", {
- rulesFolderPath,
- error: error instanceof Error ? error.message : String(error),
- }),
- )
- // Continue with mode deletion even if folder deletion fails
- }
- }
- // Switch back to default mode after deletion
- await updateGlobalState("mode", defaultModeSlug)
- await provider.postStateToWebview()
- }
- break
- case "exportMode":
- if (message.slug) {
- try {
- // Get custom mode prompts to check if built-in mode has been customized
- const customModePrompts = getGlobalState("customModePrompts") || {}
- const customPrompt = customModePrompts[message.slug]
- // Export the mode with any customizations merged directly
- const result = await provider.customModesManager.exportModeWithRules(message.slug, customPrompt)
- if (result.success && result.yaml) {
- // Get last used directory for export
- const lastExportPath = getGlobalState("lastModeExportPath")
- let defaultUri: vscode.Uri
- if (lastExportPath) {
- // Use the directory from the last export
- const lastDir = path.dirname(lastExportPath)
- defaultUri = vscode.Uri.file(path.join(lastDir, `${message.slug}-export.yaml`))
- } else {
- // Default to workspace or home directory
- const workspaceFolders = vscode.workspace.workspaceFolders
- if (workspaceFolders && workspaceFolders.length > 0) {
- defaultUri = vscode.Uri.file(
- path.join(workspaceFolders[0].uri.fsPath, `${message.slug}-export.yaml`),
- )
- } else {
- defaultUri = vscode.Uri.file(`${message.slug}-export.yaml`)
- }
- }
- // Show save dialog
- const saveUri = await vscode.window.showSaveDialog({
- defaultUri,
- filters: {
- "YAML files": ["yaml", "yml"],
- },
- title: "Save mode export",
- })
- if (saveUri && result.yaml) {
- // Save the directory for next time
- await updateGlobalState("lastModeExportPath", saveUri.fsPath)
- // Write the file to the selected location
- await fs.writeFile(saveUri.fsPath, result.yaml, "utf-8")
- // Send success message to webview
- provider.postMessageToWebview({
- type: "exportModeResult",
- success: true,
- slug: message.slug,
- })
- // Show info message
- vscode.window.showInformationMessage(t("common:info.mode_exported", { mode: message.slug }))
- } else {
- // User cancelled the save dialog
- provider.postMessageToWebview({
- type: "exportModeResult",
- success: false,
- error: "Export cancelled",
- slug: message.slug,
- })
- }
- } else {
- // Send error message to webview
- provider.postMessageToWebview({
- type: "exportModeResult",
- success: false,
- error: result.error,
- slug: message.slug,
- })
- }
- } catch (error) {
- const errorMessage = error instanceof Error ? error.message : String(error)
- provider.log(`Failed to export mode ${message.slug}: ${errorMessage}`)
- // Send error message to webview
- provider.postMessageToWebview({
- type: "exportModeResult",
- success: false,
- error: errorMessage,
- slug: message.slug,
- })
- }
- }
- break
- case "importMode":
- try {
- // Get last used directory for import
- const lastImportPath = getGlobalState("lastModeImportPath")
- let defaultUri: vscode.Uri | undefined
- if (lastImportPath) {
- // Use the directory from the last import
- const lastDir = path.dirname(lastImportPath)
- defaultUri = vscode.Uri.file(lastDir)
- } else {
- // Default to workspace or home directory
- const workspaceFolders = vscode.workspace.workspaceFolders
- if (workspaceFolders && workspaceFolders.length > 0) {
- defaultUri = vscode.Uri.file(workspaceFolders[0].uri.fsPath)
- }
- }
- // Show file picker to select YAML file
- const fileUri = await vscode.window.showOpenDialog({
- canSelectFiles: true,
- canSelectFolders: false,
- canSelectMany: false,
- defaultUri,
- filters: {
- "YAML files": ["yaml", "yml"],
- },
- title: "Select mode export file to import",
- })
- if (fileUri && fileUri[0]) {
- // Save the directory for next time
- await updateGlobalState("lastModeImportPath", fileUri[0].fsPath)
- // Read the file content
- const yamlContent = await fs.readFile(fileUri[0].fsPath, "utf-8")
- // Import the mode with the specified source level
- const result = await provider.customModesManager.importModeWithRules(
- yamlContent,
- message.source || "project", // Default to project if not specified
- )
- if (result.success) {
- // Update state after importing
- const customModes = await provider.customModesManager.getCustomModes()
- await updateGlobalState("customModes", customModes)
- await provider.postStateToWebview()
- // Send success message to webview, include the imported slug so UI can switch
- provider.postMessageToWebview({
- type: "importModeResult",
- success: true,
- slug: result.slug,
- })
- // Show success message
- vscode.window.showInformationMessage(t("common:info.mode_imported"))
- } else {
- // Send error message to webview
- provider.postMessageToWebview({
- type: "importModeResult",
- success: false,
- error: result.error,
- })
- // Show error message
- vscode.window.showErrorMessage(t("common:errors.mode_import_failed", { error: result.error }))
- }
- } else {
- // User cancelled the file dialog - reset the importing state
- provider.postMessageToWebview({
- type: "importModeResult",
- success: false,
- error: "cancelled",
- })
- }
- } catch (error) {
- const errorMessage = error instanceof Error ? error.message : String(error)
- provider.log(`Failed to import mode: ${errorMessage}`)
- // Send error message to webview
- provider.postMessageToWebview({
- type: "importModeResult",
- success: false,
- error: errorMessage,
- })
- // Show error message
- vscode.window.showErrorMessage(t("common:errors.mode_import_failed", { error: errorMessage }))
- }
- break
- case "checkRulesDirectory":
- if (message.slug) {
- const hasContent = await provider.customModesManager.checkRulesDirectoryHasContent(message.slug)
- provider.postMessageToWebview({
- type: "checkRulesDirectoryResult",
- slug: message.slug,
- hasContent: hasContent,
- })
- }
- break
- case "telemetrySetting": {
- const telemetrySetting = message.text as TelemetrySetting
- const previousSetting = getGlobalState("telemetrySetting") || "unset"
- const isOptedIn = telemetrySetting !== "disabled"
- const wasPreviouslyOptedIn = previousSetting !== "disabled"
- // If turning telemetry OFF, fire event BEFORE disabling
- if (wasPreviouslyOptedIn && !isOptedIn && TelemetryService.hasInstance()) {
- TelemetryService.instance.captureTelemetrySettingsChanged(previousSetting, telemetrySetting)
- }
- // Update the telemetry state
- await updateGlobalState("telemetrySetting", telemetrySetting)
- if (TelemetryService.hasInstance()) {
- TelemetryService.instance.updateTelemetryState(isOptedIn)
- }
- // If turning telemetry ON, fire event AFTER enabling
- if (!wasPreviouslyOptedIn && isOptedIn && TelemetryService.hasInstance()) {
- TelemetryService.instance.captureTelemetrySettingsChanged(previousSetting, telemetrySetting)
- }
- await provider.postStateToWebview()
- break
- }
- case "debugSetting": {
- await vscode.workspace
- .getConfiguration(Package.name)
- .update("debug", message.bool ?? false, vscode.ConfigurationTarget.Global)
- await provider.postStateToWebview()
- break
- }
- case "cloudButtonClicked": {
- // Navigate to the cloud tab.
- provider.postMessageToWebview({ type: "action", action: "cloudButtonClicked" })
- break
- }
- case "rooCloudSignIn": {
- try {
- TelemetryService.instance.captureEvent(TelemetryEventName.AUTHENTICATION_INITIATED)
- // Use provider signup flow if useProviderSignup is explicitly true
- await CloudService.instance.login(undefined, message.useProviderSignup ?? false)
- } catch (error) {
- provider.log(`AuthService#login failed: ${error}`)
- vscode.window.showErrorMessage("Sign in failed.")
- }
- break
- }
- case "cloudLandingPageSignIn": {
- try {
- const landingPageSlug = message.text || "supernova"
- TelemetryService.instance.captureEvent(TelemetryEventName.AUTHENTICATION_INITIATED)
- await CloudService.instance.login(landingPageSlug)
- } catch (error) {
- provider.log(`CloudService#login failed: ${error}`)
- vscode.window.showErrorMessage("Sign in failed.")
- }
- break
- }
- case "rooCloudSignOut": {
- try {
- await CloudService.instance.logout()
- await provider.postStateToWebview()
- provider.postMessageToWebview({ type: "authenticatedUser", userInfo: undefined })
- } catch (error) {
- provider.log(`AuthService#logout failed: ${error}`)
- vscode.window.showErrorMessage("Sign out failed.")
- }
- break
- }
- case "claudeCodeSignIn": {
- try {
- const { claudeCodeOAuthManager } = await import("../../integrations/claude-code/oauth")
- const authUrl = claudeCodeOAuthManager.startAuthorizationFlow()
- // Open the authorization URL in the browser
- await vscode.env.openExternal(vscode.Uri.parse(authUrl))
- // Wait for the callback in a separate promise (non-blocking)
- claudeCodeOAuthManager
- .waitForCallback()
- .then(async () => {
- vscode.window.showInformationMessage("Successfully signed in to Claude Code")
- await provider.postStateToWebview()
- })
- .catch((error) => {
- provider.log(`Claude Code OAuth callback failed: ${error}`)
- if (!String(error).includes("timed out")) {
- vscode.window.showErrorMessage(`Claude Code sign in failed: ${error.message || error}`)
- }
- })
- } catch (error) {
- provider.log(`Claude Code OAuth failed: ${error}`)
- vscode.window.showErrorMessage("Claude Code sign in failed.")
- }
- break
- }
- case "claudeCodeSignOut": {
- try {
- const { claudeCodeOAuthManager } = await import("../../integrations/claude-code/oauth")
- await claudeCodeOAuthManager.clearCredentials()
- vscode.window.showInformationMessage("Signed out from Claude Code")
- await provider.postStateToWebview()
- } catch (error) {
- provider.log(`Claude Code sign out failed: ${error}`)
- vscode.window.showErrorMessage("Claude Code sign out failed.")
- }
- break
- }
- case "openAiCodexSignIn": {
- try {
- const { openAiCodexOAuthManager } = await import("../../integrations/openai-codex/oauth")
- const authUrl = openAiCodexOAuthManager.startAuthorizationFlow()
- // Open the authorization URL in the browser
- await vscode.env.openExternal(vscode.Uri.parse(authUrl))
- // Wait for the callback in a separate promise (non-blocking)
- openAiCodexOAuthManager
- .waitForCallback()
- .then(async () => {
- vscode.window.showInformationMessage("Successfully signed in to OpenAI Codex")
- await provider.postStateToWebview()
- })
- .catch((error) => {
- provider.log(`OpenAI Codex OAuth callback failed: ${error}`)
- if (!String(error).includes("timed out")) {
- vscode.window.showErrorMessage(`OpenAI Codex sign in failed: ${error.message || error}`)
- }
- })
- } catch (error) {
- provider.log(`OpenAI Codex OAuth failed: ${error}`)
- vscode.window.showErrorMessage("OpenAI Codex sign in failed.")
- }
- break
- }
- case "openAiCodexSignOut": {
- try {
- const { openAiCodexOAuthManager } = await import("../../integrations/openai-codex/oauth")
- await openAiCodexOAuthManager.clearCredentials()
- vscode.window.showInformationMessage("Signed out from OpenAI Codex")
- await provider.postStateToWebview()
- } catch (error) {
- provider.log(`OpenAI Codex sign out failed: ${error}`)
- vscode.window.showErrorMessage("OpenAI Codex sign out failed.")
- }
- break
- }
- case "rooCloudManualUrl": {
- try {
- if (!message.text) {
- vscode.window.showErrorMessage(t("common:errors.manual_url_empty"))
- break
- }
- // Parse the callback URL to extract parameters
- const callbackUrl = message.text.trim()
- const uri = vscode.Uri.parse(callbackUrl)
- if (!uri.query) {
- throw new Error(t("common:errors.manual_url_no_query"))
- }
- const query = new URLSearchParams(uri.query)
- const code = query.get("code")
- const state = query.get("state")
- const organizationId = query.get("organizationId")
- if (!code || !state) {
- throw new Error(t("common:errors.manual_url_missing_params"))
- }
- // Reuse the existing authentication flow
- await CloudService.instance.handleAuthCallback(
- code,
- state,
- organizationId === "null" ? null : organizationId,
- )
- await provider.postStateToWebview()
- } catch (error) {
- provider.log(`ManualUrl#handleAuthCallback failed: ${error}`)
- const errorMessage = error instanceof Error ? error.message : t("common:errors.manual_url_auth_failed")
- // Show error message through VS Code UI
- vscode.window.showErrorMessage(`${t("common:errors.manual_url_auth_error")}: ${errorMessage}`)
- }
- break
- }
- case "clearCloudAuthSkipModel": {
- // Clear the flag that indicates auth completed without model selection
- await provider.context.globalState.update("roo-auth-skip-model", undefined)
- await provider.postStateToWebview()
- break
- }
- case "switchOrganization": {
- try {
- const organizationId = message.organizationId ?? null
- // Switch to the new organization context
- await CloudService.instance.switchOrganization(organizationId)
- // Refresh the state to update UI
- await provider.postStateToWebview()
- // Send success response back to webview
- await provider.postMessageToWebview({
- type: "organizationSwitchResult",
- success: true,
- organizationId: organizationId,
- })
- } catch (error) {
- provider.log(`Organization switch failed: ${error}`)
- const errorMessage = error instanceof Error ? error.message : String(error)
- // Send error response back to webview
- await provider.postMessageToWebview({
- type: "organizationSwitchResult",
- success: false,
- error: errorMessage,
- organizationId: message.organizationId ?? null,
- })
- vscode.window.showErrorMessage(`Failed to switch organization: ${errorMessage}`)
- }
- break
- }
- case "saveCodeIndexSettingsAtomic": {
- if (!message.codeIndexSettings) {
- break
- }
- const settings = message.codeIndexSettings
- try {
- // Check if embedder provider has changed
- const currentConfig = getGlobalState("codebaseIndexConfig") || {}
- const embedderProviderChanged =
- currentConfig.codebaseIndexEmbedderProvider !== settings.codebaseIndexEmbedderProvider
- // Save global state settings atomically
- const globalStateConfig = {
- ...currentConfig,
- codebaseIndexEnabled: settings.codebaseIndexEnabled,
- codebaseIndexQdrantUrl: settings.codebaseIndexQdrantUrl,
- codebaseIndexEmbedderProvider: settings.codebaseIndexEmbedderProvider,
- codebaseIndexEmbedderBaseUrl: settings.codebaseIndexEmbedderBaseUrl,
- codebaseIndexEmbedderModelId: settings.codebaseIndexEmbedderModelId,
- codebaseIndexEmbedderModelDimension: settings.codebaseIndexEmbedderModelDimension, // Generic dimension
- codebaseIndexOpenAiCompatibleBaseUrl: settings.codebaseIndexOpenAiCompatibleBaseUrl,
- codebaseIndexBedrockRegion: settings.codebaseIndexBedrockRegion,
- codebaseIndexBedrockProfile: settings.codebaseIndexBedrockProfile,
- codebaseIndexSearchMaxResults: settings.codebaseIndexSearchMaxResults,
- codebaseIndexSearchMinScore: settings.codebaseIndexSearchMinScore,
- codebaseIndexOpenRouterSpecificProvider: settings.codebaseIndexOpenRouterSpecificProvider,
- }
- // Save global state first
- await updateGlobalState("codebaseIndexConfig", globalStateConfig)
- // Save secrets directly using context proxy
- if (settings.codeIndexOpenAiKey !== undefined) {
- await provider.contextProxy.storeSecret("codeIndexOpenAiKey", settings.codeIndexOpenAiKey)
- }
- if (settings.codeIndexQdrantApiKey !== undefined) {
- await provider.contextProxy.storeSecret("codeIndexQdrantApiKey", settings.codeIndexQdrantApiKey)
- }
- if (settings.codebaseIndexOpenAiCompatibleApiKey !== undefined) {
- await provider.contextProxy.storeSecret(
- "codebaseIndexOpenAiCompatibleApiKey",
- settings.codebaseIndexOpenAiCompatibleApiKey,
- )
- }
- if (settings.codebaseIndexGeminiApiKey !== undefined) {
- await provider.contextProxy.storeSecret(
- "codebaseIndexGeminiApiKey",
- settings.codebaseIndexGeminiApiKey,
- )
- }
- if (settings.codebaseIndexMistralApiKey !== undefined) {
- await provider.contextProxy.storeSecret(
- "codebaseIndexMistralApiKey",
- settings.codebaseIndexMistralApiKey,
- )
- }
- if (settings.codebaseIndexVercelAiGatewayApiKey !== undefined) {
- await provider.contextProxy.storeSecret(
- "codebaseIndexVercelAiGatewayApiKey",
- settings.codebaseIndexVercelAiGatewayApiKey,
- )
- }
- if (settings.codebaseIndexOpenRouterApiKey !== undefined) {
- await provider.contextProxy.storeSecret(
- "codebaseIndexOpenRouterApiKey",
- settings.codebaseIndexOpenRouterApiKey,
- )
- }
- // Send success response first - settings are saved regardless of validation
- await provider.postMessageToWebview({
- type: "codeIndexSettingsSaved",
- success: true,
- settings: globalStateConfig,
- })
- // Update webview state
- await provider.postStateToWebview()
- // Then handle validation and initialization for the current workspace
- const currentCodeIndexManager = provider.getCurrentWorkspaceCodeIndexManager()
- if (currentCodeIndexManager) {
- // If embedder provider changed, perform proactive validation
- if (embedderProviderChanged) {
- try {
- // Force handleSettingsChange which will trigger validation
- await currentCodeIndexManager.handleSettingsChange()
- } catch (error) {
- // Validation failed - the error state is already set by handleSettingsChange
- provider.log(
- `Embedder validation failed after provider change: ${error instanceof Error ? error.message : String(error)}`,
- )
- // Send validation error to webview
- await provider.postMessageToWebview({
- type: "indexingStatusUpdate",
- values: currentCodeIndexManager.getCurrentStatus(),
- })
- // Exit early - don't try to start indexing with invalid configuration
- break
- }
- } else {
- // No provider change, just handle settings normally
- try {
- await currentCodeIndexManager.handleSettingsChange()
- } catch (error) {
- // Log but don't fail - settings are saved
- provider.log(
- `Settings change handling error: ${error instanceof Error ? error.message : String(error)}`,
- )
- }
- }
- // Wait a bit more to ensure everything is ready
- await new Promise((resolve) => setTimeout(resolve, 200))
- // Auto-start indexing if now enabled and configured
- if (currentCodeIndexManager.isFeatureEnabled && currentCodeIndexManager.isFeatureConfigured) {
- if (!currentCodeIndexManager.isInitialized) {
- try {
- await currentCodeIndexManager.initialize(provider.contextProxy)
- provider.log(`Code index manager initialized after settings save`)
- } catch (error) {
- provider.log(
- `Code index initialization failed: ${error instanceof Error ? error.message : String(error)}`,
- )
- // Send error status to webview
- await provider.postMessageToWebview({
- type: "indexingStatusUpdate",
- values: currentCodeIndexManager.getCurrentStatus(),
- })
- }
- }
- }
- } else {
- // No workspace open - send error status
- provider.log("Cannot save code index settings: No workspace folder open")
- await provider.postMessageToWebview({
- type: "indexingStatusUpdate",
- values: {
- systemStatus: "Error",
- message: t("embeddings:orchestrator.indexingRequiresWorkspace"),
- processedItems: 0,
- totalItems: 0,
- currentItemUnit: "items",
- },
- })
- }
- } catch (error) {
- provider.log(`Error saving code index settings: ${error.message || error}`)
- await provider.postMessageToWebview({
- type: "codeIndexSettingsSaved",
- success: false,
- error: error.message || "Failed to save settings",
- })
- }
- break
- }
- case "requestIndexingStatus": {
- const manager = provider.getCurrentWorkspaceCodeIndexManager()
- if (!manager) {
- // No workspace open - send error status
- provider.postMessageToWebview({
- type: "indexingStatusUpdate",
- values: {
- systemStatus: "Error",
- message: t("embeddings:orchestrator.indexingRequiresWorkspace"),
- processedItems: 0,
- totalItems: 0,
- currentItemUnit: "items",
- workerspacePath: undefined,
- },
- })
- return
- }
- const status = manager
- ? manager.getCurrentStatus()
- : {
- systemStatus: "Standby",
- message: "No workspace folder open",
- processedItems: 0,
- totalItems: 0,
- currentItemUnit: "items",
- workspacePath: undefined,
- }
- provider.postMessageToWebview({
- type: "indexingStatusUpdate",
- values: status,
- })
- break
- }
- case "requestCodeIndexSecretStatus": {
- // Check if secrets are set using the VSCode context directly for async access
- const hasOpenAiKey = !!(await provider.context.secrets.get("codeIndexOpenAiKey"))
- const hasQdrantApiKey = !!(await provider.context.secrets.get("codeIndexQdrantApiKey"))
- const hasOpenAiCompatibleApiKey = !!(await provider.context.secrets.get(
- "codebaseIndexOpenAiCompatibleApiKey",
- ))
- const hasGeminiApiKey = !!(await provider.context.secrets.get("codebaseIndexGeminiApiKey"))
- const hasMistralApiKey = !!(await provider.context.secrets.get("codebaseIndexMistralApiKey"))
- const hasVercelAiGatewayApiKey = !!(await provider.context.secrets.get(
- "codebaseIndexVercelAiGatewayApiKey",
- ))
- const hasOpenRouterApiKey = !!(await provider.context.secrets.get("codebaseIndexOpenRouterApiKey"))
- provider.postMessageToWebview({
- type: "codeIndexSecretStatus",
- values: {
- hasOpenAiKey,
- hasQdrantApiKey,
- hasOpenAiCompatibleApiKey,
- hasGeminiApiKey,
- hasMistralApiKey,
- hasVercelAiGatewayApiKey,
- hasOpenRouterApiKey,
- },
- })
- break
- }
- case "startIndexing": {
- try {
- const manager = provider.getCurrentWorkspaceCodeIndexManager()
- if (!manager) {
- // No workspace open - send error status
- provider.postMessageToWebview({
- type: "indexingStatusUpdate",
- values: {
- systemStatus: "Error",
- message: t("embeddings:orchestrator.indexingRequiresWorkspace"),
- processedItems: 0,
- totalItems: 0,
- currentItemUnit: "items",
- },
- })
- provider.log("Cannot start indexing: No workspace folder open")
- return
- }
- if (manager.isFeatureEnabled && manager.isFeatureConfigured) {
- // Mimic extension startup behavior: initialize first, which will
- // check if Qdrant container is active and reuse existing collection
- await manager.initialize(provider.contextProxy)
- // Only call startIndexing if we're in a state that requires it
- // (e.g., Standby or Error). If already Indexed or Indexing, the
- // initialize() call above will have already started the watcher.
- const currentState = manager.state
- if (currentState === "Standby" || currentState === "Error") {
- // startIndexing now handles error recovery internally
- manager.startIndexing()
- // If startIndexing recovered from error, we need to reinitialize
- if (!manager.isInitialized) {
- await manager.initialize(provider.contextProxy)
- // Try starting again after initialization
- if (manager.state === "Standby" || manager.state === "Error") {
- manager.startIndexing()
- }
- }
- }
- }
- } catch (error) {
- provider.log(`Error starting indexing: ${error instanceof Error ? error.message : String(error)}`)
- }
- break
- }
- case "clearIndexData": {
- try {
- const manager = provider.getCurrentWorkspaceCodeIndexManager()
- if (!manager) {
- provider.log("Cannot clear index data: No workspace folder open")
- provider.postMessageToWebview({
- type: "indexCleared",
- values: {
- success: false,
- error: t("embeddings:orchestrator.indexingRequiresWorkspace"),
- },
- })
- return
- }
- await manager.clearIndexData()
- provider.postMessageToWebview({ type: "indexCleared", values: { success: true } })
- } catch (error) {
- provider.log(`Error clearing index data: ${error instanceof Error ? error.message : String(error)}`)
- provider.postMessageToWebview({
- type: "indexCleared",
- values: {
- success: false,
- error: error instanceof Error ? error.message : String(error),
- },
- })
- }
- break
- }
- case "focusPanelRequest": {
- // Execute the focusPanel command to focus the WebView
- await vscode.commands.executeCommand(getCommand("focusPanel"))
- break
- }
- case "filterMarketplaceItems": {
- if (marketplaceManager && message.filters) {
- try {
- await marketplaceManager.updateWithFilteredItems({
- type: message.filters.type as MarketplaceItemType | undefined,
- search: message.filters.search,
- tags: message.filters.tags,
- })
- await provider.postStateToWebview()
- } catch (error) {
- console.error("Marketplace: Error filtering items:", error)
- vscode.window.showErrorMessage("Failed to filter marketplace items")
- }
- }
- break
- }
- case "fetchMarketplaceData": {
- // Fetch marketplace data on demand
- await provider.fetchMarketplaceData()
- break
- }
- case "installMarketplaceItem": {
- if (marketplaceManager && message.mpItem && message.mpInstallOptions) {
- try {
- const configFilePath = await marketplaceManager.installMarketplaceItem(
- message.mpItem,
- message.mpInstallOptions,
- )
- await provider.postStateToWebview()
- console.log(`Marketplace item installed and config file opened: ${configFilePath}`)
- // Send success message to webview
- provider.postMessageToWebview({
- type: "marketplaceInstallResult",
- success: true,
- slug: message.mpItem.id,
- })
- } catch (error) {
- console.error(`Error installing marketplace item: ${error}`)
- // Send error message to webview
- provider.postMessageToWebview({
- type: "marketplaceInstallResult",
- success: false,
- error: error instanceof Error ? error.message : String(error),
- slug: message.mpItem.id,
- })
- }
- }
- break
- }
- case "removeInstalledMarketplaceItem": {
- if (marketplaceManager && message.mpItem && message.mpInstallOptions) {
- try {
- await marketplaceManager.removeInstalledMarketplaceItem(message.mpItem, message.mpInstallOptions)
- await provider.postStateToWebview()
- // Send success message to webview
- provider.postMessageToWebview({
- type: "marketplaceRemoveResult",
- success: true,
- slug: message.mpItem.id,
- })
- } catch (error) {
- console.error(`Error removing marketplace item: ${error}`)
- // Show error message to user
- vscode.window.showErrorMessage(
- `Failed to remove marketplace item: ${error instanceof Error ? error.message : String(error)}`,
- )
- // Send error message to webview
- provider.postMessageToWebview({
- type: "marketplaceRemoveResult",
- success: false,
- error: error instanceof Error ? error.message : String(error),
- slug: message.mpItem.id,
- })
- }
- } else {
- // MarketplaceManager not available or missing required parameters
- const errorMessage = !marketplaceManager
- ? "Marketplace manager is not available"
- : "Missing required parameters for marketplace item removal"
- console.error(errorMessage)
- vscode.window.showErrorMessage(errorMessage)
- if (message.mpItem?.id) {
- provider.postMessageToWebview({
- type: "marketplaceRemoveResult",
- success: false,
- error: errorMessage,
- slug: message.mpItem.id,
- })
- }
- }
- break
- }
- case "installMarketplaceItemWithParameters": {
- if (marketplaceManager && message.payload && "item" in message.payload && "parameters" in message.payload) {
- try {
- const configFilePath = await marketplaceManager.installMarketplaceItem(message.payload.item, {
- parameters: message.payload.parameters,
- })
- await provider.postStateToWebview()
- console.log(`Marketplace item with parameters installed and config file opened: ${configFilePath}`)
- } catch (error) {
- console.error(`Error installing marketplace item with parameters: ${error}`)
- vscode.window.showErrorMessage(
- `Failed to install marketplace item: ${error instanceof Error ? error.message : String(error)}`,
- )
- }
- }
- break
- }
- case "switchTab": {
- if (message.tab) {
- // Capture tab shown event for all switchTab messages (which are user-initiated).
- if (TelemetryService.hasInstance()) {
- TelemetryService.instance.captureTabShown(message.tab)
- }
- await provider.postMessageToWebview({
- type: "action",
- action: "switchTab",
- tab: message.tab,
- values: message.values,
- })
- }
- break
- }
- case "requestCommands": {
- try {
- const { getCommands } = await import("../../services/command/commands")
- const commands = await getCommands(getCurrentCwd())
- const commandList = commands.map((command) => ({
- name: command.name,
- source: command.source,
- filePath: command.filePath,
- description: command.description,
- argumentHint: command.argumentHint,
- }))
- await provider.postMessageToWebview({ type: "commands", commands: commandList })
- } catch (error) {
- provider.log(`Error fetching commands: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`)
- await provider.postMessageToWebview({ type: "commands", commands: [] })
- }
- break
- }
- case "requestModes": {
- try {
- const modes = await provider.getModes()
- await provider.postMessageToWebview({ type: "modes", modes })
- } catch (error) {
- provider.log(`Error fetching modes: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`)
- await provider.postMessageToWebview({ type: "modes", modes: [] })
- }
- break
- }
- case "openCommandFile": {
- try {
- if (message.text) {
- const { getCommand } = await import("../../services/command/commands")
- const command = await getCommand(getCurrentCwd(), message.text)
- if (command && command.filePath) {
- openFile(command.filePath)
- } else {
- vscode.window.showErrorMessage(t("common:errors.command_not_found", { name: message.text }))
- }
- }
- } catch (error) {
- provider.log(
- `Error opening command file: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
- )
- vscode.window.showErrorMessage(t("common:errors.open_command_file"))
- }
- break
- }
- case "deleteCommand": {
- try {
- if (message.text && message.values?.source) {
- const { getCommand } = await import("../../services/command/commands")
- const command = await getCommand(getCurrentCwd(), message.text)
- if (command && command.filePath) {
- // Delete the command file
- await fs.unlink(command.filePath)
- provider.log(`Deleted command file: ${command.filePath}`)
- } else {
- vscode.window.showErrorMessage(t("common:errors.command_not_found", { name: message.text }))
- }
- }
- } catch (error) {
- provider.log(`Error deleting command: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`)
- vscode.window.showErrorMessage(t("common:errors.delete_command"))
- }
- break
- }
- case "createCommand": {
- try {
- const source = message.values?.source as "global" | "project"
- const fileName = message.text // Custom filename from user input
- if (!source) {
- provider.log("Missing source for createCommand")
- break
- }
- // Determine the commands directory based on source
- let commandsDir: string
- if (source === "global") {
- const globalConfigDir = path.join(os.homedir(), ".roo")
- commandsDir = path.join(globalConfigDir, "commands")
- } else {
- if (!vscode.workspace.workspaceFolders?.length) {
- vscode.window.showErrorMessage(t("common:errors.no_workspace"))
- return
- }
- // Project commands
- const workspaceRoot = getCurrentCwd()
- if (!workspaceRoot) {
- vscode.window.showErrorMessage(t("common:errors.no_workspace_for_project_command"))
- break
- }
- commandsDir = path.join(workspaceRoot, ".roo", "commands")
- }
- // Ensure the commands directory exists
- await fs.mkdir(commandsDir, { recursive: true })
- // Use provided filename or generate a unique one
- let commandName: string
- if (fileName && fileName.trim()) {
- let cleanFileName = fileName.trim()
- // Strip leading slash if present
- if (cleanFileName.startsWith("/")) {
- cleanFileName = cleanFileName.substring(1)
- }
- // Remove .md extension if present BEFORE slugification
- if (cleanFileName.toLowerCase().endsWith(".md")) {
- cleanFileName = cleanFileName.slice(0, -3)
- }
- // Slugify the command name: lowercase, replace spaces with dashes, remove special characters
- commandName = cleanFileName
- .toLowerCase()
- .replace(/\s+/g, "-") // Replace spaces with dashes
- .replace(/[^a-z0-9-]/g, "") // Remove special characters except dashes
- .replace(/-+/g, "-") // Replace multiple dashes with single dash
- .replace(/^-|-$/g, "") // Remove leading/trailing dashes
- // Ensure we have a valid command name
- if (!commandName || commandName.length === 0) {
- commandName = "new-command"
- }
- } else {
- // Generate a unique command name
- commandName = "new-command"
- let counter = 1
- let filePath = path.join(commandsDir, `${commandName}.md`)
- while (
- await fs
- .access(filePath)
- .then(() => true)
- .catch(() => false)
- ) {
- commandName = `new-command-${counter}`
- filePath = path.join(commandsDir, `${commandName}.md`)
- counter++
- }
- }
- const filePath = path.join(commandsDir, `${commandName}.md`)
- // Check if file already exists
- if (
- await fs
- .access(filePath)
- .then(() => true)
- .catch(() => false)
- ) {
- vscode.window.showErrorMessage(t("common:errors.command_already_exists", { commandName }))
- break
- }
- // Create the command file with template content
- const templateContent = t("common:errors.command_template_content")
- await fs.writeFile(filePath, templateContent, "utf8")
- provider.log(`Created new command file: ${filePath}`)
- // Open the new file in the editor
- openFile(filePath)
- // Refresh commands list
- const { getCommands } = await import("../../services/command/commands")
- const commands = await getCommands(getCurrentCwd() || "")
- const commandList = commands.map((command) => ({
- name: command.name,
- source: command.source,
- filePath: command.filePath,
- description: command.description,
- argumentHint: command.argumentHint,
- }))
- await provider.postMessageToWebview({
- type: "commands",
- commands: commandList,
- })
- } catch (error) {
- provider.log(`Error creating command: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`)
- vscode.window.showErrorMessage(t("common:errors.create_command_failed"))
- }
- break
- }
- case "insertTextIntoTextarea": {
- const text = message.text
- if (text) {
- // Send message to insert text into the chat textarea
- await provider.postMessageToWebview({
- type: "insertTextIntoTextarea",
- text: text,
- })
- }
- break
- }
- case "showMdmAuthRequiredNotification": {
- // Show notification that organization requires authentication
- vscode.window.showWarningMessage(t("common:mdm.info.organization_requires_auth"))
- break
- }
- /**
- * Chat Message Queue
- */
- case "queueMessage": {
- const resolved = await resolveIncomingImages({ text: message.text, images: message.images })
- provider.getCurrentTask()?.messageQueueService.addMessage(resolved.text, resolved.images)
- break
- }
- case "removeQueuedMessage": {
- provider.getCurrentTask()?.messageQueueService.removeMessage(message.text ?? "")
- break
- }
- case "editQueuedMessage": {
- if (message.payload) {
- const { id, text, images } = message.payload as EditQueuedMessagePayload
- provider.getCurrentTask()?.messageQueueService.updateMessage(id, text, images)
- }
- break
- }
- case "dismissUpsell": {
- if (message.upsellId) {
- try {
- // Get current list of dismissed upsells
- const dismissedUpsells = getGlobalState("dismissedUpsells") || []
- // Add the new upsell ID if not already present
- let updatedList = dismissedUpsells
- if (!dismissedUpsells.includes(message.upsellId)) {
- updatedList = [...dismissedUpsells, message.upsellId]
- await updateGlobalState("dismissedUpsells", updatedList)
- }
- // Send updated list back to webview (use the already computed updatedList)
- await provider.postMessageToWebview({
- type: "dismissedUpsells",
- list: updatedList,
- })
- } catch (error) {
- // Fail silently as per Bruno's comment - it's OK to fail silently in this case
- provider.log(`Failed to dismiss upsell: ${error instanceof Error ? error.message : String(error)}`)
- }
- }
- break
- }
- case "getDismissedUpsells": {
- // Send the current list of dismissed upsells to the webview
- const dismissedUpsells = getGlobalState("dismissedUpsells") || []
- await provider.postMessageToWebview({
- type: "dismissedUpsells",
- list: dismissedUpsells,
- })
- break
- }
- case "requestClaudeCodeRateLimits": {
- try {
- const { claudeCodeOAuthManager } = await import("../../integrations/claude-code/oauth")
- const accessToken = await claudeCodeOAuthManager.getAccessToken()
- if (!accessToken) {
- provider.postMessageToWebview({
- type: "claudeCodeRateLimits",
- error: "Not authenticated with Claude Code",
- })
- break
- }
- const { fetchRateLimitInfo } = await import("../../integrations/claude-code/streaming-client")
- const rateLimits = await fetchRateLimitInfo(accessToken)
- provider.postMessageToWebview({
- type: "claudeCodeRateLimits",
- values: rateLimits,
- })
- } catch (error) {
- const errorMessage = error instanceof Error ? error.message : String(error)
- provider.log(`Error fetching Claude Code rate limits: ${errorMessage}`)
- provider.postMessageToWebview({
- type: "claudeCodeRateLimits",
- error: errorMessage,
- })
- }
- break
- }
- case "openDebugApiHistory":
- case "openDebugUiHistory": {
- const currentTask = provider.getCurrentTask()
- if (!currentTask) {
- vscode.window.showErrorMessage("No active task to view history for")
- break
- }
- try {
- const { getTaskDirectoryPath } = await import("../../utils/storage")
- const globalStoragePath = provider.contextProxy.globalStorageUri.fsPath
- const taskDirPath = await getTaskDirectoryPath(globalStoragePath, currentTask.taskId)
- const fileName =
- message.type === "openDebugApiHistory" ? "api_conversation_history.json" : "ui_messages.json"
- const sourceFilePath = path.join(taskDirPath, fileName)
- // Check if file exists
- if (!(await fileExistsAtPath(sourceFilePath))) {
- vscode.window.showErrorMessage(`File not found: ${fileName}`)
- break
- }
- // Read the source file
- const content = await fs.readFile(sourceFilePath, "utf8")
- let jsonContent: unknown
- try {
- jsonContent = JSON.parse(content)
- } catch {
- vscode.window.showErrorMessage(`Failed to parse ${fileName}`)
- break
- }
- // Prettify the JSON
- const prettifiedContent = JSON.stringify(jsonContent, null, 2)
- // Create a temporary file
- const tmpDir = os.tmpdir()
- const timestamp = Date.now()
- const tempFileName = `roo-debug-${message.type === "openDebugApiHistory" ? "api" : "ui"}-${currentTask.taskId.slice(0, 8)}-${timestamp}.json`
- const tempFilePath = path.join(tmpDir, tempFileName)
- await fs.writeFile(tempFilePath, prettifiedContent, "utf8")
- // Open the temp file in VS Code
- const doc = await vscode.workspace.openTextDocument(tempFilePath)
- await vscode.window.showTextDocument(doc, { preview: true })
- } catch (error) {
- const errorMessage = error instanceof Error ? error.message : String(error)
- provider.log(`Error opening debug history: ${errorMessage}`)
- vscode.window.showErrorMessage(`Failed to open debug history: ${errorMessage}`)
- }
- break
- }
- case "downloadErrorDiagnostics": {
- const currentTask = provider.getCurrentTask()
- if (!currentTask) {
- vscode.window.showErrorMessage("No active task to generate diagnostics for")
- break
- }
- await generateErrorDiagnostics({
- taskId: currentTask.taskId,
- globalStoragePath: provider.contextProxy.globalStorageUri.fsPath,
- values: message.values,
- log: (msg) => provider.log(msg),
- })
- break
- }
- default: {
- // console.log(`Unhandled message type: ${message.type}`)
- //
- // Currently unhandled:
- //
- // "currentApiConfigName" |
- // "codebaseIndexEnabled" |
- // "enhancedPrompt" |
- // "systemPrompt" |
- // "exportModeResult" |
- // "importModeResult" |
- // "checkRulesDirectoryResult" |
- // "browserConnectionResult" |
- // "vsCodeSetting" |
- // "indexingStatusUpdate" |
- // "indexCleared" |
- // "marketplaceInstallResult" |
- // "shareTaskSuccess" |
- // "playSound" |
- // "draggedImages" |
- // "setApiConfigPassword" |
- // "setopenAiCustomModelInfo" |
- // "marketplaceButtonClicked" |
- // "cancelMarketplaceInstall" |
- // "imageGenerationSettings"
- break
- }
- }
- }
|