ClineProvider.ts 89 KB

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