ClineProvider.ts 97 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749
  1. import { Anthropic } from "@anthropic-ai/sdk"
  2. import delay from "delay"
  3. import axios from "axios"
  4. import EventEmitter from "events"
  5. import fs from "fs/promises"
  6. import os from "os"
  7. import pWaitFor from "p-wait-for"
  8. import * as path from "path"
  9. import * as vscode from "vscode"
  10. import { setPanel } from "../../activate/registerCommands"
  11. import { ApiConfiguration, ApiProvider, ModelInfo, API_CONFIG_KEYS } from "../../shared/api"
  12. import { findLast } from "../../shared/array"
  13. import { supportPrompt } from "../../shared/support-prompt"
  14. import { GlobalFileNames } from "../../shared/globalFileNames"
  15. import {
  16. SecretKey,
  17. GlobalStateKey,
  18. SECRET_KEYS,
  19. GLOBAL_STATE_KEYS,
  20. ConfigurationValues,
  21. } from "../../shared/globalState"
  22. import { HistoryItem } from "../../shared/HistoryItem"
  23. import { ApiConfigMeta, ExtensionMessage } from "../../shared/ExtensionMessage"
  24. import { checkoutDiffPayloadSchema, checkoutRestorePayloadSchema, WebviewMessage } from "../../shared/WebviewMessage"
  25. import { Mode, PromptComponent, defaultModeSlug, ModeConfig } from "../../shared/modes"
  26. import { checkExistKey } from "../../shared/checkExistApiConfig"
  27. import { EXPERIMENT_IDS, experiments as Experiments, experimentDefault, ExperimentId } from "../../shared/experiments"
  28. import { formatLanguage } from "../../shared/language"
  29. import { downloadTask } from "../../integrations/misc/export-markdown"
  30. import { openFile, openImage } from "../../integrations/misc/open-file"
  31. import { selectImages } from "../../integrations/misc/process-images"
  32. import { getTheme } from "../../integrations/theme/getTheme"
  33. import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
  34. import { McpHub } from "../../services/mcp/McpHub"
  35. import { McpServerManager } from "../../services/mcp/McpServerManager"
  36. import { ShadowCheckpointService } from "../../services/checkpoints/ShadowCheckpointService"
  37. import { BrowserSession } from "../../services/browser/BrowserSession"
  38. import { discoverChromeInstances } from "../../services/browser/browserDiscovery"
  39. import { fileExistsAtPath } from "../../utils/fs"
  40. import { playSound, setSoundEnabled, setSoundVolume } from "../../utils/sound"
  41. import { singleCompletionHandler } from "../../utils/single-completion-handler"
  42. import { searchCommits } from "../../utils/git"
  43. import { getDiffStrategy } from "../diff/DiffStrategy"
  44. import { SYSTEM_PROMPT } from "../prompts/system"
  45. import { ConfigManager } from "../config/ConfigManager"
  46. import { CustomModesManager } from "../config/CustomModesManager"
  47. import { ContextProxy } from "../contextProxy"
  48. import { buildApiHandler } from "../../api"
  49. import { getOpenRouterModels } from "../../api/providers/openrouter"
  50. import { getGlamaModels } from "../../api/providers/glama"
  51. import { getUnboundModels } from "../../api/providers/unbound"
  52. import { getRequestyModels } from "../../api/providers/requesty"
  53. import { getOpenAiModels } from "../../api/providers/openai"
  54. import { getOllamaModels } from "../../api/providers/ollama"
  55. import { getVsCodeLmModels } from "../../api/providers/vscode-lm"
  56. import { getLmStudioModels } from "../../api/providers/lmstudio"
  57. import { ACTION_NAMES } from "../CodeActionProvider"
  58. import { Cline, ClineOptions } from "../Cline"
  59. import { openMention } from "../mentions"
  60. import { getNonce } from "./getNonce"
  61. import { getUri } from "./getUri"
  62. import { telemetryService } from "../../services/telemetry/TelemetryService"
  63. import { TelemetrySetting } from "../../shared/TelemetrySetting"
  64. /**
  65. * https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
  66. * https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/customSidebarViewProvider.ts
  67. */
  68. export type ClineProviderEvents = {
  69. clineAdded: [cline: Cline]
  70. }
  71. export class ClineProvider extends EventEmitter<ClineProviderEvents> implements vscode.WebviewViewProvider {
  72. public static readonly sideBarId = "roo-cline.SidebarProvider" // used in package.json as the view's id. This value cannot be changed due to how vscode caches views based on their id, and updating the id would break existing instances of the extension.
  73. public static readonly tabPanelId = "roo-cline.TabPanelProvider"
  74. private static activeInstances: Set<ClineProvider> = new Set()
  75. private disposables: vscode.Disposable[] = []
  76. private view?: vscode.WebviewView | vscode.WebviewPanel
  77. private isViewLaunched = false
  78. private clineStack: Cline[] = []
  79. private workspaceTracker?: WorkspaceTracker
  80. protected mcpHub?: McpHub // Change from private to protected
  81. private latestAnnouncementId = "mar-7-2025-3-8" // update to some unique identifier when we add a new announcement
  82. private contextProxy: ContextProxy
  83. configManager: ConfigManager
  84. customModesManager: CustomModesManager
  85. constructor(
  86. readonly context: vscode.ExtensionContext,
  87. private readonly outputChannel: vscode.OutputChannel,
  88. ) {
  89. super()
  90. this.outputChannel.appendLine("ClineProvider instantiated")
  91. this.contextProxy = new ContextProxy(context)
  92. ClineProvider.activeInstances.add(this)
  93. // Register this provider with the telemetry service to enable it to add properties like mode and provider
  94. telemetryService.setProvider(this)
  95. this.workspaceTracker = new WorkspaceTracker(this)
  96. this.configManager = new ConfigManager(this.context)
  97. this.customModesManager = new CustomModesManager(this.context, async () => {
  98. await this.postStateToWebview()
  99. })
  100. // Initialize MCP Hub through the singleton manager
  101. McpServerManager.getInstance(this.context, this)
  102. .then((hub) => {
  103. this.mcpHub = hub
  104. })
  105. .catch((error) => {
  106. this.outputChannel.appendLine(`Failed to initialize MCP Hub: ${error}`)
  107. })
  108. }
  109. // Adds a new Cline instance to clineStack, marking the start of a new task.
  110. // The instance is pushed to the top of the stack (LIFO order).
  111. // When the task is completed, the top instance is removed, reactivating the previous task.
  112. async addClineToStack(cline: Cline) {
  113. console.log(`[subtasks] adding task ${cline.taskId}.${cline.instanceId} to stack`)
  114. // Add this cline instance into the stack that represents the order of all the called tasks.
  115. this.clineStack.push(cline)
  116. this.emit("clineAdded", cline)
  117. // Ensure getState() resolves correctly.
  118. const state = await this.getState()
  119. if (!state || typeof state.mode !== "string") {
  120. throw new Error("Error failed to retrieve current mode from state.")
  121. }
  122. }
  123. // Removes and destroys the top Cline instance (the current finished task),
  124. // activating the previous one (resuming the parent task).
  125. async removeClineFromStack() {
  126. if (this.clineStack.length === 0) {
  127. return
  128. }
  129. // Pop the top Cline instance from the stack.
  130. var cline = this.clineStack.pop()
  131. if (cline) {
  132. console.log(`[subtasks] removing task ${cline.taskId}.${cline.instanceId} from stack`)
  133. try {
  134. // Abort the running task and set isAbandoned to true so
  135. // all running promises will exit as well.
  136. await cline.abortTask(true)
  137. } catch (e) {
  138. this.log(
  139. `[subtasks] encountered error while aborting task ${cline.taskId}.${cline.instanceId}: ${e.message}`,
  140. )
  141. }
  142. // Make sure no reference kept, once promises end it will be
  143. // garbage collected.
  144. cline = undefined
  145. }
  146. }
  147. // returns the current cline object in the stack (the top one)
  148. // if the stack is empty, returns undefined
  149. getCurrentCline(): Cline | undefined {
  150. if (this.clineStack.length === 0) {
  151. return undefined
  152. }
  153. return this.clineStack[this.clineStack.length - 1]
  154. }
  155. // returns the current clineStack length (how many cline objects are in the stack)
  156. getClineStackSize(): number {
  157. return this.clineStack.length
  158. }
  159. public getCurrentTaskStack(): string[] {
  160. return this.clineStack.map((cline) => cline.taskId)
  161. }
  162. // remove the current task/cline instance (at the top of the stack), ao this task is finished
  163. // and resume the previous task/cline instance (if it exists)
  164. // this is used when a sub task is finished and the parent task needs to be resumed
  165. async finishSubTask(lastMessage?: string) {
  166. console.log(`[subtasks] finishing subtask ${lastMessage}`)
  167. // remove the last cline instance from the stack (this is the finished sub task)
  168. await this.removeClineFromStack()
  169. // resume the last cline instance in the stack (if it exists - this is the 'parnt' calling task)
  170. this.getCurrentCline()?.resumePausedTask(lastMessage)
  171. }
  172. /*
  173. VSCode extensions use the disposable pattern to clean up resources when the sidebar/editor tab is closed by the user or system. This applies to event listening, commands, interacting with the UI, etc.
  174. - https://vscode-docs.readthedocs.io/en/stable/extensions/patterns-and-principles/
  175. - https://github.com/microsoft/vscode-extension-samples/blob/main/webview-sample/src/extension.ts
  176. */
  177. async dispose() {
  178. this.outputChannel.appendLine("Disposing ClineProvider...")
  179. await this.removeClineFromStack()
  180. this.outputChannel.appendLine("Cleared task")
  181. if (this.view && "dispose" in this.view) {
  182. this.view.dispose()
  183. this.outputChannel.appendLine("Disposed webview")
  184. }
  185. while (this.disposables.length) {
  186. const x = this.disposables.pop()
  187. if (x) {
  188. x.dispose()
  189. }
  190. }
  191. this.workspaceTracker?.dispose()
  192. this.workspaceTracker = undefined
  193. this.mcpHub?.dispose()
  194. this.mcpHub = undefined
  195. this.customModesManager?.dispose()
  196. this.outputChannel.appendLine("Disposed all disposables")
  197. ClineProvider.activeInstances.delete(this)
  198. // Unregister from McpServerManager
  199. McpServerManager.unregisterProvider(this)
  200. }
  201. public static getVisibleInstance(): ClineProvider | undefined {
  202. return findLast(Array.from(this.activeInstances), (instance) => instance.view?.visible === true)
  203. }
  204. public static async getInstance(): Promise<ClineProvider | undefined> {
  205. let visibleProvider = ClineProvider.getVisibleInstance()
  206. // If no visible provider, try to show the sidebar view
  207. if (!visibleProvider) {
  208. await vscode.commands.executeCommand("roo-cline.SidebarProvider.focus")
  209. // Wait briefly for the view to become visible
  210. await delay(100)
  211. visibleProvider = ClineProvider.getVisibleInstance()
  212. }
  213. // If still no visible provider, return
  214. if (!visibleProvider) {
  215. return
  216. }
  217. return visibleProvider
  218. }
  219. public static async isActiveTask(): Promise<boolean> {
  220. const visibleProvider = await ClineProvider.getInstance()
  221. if (!visibleProvider) {
  222. return false
  223. }
  224. // check if there is a cline instance in the stack (if this provider has an active task)
  225. if (visibleProvider.getCurrentCline()) {
  226. return true
  227. }
  228. return false
  229. }
  230. public static async handleCodeAction(
  231. command: string,
  232. promptType: keyof typeof ACTION_NAMES,
  233. params: Record<string, string | any[]>,
  234. ): Promise<void> {
  235. const visibleProvider = await ClineProvider.getInstance()
  236. if (!visibleProvider) {
  237. return
  238. }
  239. const { customSupportPrompts } = await visibleProvider.getState()
  240. const prompt = supportPrompt.create(promptType, params, customSupportPrompts)
  241. if (command.endsWith("addToContext")) {
  242. await visibleProvider.postMessageToWebview({
  243. type: "invoke",
  244. invoke: "setChatBoxMessage",
  245. text: prompt,
  246. })
  247. return
  248. }
  249. if (visibleProvider.getCurrentCline() && command.endsWith("InCurrentTask")) {
  250. await visibleProvider.postMessageToWebview({ type: "invoke", invoke: "sendMessage", text: prompt })
  251. return
  252. }
  253. await visibleProvider.initClineWithTask(prompt)
  254. }
  255. public static async handleTerminalAction(
  256. command: string,
  257. promptType: "TERMINAL_ADD_TO_CONTEXT" | "TERMINAL_FIX" | "TERMINAL_EXPLAIN",
  258. params: Record<string, string | any[]>,
  259. ): Promise<void> {
  260. const visibleProvider = await ClineProvider.getInstance()
  261. if (!visibleProvider) {
  262. return
  263. }
  264. const { customSupportPrompts } = await visibleProvider.getState()
  265. const prompt = supportPrompt.create(promptType, params, customSupportPrompts)
  266. if (command.endsWith("AddToContext")) {
  267. await visibleProvider.postMessageToWebview({
  268. type: "invoke",
  269. invoke: "setChatBoxMessage",
  270. text: prompt,
  271. })
  272. return
  273. }
  274. if (visibleProvider.getCurrentCline() && command.endsWith("InCurrentTask")) {
  275. await visibleProvider.postMessageToWebview({
  276. type: "invoke",
  277. invoke: "sendMessage",
  278. text: prompt,
  279. })
  280. return
  281. }
  282. await visibleProvider.initClineWithTask(prompt)
  283. }
  284. async resolveWebviewView(webviewView: vscode.WebviewView | vscode.WebviewPanel) {
  285. this.outputChannel.appendLine("Resolving webview view")
  286. if (!this.contextProxy.isInitialized) {
  287. await this.contextProxy.initialize()
  288. }
  289. this.view = webviewView
  290. // Set panel reference according to webview type
  291. if ("onDidChangeViewState" in webviewView) {
  292. // Tag page type
  293. setPanel(webviewView, "tab")
  294. } else if ("onDidChangeVisibility" in webviewView) {
  295. // Sidebar Type
  296. setPanel(webviewView, "sidebar")
  297. }
  298. // Initialize sound enabled state
  299. this.getState().then(({ soundEnabled }) => {
  300. setSoundEnabled(soundEnabled ?? false)
  301. })
  302. webviewView.webview.options = {
  303. // Allow scripts in the webview
  304. enableScripts: true,
  305. localResourceRoots: [this.contextProxy.extensionUri],
  306. }
  307. webviewView.webview.html =
  308. this.contextProxy.extensionMode === vscode.ExtensionMode.Development
  309. ? await this.getHMRHtmlContent(webviewView.webview)
  310. : this.getHtmlContent(webviewView.webview)
  311. // Sets up an event listener to listen for messages passed from the webview view context
  312. // and executes code based on the message that is recieved
  313. this.setWebviewMessageListener(webviewView.webview)
  314. // Logs show up in bottom panel > Debug Console
  315. //console.log("registering listener")
  316. // Listen for when the panel becomes visible
  317. // https://github.com/microsoft/vscode-discussions/discussions/840
  318. if ("onDidChangeViewState" in webviewView) {
  319. // WebviewView and WebviewPanel have all the same properties except for this visibility listener
  320. // panel
  321. webviewView.onDidChangeViewState(
  322. () => {
  323. if (this.view?.visible) {
  324. this.postMessageToWebview({ type: "action", action: "didBecomeVisible" })
  325. }
  326. },
  327. null,
  328. this.disposables,
  329. )
  330. } else if ("onDidChangeVisibility" in webviewView) {
  331. // sidebar
  332. webviewView.onDidChangeVisibility(
  333. () => {
  334. if (this.view?.visible) {
  335. this.postMessageToWebview({ type: "action", action: "didBecomeVisible" })
  336. }
  337. },
  338. null,
  339. this.disposables,
  340. )
  341. }
  342. // Listen for when the view is disposed
  343. // This happens when the user closes the view or when the view is closed programmatically
  344. webviewView.onDidDispose(
  345. async () => {
  346. await this.dispose()
  347. },
  348. null,
  349. this.disposables,
  350. )
  351. // Listen for when color changes
  352. vscode.workspace.onDidChangeConfiguration(
  353. async (e) => {
  354. if (e && e.affectsConfiguration("workbench.colorTheme")) {
  355. // Sends latest theme name to webview
  356. await this.postMessageToWebview({ type: "theme", text: JSON.stringify(await getTheme()) })
  357. }
  358. },
  359. null,
  360. this.disposables,
  361. )
  362. // If the extension is starting a new session, clear previous task state.
  363. await this.removeClineFromStack()
  364. this.outputChannel.appendLine("Webview view resolved")
  365. }
  366. public async initClineWithSubTask(parent: Cline, task?: string, images?: string[]) {
  367. return this.initClineWithTask(task, images, parent)
  368. }
  369. // when initializing a new task, (not from history but from a tool command new_task) there is no need to remove the previouse task
  370. // since the new task is a sub task of the previous one, and when it finishes it is removed from the stack and the caller is resumed
  371. // in this way we can have a chain of tasks, each one being a sub task of the previous one until the main task is finished
  372. public async initClineWithTask(task?: string, images?: string[], parentTask?: Cline) {
  373. const {
  374. apiConfiguration,
  375. customModePrompts,
  376. diffEnabled: enableDiff,
  377. enableCheckpoints,
  378. checkpointStorage,
  379. fuzzyMatchThreshold,
  380. mode,
  381. customInstructions: globalInstructions,
  382. experiments,
  383. } = await this.getState()
  384. const modePrompt = customModePrompts?.[mode] as PromptComponent
  385. const effectiveInstructions = [globalInstructions, modePrompt?.customInstructions].filter(Boolean).join("\n\n")
  386. const cline = new Cline({
  387. provider: this,
  388. apiConfiguration,
  389. customInstructions: effectiveInstructions,
  390. enableDiff,
  391. enableCheckpoints,
  392. checkpointStorage,
  393. fuzzyMatchThreshold,
  394. task,
  395. images,
  396. experiments,
  397. rootTask: this.clineStack.length > 0 ? this.clineStack[0] : undefined,
  398. parentTask,
  399. taskNumber: this.clineStack.length + 1,
  400. })
  401. await this.addClineToStack(cline)
  402. this.log(
  403. `[subtasks] ${cline.parentTask ? "child" : "parent"} task ${cline.taskId}.${cline.instanceId} instantiated`,
  404. )
  405. return cline
  406. }
  407. public async initClineWithHistoryItem(historyItem: HistoryItem & { rootTask?: Cline; parentTask?: Cline }) {
  408. await this.removeClineFromStack()
  409. const {
  410. apiConfiguration,
  411. customModePrompts,
  412. diffEnabled: enableDiff,
  413. enableCheckpoints,
  414. checkpointStorage,
  415. fuzzyMatchThreshold,
  416. mode,
  417. customInstructions: globalInstructions,
  418. experiments,
  419. } = await this.getState()
  420. const modePrompt = customModePrompts?.[mode] as PromptComponent
  421. const effectiveInstructions = [globalInstructions, modePrompt?.customInstructions].filter(Boolean).join("\n\n")
  422. const taskId = historyItem.id
  423. const globalStorageDir = this.contextProxy.globalStorageUri.fsPath
  424. const workspaceDir = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? ""
  425. const checkpoints: Pick<ClineOptions, "enableCheckpoints" | "checkpointStorage"> = {
  426. enableCheckpoints,
  427. checkpointStorage,
  428. }
  429. if (enableCheckpoints) {
  430. try {
  431. checkpoints.checkpointStorage = await ShadowCheckpointService.getTaskStorage({
  432. taskId,
  433. globalStorageDir,
  434. workspaceDir,
  435. })
  436. this.log(
  437. `[ClineProvider#initClineWithHistoryItem] Using ${checkpoints.checkpointStorage} storage for ${taskId}`,
  438. )
  439. } catch (error) {
  440. checkpoints.enableCheckpoints = false
  441. this.log(`[ClineProvider#initClineWithHistoryItem] Error getting task storage: ${error.message}`)
  442. }
  443. }
  444. const cline = new Cline({
  445. provider: this,
  446. apiConfiguration,
  447. customInstructions: effectiveInstructions,
  448. enableDiff,
  449. ...checkpoints,
  450. fuzzyMatchThreshold,
  451. historyItem,
  452. experiments,
  453. rootTask: historyItem.rootTask,
  454. parentTask: historyItem.parentTask,
  455. taskNumber: historyItem.number,
  456. })
  457. await this.addClineToStack(cline)
  458. this.log(
  459. `[subtasks] ${cline.parentTask ? "child" : "parent"} task ${cline.taskId}.${cline.instanceId} instantiated`,
  460. )
  461. return cline
  462. }
  463. public async postMessageToWebview(message: ExtensionMessage) {
  464. await this.view?.webview.postMessage(message)
  465. }
  466. private async getHMRHtmlContent(webview: vscode.Webview): Promise<string> {
  467. const localPort = "5173"
  468. const localServerUrl = `localhost:${localPort}`
  469. // Check if local dev server is running.
  470. try {
  471. await axios.get(`http://${localServerUrl}`)
  472. } catch (error) {
  473. vscode.window.showErrorMessage(
  474. "Local development server is not running, HMR will not work. Please run 'npm run dev' before launching the extension to enable HMR.",
  475. )
  476. return this.getHtmlContent(webview)
  477. }
  478. const nonce = getNonce()
  479. const stylesUri = getUri(webview, this.contextProxy.extensionUri, [
  480. "webview-ui",
  481. "build",
  482. "assets",
  483. "index.css",
  484. ])
  485. const codiconsUri = getUri(webview, this.contextProxy.extensionUri, [
  486. "node_modules",
  487. "@vscode",
  488. "codicons",
  489. "dist",
  490. "codicon.css",
  491. ])
  492. const file = "src/index.tsx"
  493. const scriptUri = `http://${localServerUrl}/${file}`
  494. const reactRefresh = /*html*/ `
  495. <script nonce="${nonce}" type="module">
  496. import RefreshRuntime from "http://localhost:${localPort}/@react-refresh"
  497. RefreshRuntime.injectIntoGlobalHook(window)
  498. window.$RefreshReg$ = () => {}
  499. window.$RefreshSig$ = () => (type) => type
  500. window.__vite_plugin_react_preamble_installed__ = true
  501. </script>
  502. `
  503. const csp = [
  504. "default-src 'none'",
  505. `font-src ${webview.cspSource}`,
  506. `style-src ${webview.cspSource} 'unsafe-inline' https://* http://${localServerUrl} http://0.0.0.0:${localPort}`,
  507. `img-src ${webview.cspSource} data:`,
  508. `script-src 'unsafe-eval' https://* https://*.posthog.com http://${localServerUrl} http://0.0.0.0:${localPort} 'nonce-${nonce}'`,
  509. `connect-src https://* https://*.posthog.com ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`,
  510. ]
  511. return /*html*/ `
  512. <!DOCTYPE html>
  513. <html lang="en">
  514. <head>
  515. <meta charset="utf-8">
  516. <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
  517. <meta http-equiv="Content-Security-Policy" content="${csp.join("; ")}">
  518. <link rel="stylesheet" type="text/css" href="${stylesUri}">
  519. <link href="${codiconsUri}" rel="stylesheet" />
  520. <title>Roo Code</title>
  521. </head>
  522. <body>
  523. <div id="root"></div>
  524. ${reactRefresh}
  525. <script type="module" src="${scriptUri}"></script>
  526. </body>
  527. </html>
  528. `
  529. }
  530. /**
  531. * Defines and returns the HTML that should be rendered within the webview panel.
  532. *
  533. * @remarks This is also the place where references to the React webview build files
  534. * are created and inserted into the webview HTML.
  535. *
  536. * @param webview A reference to the extension webview
  537. * @param extensionUri The URI of the directory containing the extension
  538. * @returns A template string literal containing the HTML that should be
  539. * rendered within the webview panel
  540. */
  541. private getHtmlContent(webview: vscode.Webview): string {
  542. // Get the local path to main script run in the webview,
  543. // then convert it to a uri we can use in the webview.
  544. // The CSS file from the React build output
  545. const stylesUri = getUri(webview, this.contextProxy.extensionUri, [
  546. "webview-ui",
  547. "build",
  548. "assets",
  549. "index.css",
  550. ])
  551. // The JS file from the React build output
  552. const scriptUri = getUri(webview, this.contextProxy.extensionUri, ["webview-ui", "build", "assets", "index.js"])
  553. // The codicon font from the React build output
  554. // https://github.com/microsoft/vscode-extension-samples/blob/main/webview-codicons-sample/src/extension.ts
  555. // we installed this package in the extension so that we can access it how its intended from the extension (the font file is likely bundled in vscode), and we just import the css fileinto our react app we don't have access to it
  556. // don't forget to add font-src ${webview.cspSource};
  557. const codiconsUri = getUri(webview, this.contextProxy.extensionUri, [
  558. "node_modules",
  559. "@vscode",
  560. "codicons",
  561. "dist",
  562. "codicon.css",
  563. ])
  564. // const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "main.js"))
  565. // const styleResetUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "reset.css"))
  566. // const styleVSCodeUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "vscode.css"))
  567. // // Same for stylesheet
  568. // const stylesheetUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "main.css"))
  569. // Use a nonce to only allow a specific script to be run.
  570. /*
  571. content security policy of your webview to only allow scripts that have a specific nonce
  572. create a content security policy meta tag so that only loading scripts with a nonce is allowed
  573. As your extension grows you will likely want to add custom styles, fonts, and/or images to your webview. If you do, you will need to update the content security policy meta tag to explicity allow for these resources. E.g.
  574. <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource}; font-src ${webview.cspSource}; img-src ${webview.cspSource} https:; script-src 'nonce-${nonce}';">
  575. - 'unsafe-inline' is required for styles due to vscode-webview-toolkit's dynamic style injection
  576. - since we pass base64 images to the webview, we need to specify img-src ${webview.cspSource} data:;
  577. in meta tag we add nonce attribute: A cryptographic nonce (only used once) to allow scripts. The server must generate a unique nonce value each time it transmits a policy. It is critical to provide a nonce that cannot be guessed as bypassing a resource's policy is otherwise trivial.
  578. */
  579. const nonce = getNonce()
  580. // Tip: Install the es6-string-html VS Code extension to enable code highlighting below
  581. return /*html*/ `
  582. <!DOCTYPE html>
  583. <html lang="en">
  584. <head>
  585. <meta charset="utf-8">
  586. <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
  587. <meta name="theme-color" content="#000000">
  588. <meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src ${webview.cspSource}; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} data:; script-src 'nonce-${nonce}' https://us-assets.i.posthog.com; connect-src https://us.i.posthog.com https://us-assets.i.posthog.com;">
  589. <link rel="stylesheet" type="text/css" href="${stylesUri}">
  590. <link href="${codiconsUri}" rel="stylesheet" />
  591. <title>Roo Code</title>
  592. </head>
  593. <body>
  594. <noscript>You need to enable JavaScript to run this app.</noscript>
  595. <div id="root"></div>
  596. <script nonce="${nonce}" type="module" src="${scriptUri}"></script>
  597. </body>
  598. </html>
  599. `
  600. }
  601. /**
  602. * Sets up an event listener to listen for messages passed from the webview context and
  603. * executes code based on the message that is recieved.
  604. *
  605. * @param webview A reference to the extension webview
  606. */
  607. private setWebviewMessageListener(webview: vscode.Webview) {
  608. webview.onDidReceiveMessage(
  609. async (message: WebviewMessage) => {
  610. switch (message.type) {
  611. case "webviewDidLaunch":
  612. // Load custom modes first
  613. const customModes = await this.customModesManager.getCustomModes()
  614. await this.updateGlobalState("customModes", customModes)
  615. this.postStateToWebview()
  616. this.workspaceTracker?.initializeFilePaths() // don't await
  617. getTheme().then((theme) =>
  618. this.postMessageToWebview({ type: "theme", text: JSON.stringify(theme) }),
  619. )
  620. // If MCP Hub is already initialized, update the webview with current server list
  621. if (this.mcpHub) {
  622. this.postMessageToWebview({
  623. type: "mcpServers",
  624. mcpServers: this.mcpHub.getAllServers(),
  625. })
  626. }
  627. const cacheDir = await this.ensureCacheDirectoryExists()
  628. // Post last cached models in case the call to endpoint fails.
  629. this.readModelsFromCache(GlobalFileNames.openRouterModels).then((openRouterModels) => {
  630. if (openRouterModels) {
  631. this.postMessageToWebview({ type: "openRouterModels", openRouterModels })
  632. }
  633. })
  634. // GUI relies on model info to be up-to-date to provide
  635. // the most accurate pricing, so we need to fetch the
  636. // latest details on launch.
  637. // We do this for all users since many users switch
  638. // between api providers and if they were to switch back
  639. // to OpenRouter it would be showing outdated model info
  640. // if we hadn't retrieved the latest at this point
  641. // (see normalizeApiConfiguration > openrouter).
  642. const { apiConfiguration: currentApiConfig } = await this.getState()
  643. getOpenRouterModels(currentApiConfig).then(async (openRouterModels) => {
  644. if (Object.keys(openRouterModels).length > 0) {
  645. await fs.writeFile(
  646. path.join(cacheDir, GlobalFileNames.openRouterModels),
  647. JSON.stringify(openRouterModels),
  648. )
  649. await this.postMessageToWebview({ type: "openRouterModels", openRouterModels })
  650. // Update model info in state (this needs to be
  651. // done here since we don't want to update state
  652. // while settings is open, and we may refresh
  653. // models there).
  654. const { apiConfiguration } = await this.getState()
  655. if (apiConfiguration.openRouterModelId) {
  656. await this.updateGlobalState(
  657. "openRouterModelInfo",
  658. openRouterModels[apiConfiguration.openRouterModelId],
  659. )
  660. await this.postStateToWebview()
  661. }
  662. }
  663. })
  664. this.readModelsFromCache(GlobalFileNames.glamaModels).then((glamaModels) => {
  665. if (glamaModels) {
  666. this.postMessageToWebview({ type: "glamaModels", glamaModels })
  667. }
  668. })
  669. getGlamaModels().then(async (glamaModels) => {
  670. if (Object.keys(glamaModels).length > 0) {
  671. await fs.writeFile(
  672. path.join(cacheDir, GlobalFileNames.glamaModels),
  673. JSON.stringify(glamaModels),
  674. )
  675. await this.postMessageToWebview({ type: "glamaModels", glamaModels })
  676. const { apiConfiguration } = await this.getState()
  677. if (apiConfiguration.glamaModelId) {
  678. await this.updateGlobalState(
  679. "glamaModelInfo",
  680. glamaModels[apiConfiguration.glamaModelId],
  681. )
  682. await this.postStateToWebview()
  683. }
  684. }
  685. })
  686. this.readModelsFromCache(GlobalFileNames.unboundModels).then((unboundModels) => {
  687. if (unboundModels) {
  688. this.postMessageToWebview({ type: "unboundModels", unboundModels })
  689. }
  690. })
  691. getUnboundModels().then(async (unboundModels) => {
  692. if (Object.keys(unboundModels).length > 0) {
  693. await fs.writeFile(
  694. path.join(cacheDir, GlobalFileNames.unboundModels),
  695. JSON.stringify(unboundModels),
  696. )
  697. await this.postMessageToWebview({ type: "unboundModels", unboundModels })
  698. const { apiConfiguration } = await this.getState()
  699. if (apiConfiguration?.unboundModelId) {
  700. await this.updateGlobalState(
  701. "unboundModelInfo",
  702. unboundModels[apiConfiguration.unboundModelId],
  703. )
  704. await this.postStateToWebview()
  705. }
  706. }
  707. })
  708. this.readModelsFromCache(GlobalFileNames.requestyModels).then((requestyModels) => {
  709. if (requestyModels) {
  710. this.postMessageToWebview({ type: "requestyModels", requestyModels })
  711. }
  712. })
  713. getRequestyModels().then(async (requestyModels) => {
  714. if (Object.keys(requestyModels).length > 0) {
  715. await fs.writeFile(
  716. path.join(cacheDir, GlobalFileNames.requestyModels),
  717. JSON.stringify(requestyModels),
  718. )
  719. await this.postMessageToWebview({ type: "requestyModels", requestyModels })
  720. const { apiConfiguration } = await this.getState()
  721. if (apiConfiguration.requestyModelId) {
  722. await this.updateGlobalState(
  723. "requestyModelInfo",
  724. requestyModels[apiConfiguration.requestyModelId],
  725. )
  726. await this.postStateToWebview()
  727. }
  728. }
  729. })
  730. this.configManager
  731. .listConfig()
  732. .then(async (listApiConfig) => {
  733. if (!listApiConfig) {
  734. return
  735. }
  736. if (listApiConfig.length === 1) {
  737. // check if first time init then sync with exist config
  738. if (!checkExistKey(listApiConfig[0])) {
  739. const { apiConfiguration } = await this.getState()
  740. await this.configManager.saveConfig(
  741. listApiConfig[0].name ?? "default",
  742. apiConfiguration,
  743. )
  744. listApiConfig[0].apiProvider = apiConfiguration.apiProvider
  745. }
  746. }
  747. const currentConfigName = (await this.getGlobalState("currentApiConfigName")) as string
  748. if (currentConfigName) {
  749. if (!(await this.configManager.hasConfig(currentConfigName))) {
  750. // current config name not valid, get first config in list
  751. await this.updateGlobalState("currentApiConfigName", listApiConfig?.[0]?.name)
  752. if (listApiConfig?.[0]?.name) {
  753. const apiConfig = await this.configManager.loadConfig(
  754. listApiConfig?.[0]?.name,
  755. )
  756. await Promise.all([
  757. this.updateGlobalState("listApiConfigMeta", listApiConfig),
  758. this.postMessageToWebview({ type: "listApiConfig", listApiConfig }),
  759. this.updateApiConfiguration(apiConfig),
  760. ])
  761. await this.postStateToWebview()
  762. return
  763. }
  764. }
  765. }
  766. await Promise.all([
  767. await this.updateGlobalState("listApiConfigMeta", listApiConfig),
  768. await this.postMessageToWebview({ type: "listApiConfig", listApiConfig }),
  769. ])
  770. })
  771. .catch((error) =>
  772. this.outputChannel.appendLine(
  773. `Error list api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  774. ),
  775. )
  776. // If user already opted in to telemetry, enable telemetry service
  777. this.getStateToPostToWebview().then((state) => {
  778. const { telemetrySetting } = state
  779. const isOptedIn = telemetrySetting === "enabled"
  780. telemetryService.updateTelemetryState(isOptedIn)
  781. })
  782. this.isViewLaunched = true
  783. break
  784. case "newTask":
  785. // Code that should run in response to the hello message command
  786. //vscode.window.showInformationMessage(message.text!)
  787. // Send a message to our webview.
  788. // You can send any JSON serializable data.
  789. // Could also do this in extension .ts
  790. //this.postMessageToWebview({ type: "text", text: `Extension: ${Date.now()}` })
  791. // 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
  792. await this.initClineWithTask(message.text, message.images)
  793. break
  794. case "apiConfiguration":
  795. if (message.apiConfiguration) {
  796. await this.updateApiConfiguration(message.apiConfiguration)
  797. }
  798. await this.postStateToWebview()
  799. break
  800. case "customInstructions":
  801. await this.updateCustomInstructions(message.text)
  802. break
  803. case "alwaysAllowReadOnly":
  804. await this.updateGlobalState("alwaysAllowReadOnly", message.bool ?? undefined)
  805. await this.postStateToWebview()
  806. break
  807. case "alwaysAllowWrite":
  808. await this.updateGlobalState("alwaysAllowWrite", message.bool ?? undefined)
  809. await this.postStateToWebview()
  810. break
  811. case "alwaysAllowExecute":
  812. await this.updateGlobalState("alwaysAllowExecute", message.bool ?? undefined)
  813. await this.postStateToWebview()
  814. break
  815. case "alwaysAllowBrowser":
  816. await this.updateGlobalState("alwaysAllowBrowser", message.bool ?? undefined)
  817. await this.postStateToWebview()
  818. break
  819. case "alwaysAllowMcp":
  820. await this.updateGlobalState("alwaysAllowMcp", message.bool)
  821. await this.postStateToWebview()
  822. break
  823. case "alwaysAllowModeSwitch":
  824. await this.updateGlobalState("alwaysAllowModeSwitch", message.bool)
  825. await this.postStateToWebview()
  826. break
  827. case "alwaysAllowSubtasks":
  828. await this.updateGlobalState("alwaysAllowSubtasks", message.bool)
  829. await this.postStateToWebview()
  830. break
  831. case "askResponse":
  832. this.getCurrentCline()?.handleWebviewAskResponse(
  833. message.askResponse!,
  834. message.text,
  835. message.images,
  836. )
  837. break
  838. case "clearTask":
  839. // clear task resets the current session and allows for a new task to be started, if this session is a subtask - it allows the parent task to be resumed
  840. await this.finishSubTask(`Task error: It was stopped and canceled by the user.`)
  841. await this.postStateToWebview()
  842. break
  843. case "didShowAnnouncement":
  844. await this.updateGlobalState("lastShownAnnouncementId", this.latestAnnouncementId)
  845. await this.postStateToWebview()
  846. break
  847. case "selectImages":
  848. const images = await selectImages()
  849. await this.postMessageToWebview({ type: "selectedImages", images })
  850. break
  851. case "exportCurrentTask":
  852. const currentTaskId = this.getCurrentCline()?.taskId
  853. if (currentTaskId) {
  854. this.exportTaskWithId(currentTaskId)
  855. }
  856. break
  857. case "showTaskWithId":
  858. this.showTaskWithId(message.text!)
  859. break
  860. case "deleteTaskWithId":
  861. this.deleteTaskWithId(message.text!)
  862. break
  863. case "exportTaskWithId":
  864. this.exportTaskWithId(message.text!)
  865. break
  866. case "resetState":
  867. await this.resetState()
  868. break
  869. case "refreshOpenRouterModels": {
  870. const { apiConfiguration: configForRefresh } = await this.getState()
  871. const openRouterModels = await getOpenRouterModels(configForRefresh)
  872. if (Object.keys(openRouterModels).length > 0) {
  873. const cacheDir = await this.ensureCacheDirectoryExists()
  874. await fs.writeFile(
  875. path.join(cacheDir, GlobalFileNames.openRouterModels),
  876. JSON.stringify(openRouterModels),
  877. )
  878. await this.postMessageToWebview({ type: "openRouterModels", openRouterModels })
  879. }
  880. break
  881. }
  882. case "refreshGlamaModels":
  883. const glamaModels = await getGlamaModels()
  884. if (Object.keys(glamaModels).length > 0) {
  885. const cacheDir = await this.ensureCacheDirectoryExists()
  886. await fs.writeFile(
  887. path.join(cacheDir, GlobalFileNames.glamaModels),
  888. JSON.stringify(glamaModels),
  889. )
  890. await this.postMessageToWebview({ type: "glamaModels", glamaModels })
  891. }
  892. break
  893. case "refreshUnboundModels":
  894. const unboundModels = await getUnboundModels()
  895. if (Object.keys(unboundModels).length > 0) {
  896. const cacheDir = await this.ensureCacheDirectoryExists()
  897. await fs.writeFile(
  898. path.join(cacheDir, GlobalFileNames.unboundModels),
  899. JSON.stringify(unboundModels),
  900. )
  901. await this.postMessageToWebview({ type: "unboundModels", unboundModels })
  902. }
  903. break
  904. case "refreshRequestyModels":
  905. const requestyModels = await getRequestyModels()
  906. if (Object.keys(requestyModels).length > 0) {
  907. const cacheDir = await this.ensureCacheDirectoryExists()
  908. await fs.writeFile(
  909. path.join(cacheDir, GlobalFileNames.requestyModels),
  910. JSON.stringify(requestyModels),
  911. )
  912. await this.postMessageToWebview({ type: "requestyModels", requestyModels })
  913. }
  914. break
  915. case "refreshOpenAiModels":
  916. if (message?.values?.baseUrl && message?.values?.apiKey) {
  917. const openAiModels = await getOpenAiModels(
  918. message?.values?.baseUrl,
  919. message?.values?.apiKey,
  920. )
  921. this.postMessageToWebview({ type: "openAiModels", openAiModels })
  922. }
  923. break
  924. case "requestOllamaModels":
  925. const ollamaModels = await getOllamaModels(message.text)
  926. // TODO: Cache like we do for OpenRouter, etc?
  927. this.postMessageToWebview({ type: "ollamaModels", ollamaModels })
  928. break
  929. case "requestLmStudioModels":
  930. const lmStudioModels = await getLmStudioModels(message.text)
  931. // TODO: Cache like we do for OpenRouter, etc?
  932. this.postMessageToWebview({ type: "lmStudioModels", lmStudioModels })
  933. break
  934. case "requestVsCodeLmModels":
  935. const vsCodeLmModels = await getVsCodeLmModels()
  936. // TODO: Cache like we do for OpenRouter, etc?
  937. this.postMessageToWebview({ type: "vsCodeLmModels", vsCodeLmModels })
  938. break
  939. case "openImage":
  940. openImage(message.text!)
  941. break
  942. case "openFile":
  943. openFile(message.text!, message.values as { create?: boolean; content?: string })
  944. break
  945. case "openMention":
  946. openMention(message.text)
  947. break
  948. case "checkpointDiff":
  949. const result = checkoutDiffPayloadSchema.safeParse(message.payload)
  950. if (result.success) {
  951. await this.getCurrentCline()?.checkpointDiff(result.data)
  952. }
  953. break
  954. case "checkpointRestore": {
  955. const result = checkoutRestorePayloadSchema.safeParse(message.payload)
  956. if (result.success) {
  957. await this.cancelTask()
  958. try {
  959. await pWaitFor(() => this.getCurrentCline()?.isInitialized === true, { timeout: 3_000 })
  960. } catch (error) {
  961. vscode.window.showErrorMessage("Timed out when attempting to restore checkpoint.")
  962. }
  963. try {
  964. await this.getCurrentCline()?.checkpointRestore(result.data)
  965. } catch (error) {
  966. vscode.window.showErrorMessage("Failed to restore checkpoint.")
  967. }
  968. }
  969. break
  970. }
  971. case "cancelTask":
  972. await this.cancelTask()
  973. break
  974. case "allowedCommands":
  975. await this.context.globalState.update("allowedCommands", message.commands)
  976. // Also update workspace settings
  977. await vscode.workspace
  978. .getConfiguration("roo-cline")
  979. .update("allowedCommands", message.commands, vscode.ConfigurationTarget.Global)
  980. break
  981. case "openMcpSettings": {
  982. const mcpSettingsFilePath = await this.mcpHub?.getMcpSettingsFilePath()
  983. if (mcpSettingsFilePath) {
  984. openFile(mcpSettingsFilePath)
  985. }
  986. break
  987. }
  988. case "openProjectMcpSettings": {
  989. if (!vscode.workspace.workspaceFolders?.length) {
  990. vscode.window.showErrorMessage("Please open a project folder first")
  991. return
  992. }
  993. const workspaceFolder = vscode.workspace.workspaceFolders[0]
  994. const rooDir = path.join(workspaceFolder.uri.fsPath, ".roo")
  995. const mcpPath = path.join(rooDir, "mcp.json")
  996. try {
  997. await fs.mkdir(rooDir, { recursive: true })
  998. const exists = await fileExistsAtPath(mcpPath)
  999. if (!exists) {
  1000. await fs.writeFile(mcpPath, JSON.stringify({ mcpServers: {} }, null, 2))
  1001. }
  1002. await openFile(mcpPath)
  1003. } catch (error) {
  1004. vscode.window.showErrorMessage(`Failed to create or open .roo/mcp.json: ${error}`)
  1005. }
  1006. break
  1007. }
  1008. case "openCustomModesSettings": {
  1009. const customModesFilePath = await this.customModesManager.getCustomModesFilePath()
  1010. if (customModesFilePath) {
  1011. openFile(customModesFilePath)
  1012. }
  1013. break
  1014. }
  1015. case "deleteMcpServer": {
  1016. if (!message.serverName) {
  1017. break
  1018. }
  1019. try {
  1020. this.outputChannel.appendLine(`Attempting to delete MCP server: ${message.serverName}`)
  1021. await this.mcpHub?.deleteServer(message.serverName)
  1022. this.outputChannel.appendLine(`Successfully deleted MCP server: ${message.serverName}`)
  1023. } catch (error) {
  1024. const errorMessage = error instanceof Error ? error.message : String(error)
  1025. this.outputChannel.appendLine(`Failed to delete MCP server: ${errorMessage}`)
  1026. // Error messages are already handled by McpHub.deleteServer
  1027. }
  1028. break
  1029. }
  1030. case "restartMcpServer": {
  1031. try {
  1032. await this.mcpHub?.restartConnection(message.text!)
  1033. } catch (error) {
  1034. this.outputChannel.appendLine(
  1035. `Failed to retry connection for ${message.text}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1036. )
  1037. }
  1038. break
  1039. }
  1040. case "toggleToolAlwaysAllow": {
  1041. try {
  1042. await this.mcpHub?.toggleToolAlwaysAllow(
  1043. message.serverName!,
  1044. message.toolName!,
  1045. message.alwaysAllow!,
  1046. )
  1047. } catch (error) {
  1048. this.outputChannel.appendLine(
  1049. `Failed to toggle auto-approve for tool ${message.toolName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1050. )
  1051. }
  1052. break
  1053. }
  1054. case "toggleMcpServer": {
  1055. try {
  1056. await this.mcpHub?.toggleServerDisabled(message.serverName!, message.disabled!)
  1057. } catch (error) {
  1058. this.outputChannel.appendLine(
  1059. `Failed to toggle MCP server ${message.serverName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1060. )
  1061. }
  1062. break
  1063. }
  1064. case "mcpEnabled":
  1065. const mcpEnabled = message.bool ?? true
  1066. await this.updateGlobalState("mcpEnabled", mcpEnabled)
  1067. await this.postStateToWebview()
  1068. break
  1069. case "enableMcpServerCreation":
  1070. await this.updateGlobalState("enableMcpServerCreation", message.bool ?? true)
  1071. await this.postStateToWebview()
  1072. break
  1073. case "playSound":
  1074. if (message.audioType) {
  1075. const soundPath = path.join(this.context.extensionPath, "audio", `${message.audioType}.wav`)
  1076. playSound(soundPath)
  1077. }
  1078. break
  1079. case "soundEnabled":
  1080. const soundEnabled = message.bool ?? true
  1081. await this.updateGlobalState("soundEnabled", soundEnabled)
  1082. setSoundEnabled(soundEnabled) // Add this line to update the sound utility
  1083. await this.postStateToWebview()
  1084. break
  1085. case "soundVolume":
  1086. const soundVolume = message.value ?? 0.5
  1087. await this.updateGlobalState("soundVolume", soundVolume)
  1088. setSoundVolume(soundVolume)
  1089. await this.postStateToWebview()
  1090. break
  1091. case "diffEnabled":
  1092. const diffEnabled = message.bool ?? true
  1093. await this.updateGlobalState("diffEnabled", diffEnabled)
  1094. await this.postStateToWebview()
  1095. break
  1096. case "enableCheckpoints":
  1097. const enableCheckpoints = message.bool ?? true
  1098. await this.updateGlobalState("enableCheckpoints", enableCheckpoints)
  1099. await this.postStateToWebview()
  1100. break
  1101. case "checkpointStorage":
  1102. console.log(`[ClineProvider] checkpointStorage: ${message.text}`)
  1103. const checkpointStorage = message.text ?? "task"
  1104. await this.updateGlobalState("checkpointStorage", checkpointStorage)
  1105. await this.postStateToWebview()
  1106. break
  1107. case "browserViewportSize":
  1108. const browserViewportSize = message.text ?? "900x600"
  1109. await this.updateGlobalState("browserViewportSize", browserViewportSize)
  1110. await this.postStateToWebview()
  1111. break
  1112. case "remoteBrowserHost":
  1113. await this.updateGlobalState("remoteBrowserHost", message.text)
  1114. await this.postStateToWebview()
  1115. break
  1116. case "remoteBrowserEnabled":
  1117. // Store the preference in global state
  1118. // remoteBrowserEnabled now means "enable remote browser connection"
  1119. await this.updateGlobalState("remoteBrowserEnabled", message.bool ?? false)
  1120. // If disabling remote browser connection, clear the remoteBrowserHost
  1121. if (!message.bool) {
  1122. await this.updateGlobalState("remoteBrowserHost", undefined)
  1123. }
  1124. await this.postStateToWebview()
  1125. break
  1126. case "testBrowserConnection":
  1127. try {
  1128. const browserSession = new BrowserSession(this.context)
  1129. // If no text is provided, try auto-discovery
  1130. if (!message.text) {
  1131. try {
  1132. const discoveredHost = await discoverChromeInstances()
  1133. if (discoveredHost) {
  1134. // Test the connection to the discovered host
  1135. const result = await browserSession.testConnection(discoveredHost)
  1136. // Send the result back to the webview
  1137. await this.postMessageToWebview({
  1138. type: "browserConnectionResult",
  1139. success: result.success,
  1140. text: `Auto-discovered and tested connection to Chrome at ${discoveredHost}: ${result.message}`,
  1141. values: { endpoint: result.endpoint },
  1142. })
  1143. } else {
  1144. await this.postMessageToWebview({
  1145. type: "browserConnectionResult",
  1146. success: false,
  1147. text: "No Chrome instances found on the network. Make sure Chrome is running with remote debugging enabled (--remote-debugging-port=9222).",
  1148. })
  1149. }
  1150. } catch (error) {
  1151. await this.postMessageToWebview({
  1152. type: "browserConnectionResult",
  1153. success: false,
  1154. text: `Error during auto-discovery: ${error instanceof Error ? error.message : String(error)}`,
  1155. })
  1156. }
  1157. } else {
  1158. // Test the provided URL
  1159. const result = await browserSession.testConnection(message.text)
  1160. // Send the result back to the webview
  1161. await this.postMessageToWebview({
  1162. type: "browserConnectionResult",
  1163. success: result.success,
  1164. text: result.message,
  1165. values: { endpoint: result.endpoint },
  1166. })
  1167. }
  1168. } catch (error) {
  1169. await this.postMessageToWebview({
  1170. type: "browserConnectionResult",
  1171. success: false,
  1172. text: `Error testing connection: ${error instanceof Error ? error.message : String(error)}`,
  1173. })
  1174. }
  1175. break
  1176. case "discoverBrowser":
  1177. try {
  1178. const discoveredHost = await discoverChromeInstances()
  1179. if (discoveredHost) {
  1180. // Don't update the remoteBrowserHost state when auto-discovering
  1181. // This way we don't override the user's preference
  1182. // Test the connection to get the endpoint
  1183. const browserSession = new BrowserSession(this.context)
  1184. const result = await browserSession.testConnection(discoveredHost)
  1185. // Send the result back to the webview
  1186. await this.postMessageToWebview({
  1187. type: "browserConnectionResult",
  1188. success: true,
  1189. text: `Successfully discovered and connected to Chrome at ${discoveredHost}`,
  1190. values: { endpoint: result.endpoint },
  1191. })
  1192. } else {
  1193. await this.postMessageToWebview({
  1194. type: "browserConnectionResult",
  1195. success: false,
  1196. text: "No Chrome instances found on the network. Make sure Chrome is running with remote debugging enabled (--remote-debugging-port=9222).",
  1197. })
  1198. }
  1199. } catch (error) {
  1200. await this.postMessageToWebview({
  1201. type: "browserConnectionResult",
  1202. success: false,
  1203. text: `Error discovering browser: ${error instanceof Error ? error.message : String(error)}`,
  1204. })
  1205. }
  1206. break
  1207. case "fuzzyMatchThreshold":
  1208. await this.updateGlobalState("fuzzyMatchThreshold", message.value)
  1209. await this.postStateToWebview()
  1210. break
  1211. case "alwaysApproveResubmit":
  1212. await this.updateGlobalState("alwaysApproveResubmit", message.bool ?? false)
  1213. await this.postStateToWebview()
  1214. break
  1215. case "requestDelaySeconds":
  1216. await this.updateGlobalState("requestDelaySeconds", message.value ?? 5)
  1217. await this.postStateToWebview()
  1218. break
  1219. case "rateLimitSeconds":
  1220. await this.updateGlobalState("rateLimitSeconds", message.value ?? 0)
  1221. await this.postStateToWebview()
  1222. break
  1223. case "writeDelayMs":
  1224. await this.updateGlobalState("writeDelayMs", message.value)
  1225. await this.postStateToWebview()
  1226. break
  1227. case "terminalOutputLineLimit":
  1228. await this.updateGlobalState("terminalOutputLineLimit", message.value)
  1229. await this.postStateToWebview()
  1230. break
  1231. case "mode":
  1232. await this.handleModeSwitch(message.text as Mode)
  1233. break
  1234. case "updateSupportPrompt":
  1235. try {
  1236. if (Object.keys(message?.values ?? {}).length === 0) {
  1237. return
  1238. }
  1239. const existingPrompts = (await this.getGlobalState("customSupportPrompts")) || {}
  1240. const updatedPrompts = {
  1241. ...existingPrompts,
  1242. ...message.values,
  1243. }
  1244. await this.updateGlobalState("customSupportPrompts", updatedPrompts)
  1245. await this.postStateToWebview()
  1246. } catch (error) {
  1247. this.outputChannel.appendLine(
  1248. `Error update support prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1249. )
  1250. vscode.window.showErrorMessage("Failed to update support prompt")
  1251. }
  1252. break
  1253. case "resetSupportPrompt":
  1254. try {
  1255. if (!message?.text) {
  1256. return
  1257. }
  1258. const existingPrompts = ((await this.getGlobalState("customSupportPrompts")) ||
  1259. {}) as Record<string, any>
  1260. const updatedPrompts = {
  1261. ...existingPrompts,
  1262. }
  1263. updatedPrompts[message.text] = undefined
  1264. await this.updateGlobalState("customSupportPrompts", updatedPrompts)
  1265. await this.postStateToWebview()
  1266. } catch (error) {
  1267. this.outputChannel.appendLine(
  1268. `Error reset support prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1269. )
  1270. vscode.window.showErrorMessage("Failed to reset support prompt")
  1271. }
  1272. break
  1273. case "updatePrompt":
  1274. if (message.promptMode && message.customPrompt !== undefined) {
  1275. const existingPrompts = (await this.getGlobalState("customModePrompts")) || {}
  1276. const updatedPrompts = {
  1277. ...existingPrompts,
  1278. [message.promptMode]: message.customPrompt,
  1279. }
  1280. await this.updateGlobalState("customModePrompts", updatedPrompts)
  1281. // Get current state and explicitly include customModePrompts
  1282. const currentState = await this.getState()
  1283. const stateWithPrompts = {
  1284. ...currentState,
  1285. customModePrompts: updatedPrompts,
  1286. }
  1287. // Post state with prompts
  1288. this.view?.webview.postMessage({
  1289. type: "state",
  1290. state: stateWithPrompts,
  1291. })
  1292. }
  1293. break
  1294. case "deleteMessage": {
  1295. const answer = await vscode.window.showInformationMessage(
  1296. "What would you like to delete?",
  1297. { modal: true },
  1298. "Just this message",
  1299. "This and all subsequent messages",
  1300. )
  1301. if (
  1302. (answer === "Just this message" || answer === "This and all subsequent messages") &&
  1303. this.getCurrentCline() &&
  1304. typeof message.value === "number" &&
  1305. message.value
  1306. ) {
  1307. const timeCutoff = message.value - 1000 // 1 second buffer before the message to delete
  1308. const messageIndex = this.getCurrentCline()!.clineMessages.findIndex(
  1309. (msg) => msg.ts && msg.ts >= timeCutoff,
  1310. )
  1311. const apiConversationHistoryIndex =
  1312. this.getCurrentCline()?.apiConversationHistory.findIndex(
  1313. (msg) => msg.ts && msg.ts >= timeCutoff,
  1314. )
  1315. if (messageIndex !== -1) {
  1316. const { historyItem } = await this.getTaskWithId(this.getCurrentCline()!.taskId)
  1317. if (answer === "Just this message") {
  1318. // Find the next user message first
  1319. const nextUserMessage = this.getCurrentCline()!
  1320. .clineMessages.slice(messageIndex + 1)
  1321. .find((msg) => msg.type === "say" && msg.say === "user_feedback")
  1322. // Handle UI messages
  1323. if (nextUserMessage) {
  1324. // Find absolute index of next user message
  1325. const nextUserMessageIndex = this.getCurrentCline()!.clineMessages.findIndex(
  1326. (msg) => msg === nextUserMessage,
  1327. )
  1328. // Keep messages before current message and after next user message
  1329. await this.getCurrentCline()!.overwriteClineMessages([
  1330. ...this.getCurrentCline()!.clineMessages.slice(0, messageIndex),
  1331. ...this.getCurrentCline()!.clineMessages.slice(nextUserMessageIndex),
  1332. ])
  1333. } else {
  1334. // If no next user message, keep only messages before current message
  1335. await this.getCurrentCline()!.overwriteClineMessages(
  1336. this.getCurrentCline()!.clineMessages.slice(0, messageIndex),
  1337. )
  1338. }
  1339. // Handle API messages
  1340. if (apiConversationHistoryIndex !== -1) {
  1341. if (nextUserMessage && nextUserMessage.ts) {
  1342. // Keep messages before current API message and after next user message
  1343. await this.getCurrentCline()!.overwriteApiConversationHistory([
  1344. ...this.getCurrentCline()!.apiConversationHistory.slice(
  1345. 0,
  1346. apiConversationHistoryIndex,
  1347. ),
  1348. ...this.getCurrentCline()!.apiConversationHistory.filter(
  1349. (msg) => msg.ts && msg.ts >= nextUserMessage.ts,
  1350. ),
  1351. ])
  1352. } else {
  1353. // If no next user message, keep only messages before current API message
  1354. await this.getCurrentCline()!.overwriteApiConversationHistory(
  1355. this.getCurrentCline()!.apiConversationHistory.slice(
  1356. 0,
  1357. apiConversationHistoryIndex,
  1358. ),
  1359. )
  1360. }
  1361. }
  1362. } else if (answer === "This and all subsequent messages") {
  1363. // Delete this message and all that follow
  1364. await this.getCurrentCline()!.overwriteClineMessages(
  1365. this.getCurrentCline()!.clineMessages.slice(0, messageIndex),
  1366. )
  1367. if (apiConversationHistoryIndex !== -1) {
  1368. await this.getCurrentCline()!.overwriteApiConversationHistory(
  1369. this.getCurrentCline()!.apiConversationHistory.slice(
  1370. 0,
  1371. apiConversationHistoryIndex,
  1372. ),
  1373. )
  1374. }
  1375. }
  1376. await this.initClineWithHistoryItem(historyItem)
  1377. }
  1378. }
  1379. break
  1380. }
  1381. case "screenshotQuality":
  1382. await this.updateGlobalState("screenshotQuality", message.value)
  1383. await this.postStateToWebview()
  1384. break
  1385. case "maxOpenTabsContext":
  1386. const tabCount = Math.min(Math.max(0, message.value ?? 20), 500)
  1387. await this.updateGlobalState("maxOpenTabsContext", tabCount)
  1388. await this.postStateToWebview()
  1389. break
  1390. case "maxWorkspaceFiles":
  1391. const fileCount = Math.min(Math.max(0, message.value ?? 200), 500)
  1392. await this.updateGlobalState("maxWorkspaceFiles", fileCount)
  1393. await this.postStateToWebview()
  1394. break
  1395. case "browserToolEnabled":
  1396. await this.updateGlobalState("browserToolEnabled", message.bool ?? true)
  1397. await this.postStateToWebview()
  1398. break
  1399. case "language":
  1400. await this.updateGlobalState("language", message.text)
  1401. await this.postStateToWebview()
  1402. break
  1403. case "showRooIgnoredFiles":
  1404. await this.updateGlobalState("showRooIgnoredFiles", message.bool ?? true)
  1405. await this.postStateToWebview()
  1406. break
  1407. case "enhancementApiConfigId":
  1408. await this.updateGlobalState("enhancementApiConfigId", message.text)
  1409. await this.postStateToWebview()
  1410. break
  1411. case "enableCustomModeCreation":
  1412. await this.updateGlobalState("enableCustomModeCreation", message.bool ?? true)
  1413. await this.postStateToWebview()
  1414. break
  1415. case "autoApprovalEnabled":
  1416. await this.updateGlobalState("autoApprovalEnabled", message.bool ?? false)
  1417. await this.postStateToWebview()
  1418. break
  1419. case "enhancePrompt":
  1420. if (message.text) {
  1421. try {
  1422. const {
  1423. apiConfiguration,
  1424. customSupportPrompts,
  1425. listApiConfigMeta,
  1426. enhancementApiConfigId,
  1427. } = await this.getState()
  1428. // Try to get enhancement config first, fall back to current config
  1429. let configToUse: ApiConfiguration = apiConfiguration
  1430. if (enhancementApiConfigId) {
  1431. const config = listApiConfigMeta?.find(
  1432. (c: ApiConfigMeta) => c.id === enhancementApiConfigId,
  1433. )
  1434. if (config?.name) {
  1435. const loadedConfig = await this.configManager.loadConfig(config.name)
  1436. if (loadedConfig.apiProvider) {
  1437. configToUse = loadedConfig
  1438. }
  1439. }
  1440. }
  1441. const enhancedPrompt = await singleCompletionHandler(
  1442. configToUse,
  1443. supportPrompt.create(
  1444. "ENHANCE",
  1445. {
  1446. userInput: message.text,
  1447. },
  1448. customSupportPrompts,
  1449. ),
  1450. )
  1451. await this.postMessageToWebview({
  1452. type: "enhancedPrompt",
  1453. text: enhancedPrompt,
  1454. })
  1455. } catch (error) {
  1456. this.outputChannel.appendLine(
  1457. `Error enhancing prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1458. )
  1459. vscode.window.showErrorMessage("Failed to enhance prompt")
  1460. await this.postMessageToWebview({
  1461. type: "enhancedPrompt",
  1462. })
  1463. }
  1464. }
  1465. break
  1466. case "getSystemPrompt":
  1467. try {
  1468. const systemPrompt = await generateSystemPrompt(message)
  1469. await this.postMessageToWebview({
  1470. type: "systemPrompt",
  1471. text: systemPrompt,
  1472. mode: message.mode,
  1473. })
  1474. } catch (error) {
  1475. this.outputChannel.appendLine(
  1476. `Error getting system prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1477. )
  1478. vscode.window.showErrorMessage("Failed to get system prompt")
  1479. }
  1480. break
  1481. case "copySystemPrompt":
  1482. try {
  1483. const systemPrompt = await generateSystemPrompt(message)
  1484. await vscode.env.clipboard.writeText(systemPrompt)
  1485. await vscode.window.showInformationMessage("System prompt successfully copied to clipboard")
  1486. } catch (error) {
  1487. this.outputChannel.appendLine(
  1488. `Error getting system prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1489. )
  1490. vscode.window.showErrorMessage("Failed to get system prompt")
  1491. }
  1492. break
  1493. case "searchCommits": {
  1494. const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
  1495. if (cwd) {
  1496. try {
  1497. const commits = await searchCommits(message.query || "", cwd)
  1498. await this.postMessageToWebview({
  1499. type: "commitSearchResults",
  1500. commits,
  1501. })
  1502. } catch (error) {
  1503. this.outputChannel.appendLine(
  1504. `Error searching commits: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1505. )
  1506. vscode.window.showErrorMessage("Failed to search commits")
  1507. }
  1508. }
  1509. break
  1510. }
  1511. case "saveApiConfiguration":
  1512. if (message.text && message.apiConfiguration) {
  1513. try {
  1514. await this.configManager.saveConfig(message.text, message.apiConfiguration)
  1515. const listApiConfig = await this.configManager.listConfig()
  1516. await this.updateGlobalState("listApiConfigMeta", listApiConfig)
  1517. } catch (error) {
  1518. this.outputChannel.appendLine(
  1519. `Error save api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1520. )
  1521. vscode.window.showErrorMessage("Failed to save api configuration")
  1522. }
  1523. }
  1524. break
  1525. case "upsertApiConfiguration":
  1526. if (message.text && message.apiConfiguration) {
  1527. try {
  1528. await this.configManager.saveConfig(message.text, message.apiConfiguration)
  1529. const listApiConfig = await this.configManager.listConfig()
  1530. await Promise.all([
  1531. this.updateGlobalState("listApiConfigMeta", listApiConfig),
  1532. this.updateApiConfiguration(message.apiConfiguration),
  1533. this.updateGlobalState("currentApiConfigName", message.text),
  1534. ])
  1535. await this.postStateToWebview()
  1536. } catch (error) {
  1537. this.outputChannel.appendLine(
  1538. `Error create new api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1539. )
  1540. vscode.window.showErrorMessage("Failed to create api configuration")
  1541. }
  1542. }
  1543. break
  1544. case "renameApiConfiguration":
  1545. if (message.values && message.apiConfiguration) {
  1546. try {
  1547. const { oldName, newName } = message.values
  1548. if (oldName === newName) {
  1549. break
  1550. }
  1551. await this.configManager.saveConfig(newName, message.apiConfiguration)
  1552. await this.configManager.deleteConfig(oldName)
  1553. const listApiConfig = await this.configManager.listConfig()
  1554. const config = listApiConfig?.find((c) => c.name === newName)
  1555. // Update listApiConfigMeta first to ensure UI has latest data
  1556. await this.updateGlobalState("listApiConfigMeta", listApiConfig)
  1557. await Promise.all([this.updateGlobalState("currentApiConfigName", newName)])
  1558. await this.postStateToWebview()
  1559. } catch (error) {
  1560. this.outputChannel.appendLine(
  1561. `Error rename api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1562. )
  1563. vscode.window.showErrorMessage("Failed to rename api configuration")
  1564. }
  1565. }
  1566. break
  1567. case "loadApiConfiguration":
  1568. if (message.text) {
  1569. try {
  1570. const apiConfig = await this.configManager.loadConfig(message.text)
  1571. const listApiConfig = await this.configManager.listConfig()
  1572. await Promise.all([
  1573. this.updateGlobalState("listApiConfigMeta", listApiConfig),
  1574. this.updateGlobalState("currentApiConfigName", message.text),
  1575. this.updateApiConfiguration(apiConfig),
  1576. ])
  1577. await this.postStateToWebview()
  1578. } catch (error) {
  1579. this.outputChannel.appendLine(
  1580. `Error load api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1581. )
  1582. vscode.window.showErrorMessage("Failed to load api configuration")
  1583. }
  1584. }
  1585. break
  1586. case "deleteApiConfiguration":
  1587. if (message.text) {
  1588. const answer = await vscode.window.showInformationMessage(
  1589. "Are you sure you want to delete this configuration profile?",
  1590. { modal: true },
  1591. "Yes",
  1592. )
  1593. if (answer !== "Yes") {
  1594. break
  1595. }
  1596. try {
  1597. await this.configManager.deleteConfig(message.text)
  1598. const listApiConfig = await this.configManager.listConfig()
  1599. // Update listApiConfigMeta first to ensure UI has latest data
  1600. await this.updateGlobalState("listApiConfigMeta", listApiConfig)
  1601. // If this was the current config, switch to first available
  1602. const currentApiConfigName = await this.getGlobalState("currentApiConfigName")
  1603. if (message.text === currentApiConfigName && listApiConfig?.[0]?.name) {
  1604. const apiConfig = await this.configManager.loadConfig(listApiConfig[0].name)
  1605. await Promise.all([
  1606. this.updateGlobalState("currentApiConfigName", listApiConfig[0].name),
  1607. this.updateApiConfiguration(apiConfig),
  1608. ])
  1609. }
  1610. await this.postStateToWebview()
  1611. } catch (error) {
  1612. this.outputChannel.appendLine(
  1613. `Error delete api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1614. )
  1615. vscode.window.showErrorMessage("Failed to delete api configuration")
  1616. }
  1617. }
  1618. break
  1619. case "getListApiConfiguration":
  1620. try {
  1621. const listApiConfig = await this.configManager.listConfig()
  1622. await this.updateGlobalState("listApiConfigMeta", listApiConfig)
  1623. this.postMessageToWebview({ type: "listApiConfig", listApiConfig })
  1624. } catch (error) {
  1625. this.outputChannel.appendLine(
  1626. `Error get list api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1627. )
  1628. vscode.window.showErrorMessage("Failed to get list api configuration")
  1629. }
  1630. break
  1631. case "updateExperimental": {
  1632. if (!message.values) {
  1633. break
  1634. }
  1635. const updatedExperiments = {
  1636. ...((await this.getGlobalState("experiments")) ?? experimentDefault),
  1637. ...message.values,
  1638. } as Record<ExperimentId, boolean>
  1639. await this.updateGlobalState("experiments", updatedExperiments)
  1640. // Update diffStrategy in current Cline instance if it exists
  1641. if (message.values[EXPERIMENT_IDS.DIFF_STRATEGY] !== undefined && this.getCurrentCline()) {
  1642. await this.getCurrentCline()!.updateDiffStrategy(
  1643. Experiments.isEnabled(updatedExperiments, EXPERIMENT_IDS.DIFF_STRATEGY),
  1644. Experiments.isEnabled(updatedExperiments, EXPERIMENT_IDS.MULTI_SEARCH_AND_REPLACE),
  1645. )
  1646. }
  1647. await this.postStateToWebview()
  1648. break
  1649. }
  1650. case "updateMcpTimeout":
  1651. if (message.serverName && typeof message.timeout === "number") {
  1652. try {
  1653. await this.mcpHub?.updateServerTimeout(message.serverName, message.timeout)
  1654. } catch (error) {
  1655. this.outputChannel.appendLine(
  1656. `Failed to update timeout for ${message.serverName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1657. )
  1658. vscode.window.showErrorMessage("Failed to update server timeout")
  1659. }
  1660. }
  1661. break
  1662. case "updateCustomMode":
  1663. if (message.modeConfig) {
  1664. await this.customModesManager.updateCustomMode(message.modeConfig.slug, message.modeConfig)
  1665. // Update state after saving the mode
  1666. const customModes = await this.customModesManager.getCustomModes()
  1667. await this.updateGlobalState("customModes", customModes)
  1668. await this.updateGlobalState("mode", message.modeConfig.slug)
  1669. await this.postStateToWebview()
  1670. }
  1671. break
  1672. case "deleteCustomMode":
  1673. if (message.slug) {
  1674. const answer = await vscode.window.showInformationMessage(
  1675. "Are you sure you want to delete this custom mode?",
  1676. { modal: true },
  1677. "Yes",
  1678. )
  1679. if (answer !== "Yes") {
  1680. break
  1681. }
  1682. await this.customModesManager.deleteCustomMode(message.slug)
  1683. // Switch back to default mode after deletion
  1684. await this.updateGlobalState("mode", defaultModeSlug)
  1685. await this.postStateToWebview()
  1686. }
  1687. break
  1688. case "humanRelayResponse":
  1689. if (message.requestId && message.text) {
  1690. vscode.commands.executeCommand("roo-cline.handleHumanRelayResponse", {
  1691. requestId: message.requestId,
  1692. text: message.text,
  1693. cancelled: false,
  1694. })
  1695. }
  1696. break
  1697. case "humanRelayCancel":
  1698. if (message.requestId) {
  1699. vscode.commands.executeCommand("roo-cline.handleHumanRelayResponse", {
  1700. requestId: message.requestId,
  1701. cancelled: true,
  1702. })
  1703. }
  1704. break
  1705. case "telemetrySetting": {
  1706. const telemetrySetting = message.text as TelemetrySetting
  1707. await this.updateGlobalState("telemetrySetting", telemetrySetting)
  1708. const isOptedIn = telemetrySetting === "enabled"
  1709. telemetryService.updateTelemetryState(isOptedIn)
  1710. await this.postStateToWebview()
  1711. break
  1712. }
  1713. }
  1714. },
  1715. null,
  1716. this.disposables,
  1717. )
  1718. const generateSystemPrompt = async (message: WebviewMessage) => {
  1719. const {
  1720. apiConfiguration,
  1721. customModePrompts,
  1722. customInstructions,
  1723. browserViewportSize,
  1724. diffEnabled,
  1725. mcpEnabled,
  1726. fuzzyMatchThreshold,
  1727. experiments,
  1728. enableMcpServerCreation,
  1729. browserToolEnabled,
  1730. } = await this.getState()
  1731. // Create diffStrategy based on current model and settings
  1732. const diffStrategy = getDiffStrategy(
  1733. apiConfiguration.apiModelId || apiConfiguration.openRouterModelId || "",
  1734. fuzzyMatchThreshold,
  1735. Experiments.isEnabled(experiments, EXPERIMENT_IDS.DIFF_STRATEGY),
  1736. )
  1737. const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) || ""
  1738. const mode = message.mode ?? defaultModeSlug
  1739. const customModes = await this.customModesManager.getCustomModes()
  1740. const rooIgnoreInstructions = this.getCurrentCline()?.rooIgnoreController?.getInstructions()
  1741. // Determine if browser tools can be used based on model support and user settings
  1742. const modelSupportsComputerUse = this.getCurrentCline()?.api.getModel().info.supportsComputerUse ?? false
  1743. const canUseBrowserTool = modelSupportsComputerUse && (browserToolEnabled ?? true)
  1744. const systemPrompt = await SYSTEM_PROMPT(
  1745. this.context,
  1746. cwd,
  1747. canUseBrowserTool,
  1748. mcpEnabled ? this.mcpHub : undefined,
  1749. diffStrategy,
  1750. browserViewportSize ?? "900x600",
  1751. mode,
  1752. customModePrompts,
  1753. customModes,
  1754. customInstructions,
  1755. diffEnabled,
  1756. experiments,
  1757. enableMcpServerCreation,
  1758. rooIgnoreInstructions,
  1759. )
  1760. return systemPrompt
  1761. }
  1762. }
  1763. /**
  1764. * Handle switching to a new mode, including updating the associated API configuration
  1765. * @param newMode The mode to switch to
  1766. */
  1767. public async handleModeSwitch(newMode: Mode) {
  1768. // Capture mode switch telemetry event
  1769. const currentTaskId = this.getCurrentCline()?.taskId
  1770. if (currentTaskId) {
  1771. telemetryService.captureModeSwitch(currentTaskId, newMode)
  1772. }
  1773. await this.updateGlobalState("mode", newMode)
  1774. // Load the saved API config for the new mode if it exists
  1775. const savedConfigId = await this.configManager.getModeConfigId(newMode)
  1776. const listApiConfig = await this.configManager.listConfig()
  1777. // Update listApiConfigMeta first to ensure UI has latest data
  1778. await this.updateGlobalState("listApiConfigMeta", listApiConfig)
  1779. // If this mode has a saved config, use it
  1780. if (savedConfigId) {
  1781. const config = listApiConfig?.find((c) => c.id === savedConfigId)
  1782. if (config?.name) {
  1783. const apiConfig = await this.configManager.loadConfig(config.name)
  1784. await Promise.all([
  1785. this.updateGlobalState("currentApiConfigName", config.name),
  1786. this.updateApiConfiguration(apiConfig),
  1787. ])
  1788. }
  1789. } else {
  1790. // If no saved config for this mode, save current config as default
  1791. const currentApiConfigName = await this.getGlobalState("currentApiConfigName")
  1792. if (currentApiConfigName) {
  1793. const config = listApiConfig?.find((c) => c.name === currentApiConfigName)
  1794. if (config?.id) {
  1795. await this.configManager.setModeConfig(newMode, config.id)
  1796. }
  1797. }
  1798. }
  1799. await this.postStateToWebview()
  1800. }
  1801. private async updateApiConfiguration(apiConfiguration: ApiConfiguration) {
  1802. // Update mode's default config.
  1803. const { mode } = await this.getState()
  1804. if (mode) {
  1805. const currentApiConfigName = await this.getGlobalState("currentApiConfigName")
  1806. const listApiConfig = await this.configManager.listConfig()
  1807. const config = listApiConfig?.find((c) => c.name === currentApiConfigName)
  1808. if (config?.id) {
  1809. await this.configManager.setModeConfig(mode, config.id)
  1810. }
  1811. }
  1812. await this.contextProxy.setApiConfiguration(apiConfiguration)
  1813. if (this.getCurrentCline()) {
  1814. this.getCurrentCline()!.api = buildApiHandler(apiConfiguration)
  1815. }
  1816. }
  1817. async cancelTask() {
  1818. const cline = this.getCurrentCline()
  1819. if (!cline) {
  1820. return
  1821. }
  1822. console.log(`[subtasks] cancelling task ${cline.taskId}.${cline.instanceId}`)
  1823. const { historyItem } = await this.getTaskWithId(cline.taskId)
  1824. // Preserve parent and root task information for history item.
  1825. const rootTask = cline.rootTask
  1826. const parentTask = cline.parentTask
  1827. cline.abortTask()
  1828. await pWaitFor(
  1829. () =>
  1830. this.getCurrentCline()! === undefined ||
  1831. this.getCurrentCline()!.isStreaming === false ||
  1832. this.getCurrentCline()!.didFinishAbortingStream ||
  1833. // If only the first chunk is processed, then there's no
  1834. // need to wait for graceful abort (closes edits, browser,
  1835. // etc).
  1836. this.getCurrentCline()!.isWaitingForFirstChunk,
  1837. {
  1838. timeout: 3_000,
  1839. },
  1840. ).catch(() => {
  1841. console.error("Failed to abort task")
  1842. })
  1843. if (this.getCurrentCline()) {
  1844. // 'abandoned' will prevent this Cline instance from affecting
  1845. // future Cline instances. This may happen if its hanging on a
  1846. // streaming request.
  1847. this.getCurrentCline()!.abandoned = true
  1848. }
  1849. // Clears task again, so we need to abortTask manually above.
  1850. await this.initClineWithHistoryItem({ ...historyItem, rootTask, parentTask })
  1851. }
  1852. async updateCustomInstructions(instructions?: string) {
  1853. // User may be clearing the field.
  1854. await this.updateGlobalState("customInstructions", instructions || undefined)
  1855. if (this.getCurrentCline()) {
  1856. this.getCurrentCline()!.customInstructions = instructions || undefined
  1857. }
  1858. await this.postStateToWebview()
  1859. }
  1860. // MCP
  1861. async ensureMcpServersDirectoryExists(): Promise<string> {
  1862. // Get platform-specific application data directory
  1863. let mcpServersDir: string
  1864. if (process.platform === "win32") {
  1865. // Windows: %APPDATA%\Roo-Code\MCP
  1866. mcpServersDir = path.join(os.homedir(), "AppData", "Roaming", "Roo-Code", "MCP")
  1867. } else if (process.platform === "darwin") {
  1868. // macOS: ~/Documents/Cline/MCP
  1869. mcpServersDir = path.join(os.homedir(), "Documents", "Cline", "MCP")
  1870. } else {
  1871. // Linux: ~/.local/share/Cline/MCP
  1872. mcpServersDir = path.join(os.homedir(), ".local", "share", "Roo-Code", "MCP")
  1873. }
  1874. try {
  1875. await fs.mkdir(mcpServersDir, { recursive: true })
  1876. } catch (error) {
  1877. // Fallback to a relative path if directory creation fails
  1878. return path.join(os.homedir(), ".roo-code", "mcp")
  1879. }
  1880. return mcpServersDir
  1881. }
  1882. async ensureSettingsDirectoryExists(): Promise<string> {
  1883. const settingsDir = path.join(this.contextProxy.globalStorageUri.fsPath, "settings")
  1884. await fs.mkdir(settingsDir, { recursive: true })
  1885. return settingsDir
  1886. }
  1887. private async ensureCacheDirectoryExists() {
  1888. const cacheDir = path.join(this.contextProxy.globalStorageUri.fsPath, "cache")
  1889. await fs.mkdir(cacheDir, { recursive: true })
  1890. return cacheDir
  1891. }
  1892. private async readModelsFromCache(filename: string): Promise<Record<string, ModelInfo> | undefined> {
  1893. const filePath = path.join(await this.ensureCacheDirectoryExists(), filename)
  1894. const fileExists = await fileExistsAtPath(filePath)
  1895. if (fileExists) {
  1896. const fileContents = await fs.readFile(filePath, "utf8")
  1897. return JSON.parse(fileContents)
  1898. }
  1899. return undefined
  1900. }
  1901. // OpenRouter
  1902. async handleOpenRouterCallback(code: string) {
  1903. let apiKey: string
  1904. try {
  1905. const { apiConfiguration } = await this.getState()
  1906. const baseUrl = apiConfiguration.openRouterBaseUrl || "https://openrouter.ai/api/v1"
  1907. // Extract the base domain for the auth endpoint
  1908. const baseUrlDomain = baseUrl.match(/^(https?:\/\/[^\/]+)/)?.[1] || "https://openrouter.ai"
  1909. const response = await axios.post(`${baseUrlDomain}/api/v1/auth/keys`, { code })
  1910. if (response.data && response.data.key) {
  1911. apiKey = response.data.key
  1912. } else {
  1913. throw new Error("Invalid response from OpenRouter API")
  1914. }
  1915. } catch (error) {
  1916. this.outputChannel.appendLine(
  1917. `Error exchanging code for API key: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1918. )
  1919. throw error
  1920. }
  1921. const openrouter: ApiProvider = "openrouter"
  1922. await this.contextProxy.setValues({
  1923. apiProvider: openrouter,
  1924. openRouterApiKey: apiKey,
  1925. })
  1926. await this.postStateToWebview()
  1927. if (this.getCurrentCline()) {
  1928. this.getCurrentCline()!.api = buildApiHandler({ apiProvider: openrouter, openRouterApiKey: apiKey })
  1929. }
  1930. // await this.postMessageToWebview({ type: "action", action: "settingsButtonClicked" }) // bad ux if user is on welcome
  1931. }
  1932. // Glama
  1933. async handleGlamaCallback(code: string) {
  1934. let apiKey: string
  1935. try {
  1936. const response = await axios.post("https://glama.ai/api/gateway/v1/auth/exchange-code", { code })
  1937. if (response.data && response.data.apiKey) {
  1938. apiKey = response.data.apiKey
  1939. } else {
  1940. throw new Error("Invalid response from Glama API")
  1941. }
  1942. } catch (error) {
  1943. this.outputChannel.appendLine(
  1944. `Error exchanging code for API key: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1945. )
  1946. throw error
  1947. }
  1948. const glama: ApiProvider = "glama"
  1949. await this.contextProxy.setValues({
  1950. apiProvider: glama,
  1951. glamaApiKey: apiKey,
  1952. })
  1953. await this.postStateToWebview()
  1954. if (this.getCurrentCline()) {
  1955. this.getCurrentCline()!.api = buildApiHandler({
  1956. apiProvider: glama,
  1957. glamaApiKey: apiKey,
  1958. })
  1959. }
  1960. // await this.postMessageToWebview({ type: "action", action: "settingsButtonClicked" }) // bad ux if user is on welcome
  1961. }
  1962. // Task history
  1963. async getTaskWithId(id: string): Promise<{
  1964. historyItem: HistoryItem
  1965. taskDirPath: string
  1966. apiConversationHistoryFilePath: string
  1967. uiMessagesFilePath: string
  1968. apiConversationHistory: Anthropic.MessageParam[]
  1969. }> {
  1970. const history = ((await this.getGlobalState("taskHistory")) as HistoryItem[] | undefined) || []
  1971. const historyItem = history.find((item) => item.id === id)
  1972. if (!historyItem) {
  1973. throw new Error("Task not found in history")
  1974. }
  1975. const taskDirPath = path.join(this.contextProxy.globalStorageUri.fsPath, "tasks", id)
  1976. const apiConversationHistoryFilePath = path.join(taskDirPath, GlobalFileNames.apiConversationHistory)
  1977. const uiMessagesFilePath = path.join(taskDirPath, GlobalFileNames.uiMessages)
  1978. const fileExists = await fileExistsAtPath(apiConversationHistoryFilePath)
  1979. if (!fileExists) {
  1980. // Instead of silently deleting, throw a specific error
  1981. throw new Error("TASK_FILES_MISSING")
  1982. }
  1983. const apiConversationHistory = JSON.parse(await fs.readFile(apiConversationHistoryFilePath, "utf8"))
  1984. return {
  1985. historyItem,
  1986. taskDirPath,
  1987. apiConversationHistoryFilePath,
  1988. uiMessagesFilePath,
  1989. apiConversationHistory,
  1990. }
  1991. }
  1992. async showTaskWithId(id: string) {
  1993. if (id !== this.getCurrentCline()?.taskId) {
  1994. try {
  1995. const { historyItem } = await this.getTaskWithId(id)
  1996. await this.initClineWithHistoryItem(historyItem)
  1997. } catch (error) {
  1998. if (error.message === "TASK_FILES_MISSING") {
  1999. const response = await vscode.window.showWarningMessage(
  2000. "This task's files are missing. Would you like to remove it from the task list?",
  2001. "Remove",
  2002. "Keep",
  2003. )
  2004. if (response === "Remove") {
  2005. await this.deleteTaskFromState(id)
  2006. await this.postStateToWebview()
  2007. }
  2008. return
  2009. }
  2010. throw error
  2011. }
  2012. }
  2013. await this.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
  2014. }
  2015. async exportTaskWithId(id: string) {
  2016. const { historyItem, apiConversationHistory } = await this.getTaskWithId(id)
  2017. await downloadTask(historyItem.ts, apiConversationHistory)
  2018. }
  2019. // this function deletes a task from task hidtory, and deletes it's checkpoints and delete the task folder
  2020. async deleteTaskWithId(id: string) {
  2021. // get the task directory full path
  2022. const { taskDirPath } = await this.getTaskWithId(id)
  2023. // remove task from stack if it's the current task
  2024. if (id === this.getCurrentCline()?.taskId) {
  2025. // if we found the taskid to delete - call finish to abort this task and allow a new task to be started,
  2026. // if we are deleting a subtask and parent task is still waiting for subtask to finish - it allows the parent to resume (this case should neve exist)
  2027. await this.finishSubTask(`Task failure: It was stopped and deleted by the user.`)
  2028. }
  2029. // delete task from the task history state
  2030. await this.deleteTaskFromState(id)
  2031. // get the base directory of the project
  2032. const baseDir = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
  2033. // Delete associated shadow repository or branch.
  2034. // TODO: Store `workspaceDir` in the `HistoryItem` object.
  2035. const globalStorageDir = this.contextProxy.globalStorageUri.fsPath
  2036. const workspaceDir = baseDir ?? ""
  2037. try {
  2038. await ShadowCheckpointService.deleteTask({ taskId: id, globalStorageDir, workspaceDir })
  2039. } catch (error) {
  2040. console.error(
  2041. `[deleteTaskWithId${id}] failed to delete associated shadow repository or branch: ${error instanceof Error ? error.message : String(error)}`,
  2042. )
  2043. }
  2044. // delete the entire task directory including checkpoints and all content
  2045. try {
  2046. await fs.rm(taskDirPath, { recursive: true, force: true })
  2047. console.log(`[deleteTaskWithId${id}] removed task directory`)
  2048. } catch (error) {
  2049. console.error(
  2050. `[deleteTaskWithId${id}] failed to remove task directory: ${error instanceof Error ? error.message : String(error)}`,
  2051. )
  2052. }
  2053. }
  2054. async deleteTaskFromState(id: string) {
  2055. // Remove the task from history
  2056. const taskHistory = ((await this.getGlobalState("taskHistory")) as HistoryItem[]) || []
  2057. const updatedTaskHistory = taskHistory.filter((task) => task.id !== id)
  2058. await this.updateGlobalState("taskHistory", updatedTaskHistory)
  2059. // Notify the webview that the task has been deleted
  2060. await this.postStateToWebview()
  2061. }
  2062. async postStateToWebview() {
  2063. const state = await this.getStateToPostToWebview()
  2064. this.postMessageToWebview({ type: "state", state })
  2065. }
  2066. async getStateToPostToWebview() {
  2067. const {
  2068. apiConfiguration,
  2069. lastShownAnnouncementId,
  2070. customInstructions,
  2071. alwaysAllowReadOnly,
  2072. alwaysAllowWrite,
  2073. alwaysAllowExecute,
  2074. alwaysAllowBrowser,
  2075. alwaysAllowMcp,
  2076. alwaysAllowModeSwitch,
  2077. alwaysAllowSubtasks,
  2078. soundEnabled,
  2079. diffEnabled,
  2080. enableCheckpoints,
  2081. checkpointStorage,
  2082. taskHistory,
  2083. soundVolume,
  2084. browserViewportSize,
  2085. screenshotQuality,
  2086. remoteBrowserHost,
  2087. remoteBrowserEnabled,
  2088. writeDelayMs,
  2089. terminalOutputLineLimit,
  2090. fuzzyMatchThreshold,
  2091. mcpEnabled,
  2092. enableMcpServerCreation,
  2093. alwaysApproveResubmit,
  2094. requestDelaySeconds,
  2095. rateLimitSeconds,
  2096. currentApiConfigName,
  2097. listApiConfigMeta,
  2098. mode,
  2099. customModePrompts,
  2100. customSupportPrompts,
  2101. enhancementApiConfigId,
  2102. autoApprovalEnabled,
  2103. experiments,
  2104. maxOpenTabsContext,
  2105. maxWorkspaceFiles,
  2106. browserToolEnabled,
  2107. telemetrySetting,
  2108. showRooIgnoredFiles,
  2109. language,
  2110. } = await this.getState()
  2111. const telemetryKey = process.env.POSTHOG_API_KEY
  2112. const machineId = vscode.env.machineId
  2113. const allowedCommands = vscode.workspace.getConfiguration("roo-cline").get<string[]>("allowedCommands") || []
  2114. const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) || ""
  2115. return {
  2116. version: this.context.extension?.packageJSON?.version ?? "",
  2117. apiConfiguration,
  2118. customInstructions,
  2119. alwaysAllowReadOnly: alwaysAllowReadOnly ?? false,
  2120. alwaysAllowWrite: alwaysAllowWrite ?? false,
  2121. alwaysAllowExecute: alwaysAllowExecute ?? false,
  2122. alwaysAllowBrowser: alwaysAllowBrowser ?? false,
  2123. alwaysAllowMcp: alwaysAllowMcp ?? false,
  2124. alwaysAllowModeSwitch: alwaysAllowModeSwitch ?? false,
  2125. alwaysAllowSubtasks: alwaysAllowSubtasks ?? false,
  2126. uriScheme: vscode.env.uriScheme,
  2127. currentTaskItem: this.getCurrentCline()?.taskId
  2128. ? (taskHistory || []).find((item: HistoryItem) => item.id === this.getCurrentCline()?.taskId)
  2129. : undefined,
  2130. clineMessages: this.getCurrentCline()?.clineMessages || [],
  2131. taskHistory: (taskHistory || [])
  2132. .filter((item: HistoryItem) => item.ts && item.task)
  2133. .sort((a: HistoryItem, b: HistoryItem) => b.ts - a.ts),
  2134. soundEnabled: soundEnabled ?? false,
  2135. diffEnabled: diffEnabled ?? true,
  2136. enableCheckpoints: enableCheckpoints ?? true,
  2137. checkpointStorage: checkpointStorage ?? "task",
  2138. shouldShowAnnouncement:
  2139. telemetrySetting !== "unset" && lastShownAnnouncementId !== this.latestAnnouncementId,
  2140. allowedCommands,
  2141. soundVolume: soundVolume ?? 0.5,
  2142. browserViewportSize: browserViewportSize ?? "900x600",
  2143. screenshotQuality: screenshotQuality ?? 75,
  2144. remoteBrowserHost,
  2145. remoteBrowserEnabled: remoteBrowserEnabled ?? false,
  2146. writeDelayMs: writeDelayMs ?? 1000,
  2147. terminalOutputLineLimit: terminalOutputLineLimit ?? 500,
  2148. fuzzyMatchThreshold: fuzzyMatchThreshold ?? 1.0,
  2149. mcpEnabled: mcpEnabled ?? true,
  2150. enableMcpServerCreation: enableMcpServerCreation ?? true,
  2151. alwaysApproveResubmit: alwaysApproveResubmit ?? false,
  2152. requestDelaySeconds: requestDelaySeconds ?? 10,
  2153. rateLimitSeconds: rateLimitSeconds ?? 0,
  2154. currentApiConfigName: currentApiConfigName ?? "default",
  2155. listApiConfigMeta: listApiConfigMeta ?? [],
  2156. mode: mode ?? defaultModeSlug,
  2157. customModePrompts: customModePrompts ?? {},
  2158. customSupportPrompts: customSupportPrompts ?? {},
  2159. enhancementApiConfigId,
  2160. autoApprovalEnabled: autoApprovalEnabled ?? false,
  2161. customModes: await this.customModesManager.getCustomModes(),
  2162. experiments: experiments ?? experimentDefault,
  2163. mcpServers: this.mcpHub?.getAllServers() ?? [],
  2164. maxOpenTabsContext: maxOpenTabsContext ?? 20,
  2165. maxWorkspaceFiles: maxWorkspaceFiles ?? 200,
  2166. cwd,
  2167. browserToolEnabled: browserToolEnabled ?? true,
  2168. telemetrySetting,
  2169. telemetryKey,
  2170. machineId,
  2171. showRooIgnoredFiles: showRooIgnoredFiles ?? true,
  2172. language,
  2173. }
  2174. }
  2175. // Caching mechanism to keep track of webview messages + API conversation history per provider instance
  2176. /*
  2177. Now that we use retainContextWhenHidden, we don't have to store a cache of cline messages in the user's state, but we could to reduce memory footprint in long conversations.
  2178. - We have to be careful of what state is shared between ClineProvider instances since there could be multiple instances of the extension running at once. For example when we cached cline messages using the same key, two instances of the extension could end up using the same key and overwriting each other's messages.
  2179. - Some state does need to be shared between the instances, i.e. the API key--however there doesn't seem to be a good way to notfy the other instances that the API key has changed.
  2180. We need to use a unique identifier for each ClineProvider instance's message cache since we could be running several instances of the extension outside of just the sidebar i.e. in editor panels.
  2181. // conversation history to send in API requests
  2182. /*
  2183. It seems that some API messages do not comply with vscode state requirements. Either the Anthropic library is manipulating these values somehow in the backend in a way thats creating cyclic references, or the API returns a function or a Symbol as part of the message content.
  2184. VSCode docs about state: "The value must be JSON-stringifyable ... value — A value. MUST not contain cyclic references."
  2185. For now we'll store the conversation history in memory, and if we need to store in state directly we'd need to do a manual conversion to ensure proper json stringification.
  2186. */
  2187. // getApiConversationHistory(): Anthropic.MessageParam[] {
  2188. // // const history = (await this.getGlobalState(
  2189. // // this.getApiConversationHistoryStateKey()
  2190. // // )) as Anthropic.MessageParam[]
  2191. // // return history || []
  2192. // return this.apiConversationHistory
  2193. // }
  2194. // setApiConversationHistory(history: Anthropic.MessageParam[] | undefined) {
  2195. // // await this.updateGlobalState(this.getApiConversationHistoryStateKey(), history)
  2196. // this.apiConversationHistory = history || []
  2197. // }
  2198. // addMessageToApiConversationHistory(message: Anthropic.MessageParam): Anthropic.MessageParam[] {
  2199. // // const history = await this.getApiConversationHistory()
  2200. // // history.push(message)
  2201. // // await this.setApiConversationHistory(history)
  2202. // // return history
  2203. // this.apiConversationHistory.push(message)
  2204. // return this.apiConversationHistory
  2205. // }
  2206. /*
  2207. Storage
  2208. https://dev.to/kompotkot/how-to-use-secretstorage-in-your-vscode-extensions-2hco
  2209. https://www.eliostruyf.com/devhack-code-extension-storage-options/
  2210. */
  2211. async getState() {
  2212. // Create an object to store all fetched values
  2213. const stateValues: Record<GlobalStateKey | SecretKey, any> = {} as Record<GlobalStateKey | SecretKey, any>
  2214. const secretValues: Record<SecretKey, any> = {} as Record<SecretKey, any>
  2215. // Create promise arrays for global state and secrets
  2216. const statePromises = GLOBAL_STATE_KEYS.map((key) => this.getGlobalState(key))
  2217. const secretPromises = SECRET_KEYS.map((key) => this.getSecret(key))
  2218. // Add promise for custom modes which is handled separately
  2219. const customModesPromise = this.customModesManager.getCustomModes()
  2220. let idx = 0
  2221. const valuePromises = await Promise.all([...statePromises, ...secretPromises, customModesPromise])
  2222. // Populate stateValues and secretValues
  2223. GLOBAL_STATE_KEYS.forEach((key, _) => {
  2224. stateValues[key] = valuePromises[idx]
  2225. idx = idx + 1
  2226. })
  2227. SECRET_KEYS.forEach((key, index) => {
  2228. secretValues[key] = valuePromises[idx]
  2229. idx = idx + 1
  2230. })
  2231. let customModes = valuePromises[idx] as ModeConfig[] | undefined
  2232. // Determine apiProvider with the same logic as before
  2233. let apiProvider: ApiProvider
  2234. if (stateValues.apiProvider) {
  2235. apiProvider = stateValues.apiProvider
  2236. } else {
  2237. // Either new user or legacy user that doesn't have the apiProvider stored in state
  2238. // (If they're using OpenRouter or Bedrock, then apiProvider state will exist)
  2239. if (secretValues.apiKey) {
  2240. apiProvider = "anthropic"
  2241. } else {
  2242. // New users should default to openrouter
  2243. apiProvider = "openrouter"
  2244. }
  2245. }
  2246. // Build the apiConfiguration object combining state values and secrets
  2247. // Using the dynamic approach with API_CONFIG_KEYS
  2248. const apiConfiguration: ApiConfiguration = {
  2249. // Dynamically add all API-related keys from stateValues
  2250. ...Object.fromEntries(API_CONFIG_KEYS.map((key) => [key, stateValues[key]])),
  2251. // Add all secrets
  2252. ...secretValues,
  2253. }
  2254. // Ensure apiProvider is set properly if not already in state
  2255. if (!apiConfiguration.apiProvider) {
  2256. apiConfiguration.apiProvider = apiProvider
  2257. }
  2258. // Return the same structure as before
  2259. return {
  2260. apiConfiguration,
  2261. lastShownAnnouncementId: stateValues.lastShownAnnouncementId,
  2262. customInstructions: stateValues.customInstructions,
  2263. alwaysAllowReadOnly: stateValues.alwaysAllowReadOnly ?? false,
  2264. alwaysAllowWrite: stateValues.alwaysAllowWrite ?? false,
  2265. alwaysAllowExecute: stateValues.alwaysAllowExecute ?? false,
  2266. alwaysAllowBrowser: stateValues.alwaysAllowBrowser ?? false,
  2267. alwaysAllowMcp: stateValues.alwaysAllowMcp ?? false,
  2268. alwaysAllowModeSwitch: stateValues.alwaysAllowModeSwitch ?? false,
  2269. alwaysAllowSubtasks: stateValues.alwaysAllowSubtasks ?? false,
  2270. taskHistory: stateValues.taskHistory,
  2271. allowedCommands: stateValues.allowedCommands,
  2272. soundEnabled: stateValues.soundEnabled ?? false,
  2273. diffEnabled: stateValues.diffEnabled ?? true,
  2274. enableCheckpoints: stateValues.enableCheckpoints ?? true,
  2275. checkpointStorage: stateValues.checkpointStorage ?? "task",
  2276. soundVolume: stateValues.soundVolume,
  2277. browserViewportSize: stateValues.browserViewportSize ?? "900x600",
  2278. screenshotQuality: stateValues.screenshotQuality ?? 75,
  2279. remoteBrowserHost: stateValues.remoteBrowserHost,
  2280. remoteBrowserEnabled: stateValues.remoteBrowserEnabled ?? false,
  2281. fuzzyMatchThreshold: stateValues.fuzzyMatchThreshold ?? 1.0,
  2282. writeDelayMs: stateValues.writeDelayMs ?? 1000,
  2283. terminalOutputLineLimit: stateValues.terminalOutputLineLimit ?? 500,
  2284. mode: stateValues.mode ?? defaultModeSlug,
  2285. language: stateValues.language ?? formatLanguage(vscode.env.language),
  2286. mcpEnabled: stateValues.mcpEnabled ?? true,
  2287. enableMcpServerCreation: stateValues.enableMcpServerCreation ?? true,
  2288. alwaysApproveResubmit: stateValues.alwaysApproveResubmit ?? false,
  2289. requestDelaySeconds: Math.max(5, stateValues.requestDelaySeconds ?? 10),
  2290. rateLimitSeconds: stateValues.rateLimitSeconds ?? 0,
  2291. currentApiConfigName: stateValues.currentApiConfigName ?? "default",
  2292. listApiConfigMeta: stateValues.listApiConfigMeta ?? [],
  2293. modeApiConfigs: stateValues.modeApiConfigs ?? ({} as Record<Mode, string>),
  2294. customModePrompts: stateValues.customModePrompts ?? {},
  2295. customSupportPrompts: stateValues.customSupportPrompts ?? {},
  2296. enhancementApiConfigId: stateValues.enhancementApiConfigId,
  2297. experiments: stateValues.experiments ?? experimentDefault,
  2298. autoApprovalEnabled: stateValues.autoApprovalEnabled ?? false,
  2299. customModes,
  2300. maxOpenTabsContext: stateValues.maxOpenTabsContext ?? 20,
  2301. maxWorkspaceFiles: stateValues.maxWorkspaceFiles ?? 200,
  2302. openRouterUseMiddleOutTransform: stateValues.openRouterUseMiddleOutTransform ?? true,
  2303. browserToolEnabled: stateValues.browserToolEnabled ?? true,
  2304. telemetrySetting: stateValues.telemetrySetting || "unset",
  2305. showRooIgnoredFiles: stateValues.showRooIgnoredFiles ?? true,
  2306. }
  2307. }
  2308. async updateTaskHistory(item: HistoryItem): Promise<HistoryItem[]> {
  2309. const history = ((await this.getGlobalState("taskHistory")) as HistoryItem[] | undefined) || []
  2310. const existingItemIndex = history.findIndex((h) => h.id === item.id)
  2311. if (existingItemIndex !== -1) {
  2312. history[existingItemIndex] = item
  2313. } else {
  2314. history.push(item)
  2315. }
  2316. await this.updateGlobalState("taskHistory", history)
  2317. return history
  2318. }
  2319. // global
  2320. public async updateGlobalState(key: GlobalStateKey, value: any) {
  2321. await this.contextProxy.updateGlobalState(key, value)
  2322. }
  2323. public async getGlobalState(key: GlobalStateKey) {
  2324. return await this.contextProxy.getGlobalState(key)
  2325. }
  2326. // secrets
  2327. public async storeSecret(key: SecretKey, value?: string) {
  2328. await this.contextProxy.storeSecret(key, value)
  2329. }
  2330. private async getSecret(key: SecretKey) {
  2331. return await this.contextProxy.getSecret(key)
  2332. }
  2333. // global + secret
  2334. public async setValues(values: Partial<ConfigurationValues>) {
  2335. await this.contextProxy.setValues(values)
  2336. }
  2337. // dev
  2338. async resetState() {
  2339. const answer = await vscode.window.showInformationMessage(
  2340. "Are you sure you want to reset all state and secret storage in the extension? This cannot be undone.",
  2341. { modal: true },
  2342. "Yes",
  2343. )
  2344. if (answer !== "Yes") {
  2345. return
  2346. }
  2347. await this.contextProxy.resetAllState()
  2348. await this.configManager.resetAllConfigs()
  2349. await this.customModesManager.resetCustomModes()
  2350. await this.removeClineFromStack()
  2351. await this.postStateToWebview()
  2352. await this.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
  2353. }
  2354. // logging
  2355. public log(message: string) {
  2356. this.outputChannel.appendLine(message)
  2357. console.log(message)
  2358. }
  2359. // integration tests
  2360. get viewLaunched() {
  2361. return this.isViewLaunched
  2362. }
  2363. get messages() {
  2364. return this.getCurrentCline()?.clineMessages || []
  2365. }
  2366. // Add public getter
  2367. public getMcpHub(): McpHub | undefined {
  2368. return this.mcpHub
  2369. }
  2370. /**
  2371. * Returns properties to be included in every telemetry event
  2372. * This method is called by the telemetry service to get context information
  2373. * like the current mode, API provider, etc.
  2374. */
  2375. public async getTelemetryProperties(): Promise<Record<string, any>> {
  2376. const { mode, apiConfiguration, language } = await this.getState()
  2377. const appVersion = this.context.extension?.packageJSON?.version
  2378. const vscodeVersion = vscode.version
  2379. const platform = process.platform
  2380. const properties: Record<string, any> = {
  2381. vscodeVersion,
  2382. platform,
  2383. }
  2384. // Add extension version
  2385. if (appVersion) {
  2386. properties.appVersion = appVersion
  2387. }
  2388. // Add language
  2389. if (language) {
  2390. properties.language = language
  2391. }
  2392. // Add current mode
  2393. if (mode) {
  2394. properties.mode = mode
  2395. }
  2396. // Add API provider
  2397. if (apiConfiguration?.apiProvider) {
  2398. properties.apiProvider = apiConfiguration.apiProvider
  2399. }
  2400. // Add model ID if available
  2401. const currentCline = this.getCurrentCline()
  2402. if (currentCline?.api) {
  2403. const { id: modelId } = currentCline.api.getModel()
  2404. if (modelId) {
  2405. properties.modelId = modelId
  2406. }
  2407. }
  2408. if (currentCline?.diffStrategy) {
  2409. properties.diffStrategy = currentCline.diffStrategy.getName()
  2410. }
  2411. return properties
  2412. }
  2413. async validateTaskHistory() {
  2414. const history = ((await this.getGlobalState("taskHistory")) as HistoryItem[] | undefined) || []
  2415. const validTasks: HistoryItem[] = []
  2416. for (const item of history) {
  2417. const taskDirPath = path.join(this.contextProxy.globalStorageUri.fsPath, "tasks", item.id)
  2418. const apiConversationHistoryFilePath = path.join(taskDirPath, GlobalFileNames.apiConversationHistory)
  2419. if (await fileExistsAtPath(apiConversationHistoryFilePath)) {
  2420. validTasks.push(item)
  2421. }
  2422. }
  2423. if (validTasks.length !== history.length) {
  2424. await this.updateGlobalState("taskHistory", validTasks)
  2425. await this.postStateToWebview()
  2426. const removedCount = history.length - validTasks.length
  2427. if (removedCount > 0) {
  2428. await vscode.window.showInformationMessage(
  2429. `Cleaned up ${removedCount} task(s) with missing files from history.`,
  2430. )
  2431. }
  2432. }
  2433. }
  2434. }