ClineProvider.ts 57 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635
  1. import os from "os"
  2. import * as path from "path"
  3. import fs from "fs/promises"
  4. import EventEmitter from "events"
  5. import { Anthropic } from "@anthropic-ai/sdk"
  6. import delay from "delay"
  7. import axios from "axios"
  8. import pWaitFor from "p-wait-for"
  9. import * as vscode from "vscode"
  10. import {
  11. type GlobalState,
  12. type ProviderName,
  13. type ProviderSettings,
  14. type RooCodeSettings,
  15. type ProviderSettingsEntry,
  16. type TelemetryProperties,
  17. type TelemetryPropertiesProvider,
  18. type CodeActionId,
  19. type CodeActionName,
  20. type TerminalActionId,
  21. type TerminalActionPromptType,
  22. type HistoryItem,
  23. ORGANIZATION_ALLOW_ALL,
  24. } from "@roo-code/types"
  25. import { TelemetryService } from "@roo-code/telemetry"
  26. import { CloudService } from "@roo-code/cloud"
  27. import { t } from "../../i18n"
  28. import { setPanel } from "../../activate/registerCommands"
  29. import { Package } from "../../shared/package"
  30. import { requestyDefaultModelId, openRouterDefaultModelId, glamaDefaultModelId } from "../../shared/api"
  31. import { findLast } from "../../shared/array"
  32. import { supportPrompt } from "../../shared/support-prompt"
  33. import { GlobalFileNames } from "../../shared/globalFileNames"
  34. import { ExtensionMessage } from "../../shared/ExtensionMessage"
  35. import { Mode, defaultModeSlug } from "../../shared/modes"
  36. import { experimentDefault } from "../../shared/experiments"
  37. import { formatLanguage } from "../../shared/language"
  38. import { Terminal } from "../../integrations/terminal/Terminal"
  39. import { downloadTask } from "../../integrations/misc/export-markdown"
  40. import { getTheme } from "../../integrations/theme/getTheme"
  41. import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
  42. import { McpHub } from "../../services/mcp/McpHub"
  43. import { McpServerManager } from "../../services/mcp/McpServerManager"
  44. import { ShadowCheckpointService } from "../../services/checkpoints/ShadowCheckpointService"
  45. import { CodeIndexManager } from "../../services/code-index/manager"
  46. import type { IndexProgressUpdate } from "../../services/code-index/interfaces/manager"
  47. import { fileExistsAtPath } from "../../utils/fs"
  48. import { setTtsEnabled, setTtsSpeed } from "../../utils/tts"
  49. import { ContextProxy } from "../config/ContextProxy"
  50. import { ProviderSettingsManager } from "../config/ProviderSettingsManager"
  51. import { CustomModesManager } from "../config/CustomModesManager"
  52. import { buildApiHandler } from "../../api"
  53. import { Task, TaskOptions } from "../task/Task"
  54. import { getNonce } from "./getNonce"
  55. import { getUri } from "./getUri"
  56. import { getSystemPromptFilePath } from "../prompts/sections/custom-system-prompt"
  57. import { getWorkspacePath } from "../../utils/path"
  58. import { webviewMessageHandler } from "./webviewMessageHandler"
  59. import { WebviewMessage } from "../../shared/WebviewMessage"
  60. import { EMBEDDING_MODEL_PROFILES } from "../../shared/embeddingModels"
  61. import { ProfileValidator } from "../../shared/ProfileValidator"
  62. /**
  63. * https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
  64. * https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/customSidebarViewProvider.ts
  65. */
  66. export type ClineProviderEvents = {
  67. clineCreated: [cline: Task]
  68. }
  69. class OrganizationAllowListViolationError extends Error {
  70. constructor(message: string) {
  71. super(message)
  72. }
  73. }
  74. export class ClineProvider
  75. extends EventEmitter<ClineProviderEvents>
  76. implements vscode.WebviewViewProvider, TelemetryPropertiesProvider
  77. {
  78. // Used in package.json as the view's id. This value cannot be changed due
  79. // to how VSCode caches views based on their id, and updating the id would
  80. // break existing instances of the extension.
  81. public static readonly sideBarId = `${Package.name}.SidebarProvider`
  82. public static readonly tabPanelId = `${Package.name}.TabPanelProvider`
  83. private static activeInstances: Set<ClineProvider> = new Set()
  84. private disposables: vscode.Disposable[] = []
  85. private view?: vscode.WebviewView | vscode.WebviewPanel
  86. private clineStack: Task[] = []
  87. private codeIndexStatusSubscription?: vscode.Disposable
  88. private _workspaceTracker?: WorkspaceTracker // workSpaceTracker read-only for access outside this class
  89. public get workspaceTracker(): WorkspaceTracker | undefined {
  90. return this._workspaceTracker
  91. }
  92. protected mcpHub?: McpHub // Change from private to protected
  93. public isViewLaunched = false
  94. public settingsImportedAt?: number
  95. public readonly latestAnnouncementId = "may-21-2025-3-18" // Update for v3.18.0 announcement
  96. public readonly providerSettingsManager: ProviderSettingsManager
  97. public readonly customModesManager: CustomModesManager
  98. constructor(
  99. readonly context: vscode.ExtensionContext,
  100. private readonly outputChannel: vscode.OutputChannel,
  101. private readonly renderContext: "sidebar" | "editor" = "sidebar",
  102. public readonly contextProxy: ContextProxy,
  103. public readonly codeIndexManager?: CodeIndexManager,
  104. ) {
  105. super()
  106. this.log("ClineProvider instantiated")
  107. ClineProvider.activeInstances.add(this)
  108. this.codeIndexManager = codeIndexManager
  109. this.updateGlobalState("codebaseIndexModels", EMBEDDING_MODEL_PROFILES)
  110. // Start configuration loading (which might trigger indexing) in the background.
  111. // Don't await, allowing activation to continue immediately.
  112. // Register this provider with the telemetry service to enable it to add
  113. // properties like mode and provider.
  114. TelemetryService.instance.setProvider(this)
  115. this._workspaceTracker = new WorkspaceTracker(this)
  116. this.providerSettingsManager = new ProviderSettingsManager(this.context)
  117. this.customModesManager = new CustomModesManager(this.context, async () => {
  118. await this.postStateToWebview()
  119. })
  120. // Initialize MCP Hub through the singleton manager
  121. McpServerManager.getInstance(this.context, this)
  122. .then((hub) => {
  123. this.mcpHub = hub
  124. this.mcpHub.registerClient()
  125. })
  126. .catch((error) => {
  127. this.log(`Failed to initialize MCP Hub: ${error}`)
  128. })
  129. }
  130. // Adds a new Cline instance to clineStack, marking the start of a new task.
  131. // The instance is pushed to the top of the stack (LIFO order).
  132. // When the task is completed, the top instance is removed, reactivating the previous task.
  133. async addClineToStack(cline: Task) {
  134. console.log(`[subtasks] adding task ${cline.taskId}.${cline.instanceId} to stack`)
  135. // Add this cline instance into the stack that represents the order of all the called tasks.
  136. this.clineStack.push(cline)
  137. // Ensure getState() resolves correctly.
  138. const state = await this.getState()
  139. if (!state || typeof state.mode !== "string") {
  140. throw new Error(t("common:errors.retrieve_current_mode"))
  141. }
  142. }
  143. // Removes and destroys the top Cline instance (the current finished task),
  144. // activating the previous one (resuming the parent task).
  145. async removeClineFromStack() {
  146. if (this.clineStack.length === 0) {
  147. return
  148. }
  149. // Pop the top Cline instance from the stack.
  150. let cline = this.clineStack.pop()
  151. if (cline) {
  152. console.log(`[subtasks] removing task ${cline.taskId}.${cline.instanceId} from stack`)
  153. try {
  154. // Abort the running task and set isAbandoned to true so
  155. // all running promises will exit as well.
  156. await cline.abortTask(true)
  157. } catch (e) {
  158. this.log(
  159. `[subtasks] encountered error while aborting task ${cline.taskId}.${cline.instanceId}: ${e.message}`,
  160. )
  161. }
  162. // Make sure no reference kept, once promises end it will be
  163. // garbage collected.
  164. cline = undefined
  165. }
  166. }
  167. // returns the current cline object in the stack (the top one)
  168. // if the stack is empty, returns undefined
  169. getCurrentCline(): Task | undefined {
  170. if (this.clineStack.length === 0) {
  171. return undefined
  172. }
  173. return this.clineStack[this.clineStack.length - 1]
  174. }
  175. // returns the current clineStack length (how many cline objects are in the stack)
  176. getClineStackSize(): number {
  177. return this.clineStack.length
  178. }
  179. public getCurrentTaskStack(): string[] {
  180. return this.clineStack.map((cline) => cline.taskId)
  181. }
  182. // remove the current task/cline instance (at the top of the stack), so this task is finished
  183. // and resume the previous task/cline instance (if it exists)
  184. // this is used when a sub task is finished and the parent task needs to be resumed
  185. async finishSubTask(lastMessage: string) {
  186. console.log(`[subtasks] finishing subtask ${lastMessage}`)
  187. // remove the last cline instance from the stack (this is the finished sub task)
  188. await this.removeClineFromStack()
  189. // resume the last cline instance in the stack (if it exists - this is the 'parent' calling task)
  190. await this.getCurrentCline()?.resumePausedTask(lastMessage)
  191. }
  192. /*
  193. 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.
  194. - https://vscode-docs.readthedocs.io/en/stable/extensions/patterns-and-principles/
  195. - https://github.com/microsoft/vscode-extension-samples/blob/main/webview-sample/src/extension.ts
  196. */
  197. async dispose() {
  198. this.log("Disposing ClineProvider...")
  199. await this.removeClineFromStack()
  200. this.log("Cleared task")
  201. if (this.view && "dispose" in this.view) {
  202. this.view.dispose()
  203. this.log("Disposed webview")
  204. }
  205. while (this.disposables.length) {
  206. const x = this.disposables.pop()
  207. if (x) {
  208. x.dispose()
  209. }
  210. }
  211. this._workspaceTracker?.dispose()
  212. this._workspaceTracker = undefined
  213. await this.mcpHub?.unregisterClient()
  214. this.mcpHub = undefined
  215. this.customModesManager?.dispose()
  216. this.log("Disposed all disposables")
  217. ClineProvider.activeInstances.delete(this)
  218. // Unregister from McpServerManager
  219. McpServerManager.unregisterProvider(this)
  220. }
  221. public static getVisibleInstance(): ClineProvider | undefined {
  222. return findLast(Array.from(this.activeInstances), (instance) => instance.view?.visible === true)
  223. }
  224. public static async getInstance(): Promise<ClineProvider | undefined> {
  225. let visibleProvider = ClineProvider.getVisibleInstance()
  226. // If no visible provider, try to show the sidebar view
  227. if (!visibleProvider) {
  228. await vscode.commands.executeCommand(`${Package.name}.SidebarProvider.focus`)
  229. // Wait briefly for the view to become visible
  230. await delay(100)
  231. visibleProvider = ClineProvider.getVisibleInstance()
  232. }
  233. // If still no visible provider, return
  234. if (!visibleProvider) {
  235. return
  236. }
  237. return visibleProvider
  238. }
  239. public static async isActiveTask(): Promise<boolean> {
  240. const visibleProvider = await ClineProvider.getInstance()
  241. if (!visibleProvider) {
  242. return false
  243. }
  244. // Check if there is a cline instance in the stack (if this provider has an active task)
  245. if (visibleProvider.getCurrentCline()) {
  246. return true
  247. }
  248. return false
  249. }
  250. public static async handleCodeAction(
  251. command: CodeActionId,
  252. promptType: CodeActionName,
  253. params: Record<string, string | any[]>,
  254. ): Promise<void> {
  255. // Capture telemetry for code action usage
  256. TelemetryService.instance.captureCodeActionUsed(promptType)
  257. const visibleProvider = await ClineProvider.getInstance()
  258. if (!visibleProvider) {
  259. return
  260. }
  261. const { customSupportPrompts } = await visibleProvider.getState()
  262. // TODO: Improve type safety for promptType.
  263. const prompt = supportPrompt.create(promptType, params, customSupportPrompts)
  264. if (command === "addToContext") {
  265. await visibleProvider.postMessageToWebview({ type: "invoke", invoke: "setChatBoxMessage", text: prompt })
  266. return
  267. }
  268. await visibleProvider.initClineWithTask(prompt)
  269. }
  270. public static async handleTerminalAction(
  271. command: TerminalActionId,
  272. promptType: TerminalActionPromptType,
  273. params: Record<string, string | any[]>,
  274. ): Promise<void> {
  275. TelemetryService.instance.captureCodeActionUsed(promptType)
  276. const visibleProvider = await ClineProvider.getInstance()
  277. if (!visibleProvider) {
  278. return
  279. }
  280. const { customSupportPrompts } = await visibleProvider.getState()
  281. const prompt = supportPrompt.create(promptType, params, customSupportPrompts)
  282. if (command === "terminalAddToContext") {
  283. await visibleProvider.postMessageToWebview({ type: "invoke", invoke: "setChatBoxMessage", text: prompt })
  284. return
  285. }
  286. try {
  287. await visibleProvider.initClineWithTask(prompt)
  288. } catch (error) {
  289. if (error instanceof OrganizationAllowListViolationError) {
  290. // Errors from terminal commands seem to get swallowed / ignored.
  291. vscode.window.showErrorMessage(error.message)
  292. }
  293. throw error
  294. }
  295. }
  296. async resolveWebviewView(webviewView: vscode.WebviewView | vscode.WebviewPanel) {
  297. this.log("Resolving webview view")
  298. this.view = webviewView
  299. // Set panel reference according to webview type
  300. const inTabMode = "onDidChangeViewState" in webviewView
  301. if (inTabMode) {
  302. // Tag page type
  303. setPanel(webviewView, "tab")
  304. } else if ("onDidChangeVisibility" in webviewView) {
  305. // Sidebar Type
  306. setPanel(webviewView, "sidebar")
  307. }
  308. // Initialize out-of-scope variables that need to recieve persistent global state values
  309. this.getState().then(
  310. ({
  311. terminalShellIntegrationTimeout = Terminal.defaultShellIntegrationTimeout,
  312. terminalShellIntegrationDisabled = false,
  313. terminalCommandDelay = 0,
  314. terminalZshClearEolMark = true,
  315. terminalZshOhMy = false,
  316. terminalZshP10k = false,
  317. terminalPowershellCounter = false,
  318. terminalZdotdir = false,
  319. }) => {
  320. Terminal.setShellIntegrationTimeout(terminalShellIntegrationTimeout)
  321. Terminal.setShellIntegrationDisabled(terminalShellIntegrationDisabled)
  322. Terminal.setCommandDelay(terminalCommandDelay)
  323. Terminal.setTerminalZshClearEolMark(terminalZshClearEolMark)
  324. Terminal.setTerminalZshOhMy(terminalZshOhMy)
  325. Terminal.setTerminalZshP10k(terminalZshP10k)
  326. Terminal.setPowershellCounter(terminalPowershellCounter)
  327. Terminal.setTerminalZdotdir(terminalZdotdir)
  328. },
  329. )
  330. // Initialize tts enabled state
  331. this.getState().then(({ ttsEnabled }) => {
  332. setTtsEnabled(ttsEnabled ?? false)
  333. })
  334. // Initialize tts speed state
  335. this.getState().then(({ ttsSpeed }) => {
  336. setTtsSpeed(ttsSpeed ?? 1)
  337. })
  338. webviewView.webview.options = {
  339. // Allow scripts in the webview
  340. enableScripts: true,
  341. localResourceRoots: [this.contextProxy.extensionUri],
  342. }
  343. webviewView.webview.html =
  344. this.contextProxy.extensionMode === vscode.ExtensionMode.Development
  345. ? await this.getHMRHtmlContent(webviewView.webview)
  346. : this.getHtmlContent(webviewView.webview)
  347. // Sets up an event listener to listen for messages passed from the webview view context
  348. // and executes code based on the message that is recieved
  349. this.setWebviewMessageListener(webviewView.webview)
  350. // Subscribe to code index status updates if the manager exists
  351. if (this.codeIndexManager) {
  352. this.codeIndexStatusSubscription = this.codeIndexManager.onProgressUpdate((update: IndexProgressUpdate) => {
  353. this.postMessageToWebview({
  354. type: "indexingStatusUpdate",
  355. values: update,
  356. })
  357. })
  358. // Add the subscription to the main disposables array
  359. this.disposables.push(this.codeIndexStatusSubscription)
  360. }
  361. // Logs show up in bottom panel > Debug Console
  362. //console.log("registering listener")
  363. // Listen for when the panel becomes visible
  364. // https://github.com/microsoft/vscode-discussions/discussions/840
  365. if ("onDidChangeViewState" in webviewView) {
  366. // WebviewView and WebviewPanel have all the same properties except for this visibility listener
  367. // panel
  368. webviewView.onDidChangeViewState(
  369. () => {
  370. if (this.view?.visible) {
  371. this.postMessageToWebview({ type: "action", action: "didBecomeVisible" })
  372. }
  373. },
  374. null,
  375. this.disposables,
  376. )
  377. } else if ("onDidChangeVisibility" in webviewView) {
  378. // sidebar
  379. webviewView.onDidChangeVisibility(
  380. () => {
  381. if (this.view?.visible) {
  382. this.postMessageToWebview({ type: "action", action: "didBecomeVisible" })
  383. }
  384. },
  385. null,
  386. this.disposables,
  387. )
  388. }
  389. // Listen for when the view is disposed
  390. // This happens when the user closes the view or when the view is closed programmatically
  391. webviewView.onDidDispose(
  392. async () => {
  393. if (inTabMode) {
  394. this.log("Disposing ClineProvider instance for tab view")
  395. await this.dispose()
  396. } else {
  397. this.log("Preserving ClineProvider instance for sidebar view reuse")
  398. }
  399. },
  400. null,
  401. this.disposables,
  402. )
  403. // Listen for when color changes
  404. vscode.workspace.onDidChangeConfiguration(
  405. async (e) => {
  406. if (e && e.affectsConfiguration("workbench.colorTheme")) {
  407. // Sends latest theme name to webview
  408. await this.postMessageToWebview({ type: "theme", text: JSON.stringify(await getTheme()) })
  409. }
  410. },
  411. null,
  412. this.disposables,
  413. )
  414. // If the extension is starting a new session, clear previous task state.
  415. await this.removeClineFromStack()
  416. this.log("Webview view resolved")
  417. }
  418. public async initClineWithSubTask(parent: Task, task?: string, images?: string[]) {
  419. return this.initClineWithTask(task, images, parent)
  420. }
  421. // When initializing a new task, (not from history but from a tool command
  422. // new_task) there is no need to remove the previouse task since the new
  423. // task is a subtask of the previous one, and when it finishes it is removed
  424. // from the stack and the caller is resumed in this way we can have a chain
  425. // of tasks, each one being a sub task of the previous one until the main
  426. // task is finished.
  427. public async initClineWithTask(
  428. task?: string,
  429. images?: string[],
  430. parentTask?: Task,
  431. options: Partial<
  432. Pick<
  433. TaskOptions,
  434. "enableDiff" | "enableCheckpoints" | "fuzzyMatchThreshold" | "consecutiveMistakeLimit" | "experiments"
  435. >
  436. > = {},
  437. ) {
  438. const {
  439. apiConfiguration,
  440. organizationAllowList,
  441. diffEnabled: enableDiff,
  442. enableCheckpoints,
  443. fuzzyMatchThreshold,
  444. experiments,
  445. } = await this.getState()
  446. if (!ProfileValidator.isProfileAllowed(apiConfiguration, organizationAllowList)) {
  447. throw new OrganizationAllowListViolationError(t("common:errors.violated_organization_allowlist"))
  448. }
  449. const cline = new Task({
  450. provider: this,
  451. apiConfiguration,
  452. enableDiff,
  453. enableCheckpoints,
  454. fuzzyMatchThreshold,
  455. task,
  456. images,
  457. experiments,
  458. rootTask: this.clineStack.length > 0 ? this.clineStack[0] : undefined,
  459. parentTask,
  460. taskNumber: this.clineStack.length + 1,
  461. onCreated: (cline) => this.emit("clineCreated", cline),
  462. ...options,
  463. })
  464. await this.addClineToStack(cline)
  465. this.log(
  466. `[subtasks] ${cline.parentTask ? "child" : "parent"} task ${cline.taskId}.${cline.instanceId} instantiated`,
  467. )
  468. return cline
  469. }
  470. public async initClineWithHistoryItem(historyItem: HistoryItem & { rootTask?: Task; parentTask?: Task }) {
  471. await this.removeClineFromStack()
  472. const {
  473. apiConfiguration,
  474. diffEnabled: enableDiff,
  475. enableCheckpoints,
  476. fuzzyMatchThreshold,
  477. experiments,
  478. } = await this.getState()
  479. const cline = new Task({
  480. provider: this,
  481. apiConfiguration,
  482. enableDiff,
  483. enableCheckpoints,
  484. fuzzyMatchThreshold,
  485. historyItem,
  486. experiments,
  487. rootTask: historyItem.rootTask,
  488. parentTask: historyItem.parentTask,
  489. taskNumber: historyItem.number,
  490. onCreated: (cline) => this.emit("clineCreated", cline),
  491. })
  492. await this.addClineToStack(cline)
  493. this.log(
  494. `[subtasks] ${cline.parentTask ? "child" : "parent"} task ${cline.taskId}.${cline.instanceId} instantiated`,
  495. )
  496. return cline
  497. }
  498. public async postMessageToWebview(message: ExtensionMessage) {
  499. await this.view?.webview.postMessage(message)
  500. }
  501. private async getHMRHtmlContent(webview: vscode.Webview): Promise<string> {
  502. // Try to read the port from the file
  503. let localPort = "5173" // Default fallback
  504. try {
  505. const fs = require("fs")
  506. const path = require("path")
  507. const portFilePath = path.resolve(__dirname, "../../.vite-port")
  508. if (fs.existsSync(portFilePath)) {
  509. localPort = fs.readFileSync(portFilePath, "utf8").trim()
  510. console.log(`[ClineProvider:Vite] Using Vite server port from ${portFilePath}: ${localPort}`)
  511. } else {
  512. console.log(
  513. `[ClineProvider:Vite] Port file not found at ${portFilePath}, using default port: ${localPort}`,
  514. )
  515. }
  516. } catch (err) {
  517. console.error("[ClineProvider:Vite] Failed to read Vite port file:", err)
  518. // Continue with default port if file reading fails
  519. }
  520. const localServerUrl = `localhost:${localPort}`
  521. // Check if local dev server is running.
  522. try {
  523. await axios.get(`http://${localServerUrl}`)
  524. } catch (error) {
  525. vscode.window.showErrorMessage(t("common:errors.hmr_not_running"))
  526. return this.getHtmlContent(webview)
  527. }
  528. const nonce = getNonce()
  529. const stylesUri = getUri(webview, this.contextProxy.extensionUri, [
  530. "webview-ui",
  531. "build",
  532. "assets",
  533. "index.css",
  534. ])
  535. const codiconsUri = getUri(webview, this.contextProxy.extensionUri, ["assets", "codicons", "codicon.css"])
  536. const materialIconsUri = getUri(webview, this.contextProxy.extensionUri, [
  537. "assets",
  538. "vscode-material-icons",
  539. "icons",
  540. ])
  541. const imagesUri = getUri(webview, this.contextProxy.extensionUri, ["assets", "images"])
  542. const audioUri = getUri(webview, this.contextProxy.extensionUri, ["webview-ui", "audio"])
  543. const file = "src/index.tsx"
  544. const scriptUri = `http://${localServerUrl}/${file}`
  545. const reactRefresh = /*html*/ `
  546. <script nonce="${nonce}" type="module">
  547. import RefreshRuntime from "http://localhost:${localPort}/@react-refresh"
  548. RefreshRuntime.injectIntoGlobalHook(window)
  549. window.$RefreshReg$ = () => {}
  550. window.$RefreshSig$ = () => (type) => type
  551. window.__vite_plugin_react_preamble_installed__ = true
  552. </script>
  553. `
  554. const csp = [
  555. "default-src 'none'",
  556. `font-src ${webview.cspSource}`,
  557. `style-src ${webview.cspSource} 'unsafe-inline' https://* http://${localServerUrl} http://0.0.0.0:${localPort}`,
  558. `img-src ${webview.cspSource} https://storage.googleapis.com https://img.clerk.com data:`,
  559. `media-src ${webview.cspSource}`,
  560. `script-src 'unsafe-eval' ${webview.cspSource} https://* https://*.posthog.com http://${localServerUrl} http://0.0.0.0:${localPort} 'nonce-${nonce}'`,
  561. `connect-src https://* https://*.posthog.com ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`,
  562. ]
  563. return /*html*/ `
  564. <!DOCTYPE html>
  565. <html lang="en">
  566. <head>
  567. <meta charset="utf-8">
  568. <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
  569. <meta http-equiv="Content-Security-Policy" content="${csp.join("; ")}">
  570. <link rel="stylesheet" type="text/css" href="${stylesUri}">
  571. <link href="${codiconsUri}" rel="stylesheet" />
  572. <script nonce="${nonce}">
  573. window.IMAGES_BASE_URI = "${imagesUri}"
  574. window.AUDIO_BASE_URI = "${audioUri}"
  575. window.MATERIAL_ICONS_BASE_URI = "${materialIconsUri}"
  576. </script>
  577. <title>Roo Code</title>
  578. </head>
  579. <body>
  580. <div id="root"></div>
  581. ${reactRefresh}
  582. <script type="module" src="${scriptUri}"></script>
  583. </body>
  584. </html>
  585. `
  586. }
  587. /**
  588. * Defines and returns the HTML that should be rendered within the webview panel.
  589. *
  590. * @remarks This is also the place where references to the React webview build files
  591. * are created and inserted into the webview HTML.
  592. *
  593. * @param webview A reference to the extension webview
  594. * @param extensionUri The URI of the directory containing the extension
  595. * @returns A template string literal containing the HTML that should be
  596. * rendered within the webview panel
  597. */
  598. private getHtmlContent(webview: vscode.Webview): string {
  599. // Get the local path to main script run in the webview,
  600. // then convert it to a uri we can use in the webview.
  601. // The CSS file from the React build output
  602. const stylesUri = getUri(webview, this.contextProxy.extensionUri, [
  603. "webview-ui",
  604. "build",
  605. "assets",
  606. "index.css",
  607. ])
  608. const scriptUri = getUri(webview, this.contextProxy.extensionUri, ["webview-ui", "build", "assets", "index.js"])
  609. const codiconsUri = getUri(webview, this.contextProxy.extensionUri, ["assets", "codicons", "codicon.css"])
  610. const materialIconsUri = getUri(webview, this.contextProxy.extensionUri, [
  611. "assets",
  612. "vscode-material-icons",
  613. "icons",
  614. ])
  615. const imagesUri = getUri(webview, this.contextProxy.extensionUri, ["assets", "images"])
  616. const audioUri = getUri(webview, this.contextProxy.extensionUri, ["webview-ui", "audio"])
  617. // Use a nonce to only allow a specific script to be run.
  618. /*
  619. content security policy of your webview to only allow scripts that have a specific nonce
  620. create a content security policy meta tag so that only loading scripts with a nonce is allowed
  621. 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.
  622. <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}';">
  623. - 'unsafe-inline' is required for styles due to vscode-webview-toolkit's dynamic style injection
  624. - since we pass base64 images to the webview, we need to specify img-src ${webview.cspSource} data:;
  625. 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.
  626. */
  627. const nonce = getNonce()
  628. // Tip: Install the es6-string-html VS Code extension to enable code highlighting below
  629. return /*html*/ `
  630. <!DOCTYPE html>
  631. <html lang="en">
  632. <head>
  633. <meta charset="utf-8">
  634. <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
  635. <meta name="theme-color" content="#000000">
  636. <meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src ${webview.cspSource}; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} https://storage.googleapis.com https://img.clerk.com data:; media-src ${webview.cspSource}; script-src ${webview.cspSource} 'wasm-unsafe-eval' 'nonce-${nonce}' https://us-assets.i.posthog.com 'strict-dynamic'; connect-src https://openrouter.ai https://api.requesty.ai https://us.i.posthog.com https://us-assets.i.posthog.com;">
  637. <link rel="stylesheet" type="text/css" href="${stylesUri}">
  638. <link href="${codiconsUri}" rel="stylesheet" />
  639. <script nonce="${nonce}">
  640. window.IMAGES_BASE_URI = "${imagesUri}"
  641. window.AUDIO_BASE_URI = "${audioUri}"
  642. window.MATERIAL_ICONS_BASE_URI = "${materialIconsUri}"
  643. </script>
  644. <title>Roo Code</title>
  645. </head>
  646. <body>
  647. <noscript>You need to enable JavaScript to run this app.</noscript>
  648. <div id="root"></div>
  649. <script nonce="${nonce}" type="module" src="${scriptUri}"></script>
  650. </body>
  651. </html>
  652. `
  653. }
  654. /**
  655. * Sets up an event listener to listen for messages passed from the webview context and
  656. * executes code based on the message that is recieved.
  657. *
  658. * @param webview A reference to the extension webview
  659. */
  660. private setWebviewMessageListener(webview: vscode.Webview) {
  661. const onReceiveMessage = async (message: WebviewMessage) => webviewMessageHandler(this, message)
  662. webview.onDidReceiveMessage(onReceiveMessage, null, this.disposables)
  663. }
  664. /**
  665. * Handle switching to a new mode, including updating the associated API configuration
  666. * @param newMode The mode to switch to
  667. */
  668. public async handleModeSwitch(newMode: Mode) {
  669. const cline = this.getCurrentCline()
  670. if (cline) {
  671. TelemetryService.instance.captureModeSwitch(cline.taskId, newMode)
  672. cline.emit("taskModeSwitched", cline.taskId, newMode)
  673. }
  674. await this.updateGlobalState("mode", newMode)
  675. // Load the saved API config for the new mode if it exists
  676. const savedConfigId = await this.providerSettingsManager.getModeConfigId(newMode)
  677. const listApiConfig = await this.providerSettingsManager.listConfig()
  678. // Update listApiConfigMeta first to ensure UI has latest data
  679. await this.updateGlobalState("listApiConfigMeta", listApiConfig)
  680. // If this mode has a saved config, use it.
  681. if (savedConfigId) {
  682. const profile = listApiConfig.find(({ id }) => id === savedConfigId)
  683. if (profile?.name) {
  684. await this.activateProviderProfile({ name: profile.name })
  685. }
  686. } else {
  687. // If no saved config for this mode, save current config as default.
  688. const currentApiConfigName = this.getGlobalState("currentApiConfigName")
  689. if (currentApiConfigName) {
  690. const config = listApiConfig.find((c) => c.name === currentApiConfigName)
  691. if (config?.id) {
  692. await this.providerSettingsManager.setModeConfig(newMode, config.id)
  693. }
  694. }
  695. }
  696. await this.postStateToWebview()
  697. }
  698. // Provider Profile Management
  699. getProviderProfileEntries(): ProviderSettingsEntry[] {
  700. return this.contextProxy.getValues().listApiConfigMeta || []
  701. }
  702. getProviderProfileEntry(name: string): ProviderSettingsEntry | undefined {
  703. return this.getProviderProfileEntries().find((profile) => profile.name === name)
  704. }
  705. public hasProviderProfileEntry(name: string): boolean {
  706. return !!this.getProviderProfileEntry(name)
  707. }
  708. async upsertProviderProfile(
  709. name: string,
  710. providerSettings: ProviderSettings,
  711. activate: boolean = true,
  712. ): Promise<string | undefined> {
  713. try {
  714. // TODO: Do we need to be calling `activateProfile`? It's not
  715. // clear to me what the source of truth should be; in some cases
  716. // we rely on the `ContextProxy`'s data store and in other cases
  717. // we rely on the `ProviderSettingsManager`'s data store. It might
  718. // be simpler to unify these two.
  719. const id = await this.providerSettingsManager.saveConfig(name, providerSettings)
  720. if (activate) {
  721. const { mode } = await this.getState()
  722. // These promises do the following:
  723. // 1. Adds or updates the list of provider profiles.
  724. // 2. Sets the current provider profile.
  725. // 3. Sets the current mode's provider profile.
  726. // 4. Copies the provider settings to the context.
  727. //
  728. // Note: 1, 2, and 4 can be done in one `ContextProxy` call:
  729. // this.contextProxy.setValues({ ...providerSettings, listApiConfigMeta: ..., currentApiConfigName: ... })
  730. // We should probably switch to that and verify that it works.
  731. // I left the original implementation in just to be safe.
  732. await Promise.all([
  733. this.updateGlobalState("listApiConfigMeta", await this.providerSettingsManager.listConfig()),
  734. this.updateGlobalState("currentApiConfigName", name),
  735. this.providerSettingsManager.setModeConfig(mode, id),
  736. this.contextProxy.setProviderSettings(providerSettings),
  737. ])
  738. // Notify CodeIndexManager about the settings change
  739. if (this.codeIndexManager) {
  740. await this.codeIndexManager.handleExternalSettingsChange()
  741. }
  742. // Change the provider for the current task.
  743. // TODO: We should rename `buildApiHandler` for clarity (e.g. `getProviderClient`).
  744. const task = this.getCurrentCline()
  745. if (task) {
  746. task.api = buildApiHandler(providerSettings)
  747. }
  748. } else {
  749. await this.updateGlobalState("listApiConfigMeta", await this.providerSettingsManager.listConfig())
  750. }
  751. await this.postStateToWebview()
  752. return id
  753. } catch (error) {
  754. this.log(
  755. `Error create new api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  756. )
  757. vscode.window.showErrorMessage(t("common:errors.create_api_config"))
  758. return undefined
  759. }
  760. }
  761. async deleteProviderProfile(profileToDelete: ProviderSettingsEntry) {
  762. const globalSettings = this.contextProxy.getValues()
  763. let profileToActivate: string | undefined = globalSettings.currentApiConfigName
  764. if (profileToDelete.name === profileToActivate) {
  765. profileToActivate = this.getProviderProfileEntries().find(({ name }) => name !== profileToDelete.name)?.name
  766. }
  767. if (!profileToActivate) {
  768. throw new Error("You cannot delete the last profile")
  769. }
  770. const entries = this.getProviderProfileEntries().filter(({ name }) => name !== profileToDelete.name)
  771. await this.contextProxy.setValues({
  772. ...globalSettings,
  773. currentApiConfigName: profileToActivate,
  774. listApiConfigMeta: entries,
  775. })
  776. await this.postStateToWebview()
  777. }
  778. async activateProviderProfile(args: { name: string } | { id: string }) {
  779. const { name, id, ...providerSettings } = await this.providerSettingsManager.activateProfile(args)
  780. // See `upsertProviderProfile` for a description of what this is doing.
  781. await Promise.all([
  782. this.contextProxy.setValue("listApiConfigMeta", await this.providerSettingsManager.listConfig()),
  783. this.contextProxy.setValue("currentApiConfigName", name),
  784. this.contextProxy.setProviderSettings(providerSettings),
  785. ])
  786. const { mode } = await this.getState()
  787. if (id) {
  788. await this.providerSettingsManager.setModeConfig(mode, id)
  789. }
  790. // Change the provider for the current task.
  791. const task = this.getCurrentCline()
  792. if (task) {
  793. task.api = buildApiHandler(providerSettings)
  794. }
  795. await this.postStateToWebview()
  796. }
  797. // Task Management
  798. async cancelTask() {
  799. const cline = this.getCurrentCline()
  800. if (!cline) {
  801. return
  802. }
  803. console.log(`[subtasks] cancelling task ${cline.taskId}.${cline.instanceId}`)
  804. const { historyItem } = await this.getTaskWithId(cline.taskId)
  805. // Preserve parent and root task information for history item.
  806. const rootTask = cline.rootTask
  807. const parentTask = cline.parentTask
  808. cline.abortTask()
  809. await pWaitFor(
  810. () =>
  811. this.getCurrentCline()! === undefined ||
  812. this.getCurrentCline()!.isStreaming === false ||
  813. this.getCurrentCline()!.didFinishAbortingStream ||
  814. // If only the first chunk is processed, then there's no
  815. // need to wait for graceful abort (closes edits, browser,
  816. // etc).
  817. this.getCurrentCline()!.isWaitingForFirstChunk,
  818. {
  819. timeout: 3_000,
  820. },
  821. ).catch(() => {
  822. console.error("Failed to abort task")
  823. })
  824. if (this.getCurrentCline()) {
  825. // 'abandoned' will prevent this Cline instance from affecting
  826. // future Cline instances. This may happen if its hanging on a
  827. // streaming request.
  828. this.getCurrentCline()!.abandoned = true
  829. }
  830. // Clears task again, so we need to abortTask manually above.
  831. await this.initClineWithHistoryItem({ ...historyItem, rootTask, parentTask })
  832. }
  833. async updateCustomInstructions(instructions?: string) {
  834. // User may be clearing the field.
  835. await this.updateGlobalState("customInstructions", instructions || undefined)
  836. await this.postStateToWebview()
  837. }
  838. // MCP
  839. async ensureMcpServersDirectoryExists(): Promise<string> {
  840. // Get platform-specific application data directory
  841. let mcpServersDir: string
  842. if (process.platform === "win32") {
  843. // Windows: %APPDATA%\Roo-Code\MCP
  844. mcpServersDir = path.join(os.homedir(), "AppData", "Roaming", "Roo-Code", "MCP")
  845. } else if (process.platform === "darwin") {
  846. // macOS: ~/Documents/Cline/MCP
  847. mcpServersDir = path.join(os.homedir(), "Documents", "Cline", "MCP")
  848. } else {
  849. // Linux: ~/.local/share/Cline/MCP
  850. mcpServersDir = path.join(os.homedir(), ".local", "share", "Roo-Code", "MCP")
  851. }
  852. try {
  853. await fs.mkdir(mcpServersDir, { recursive: true })
  854. } catch (error) {
  855. // Fallback to a relative path if directory creation fails
  856. return path.join(os.homedir(), ".roo-code", "mcp")
  857. }
  858. return mcpServersDir
  859. }
  860. async ensureSettingsDirectoryExists(): Promise<string> {
  861. const { getSettingsDirectoryPath } = await import("../../utils/storage")
  862. const globalStoragePath = this.contextProxy.globalStorageUri.fsPath
  863. return getSettingsDirectoryPath(globalStoragePath)
  864. }
  865. // OpenRouter
  866. async handleOpenRouterCallback(code: string) {
  867. let { apiConfiguration, currentApiConfigName } = await this.getState()
  868. let apiKey: string
  869. try {
  870. const baseUrl = apiConfiguration.openRouterBaseUrl || "https://openrouter.ai/api/v1"
  871. // Extract the base domain for the auth endpoint
  872. const baseUrlDomain = baseUrl.match(/^(https?:\/\/[^\/]+)/)?.[1] || "https://openrouter.ai"
  873. const response = await axios.post(`${baseUrlDomain}/api/v1/auth/keys`, { code })
  874. if (response.data && response.data.key) {
  875. apiKey = response.data.key
  876. } else {
  877. throw new Error("Invalid response from OpenRouter API")
  878. }
  879. } catch (error) {
  880. this.log(
  881. `Error exchanging code for API key: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  882. )
  883. throw error
  884. }
  885. const newConfiguration: ProviderSettings = {
  886. ...apiConfiguration,
  887. apiProvider: "openrouter",
  888. openRouterApiKey: apiKey,
  889. openRouterModelId: apiConfiguration?.openRouterModelId || openRouterDefaultModelId,
  890. }
  891. await this.upsertProviderProfile(currentApiConfigName, newConfiguration)
  892. }
  893. // Glama
  894. async handleGlamaCallback(code: string) {
  895. let apiKey: string
  896. try {
  897. const response = await axios.post("https://glama.ai/api/gateway/v1/auth/exchange-code", { code })
  898. if (response.data && response.data.apiKey) {
  899. apiKey = response.data.apiKey
  900. } else {
  901. throw new Error("Invalid response from Glama API")
  902. }
  903. } catch (error) {
  904. this.log(
  905. `Error exchanging code for API key: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  906. )
  907. throw error
  908. }
  909. const { apiConfiguration, currentApiConfigName } = await this.getState()
  910. const newConfiguration: ProviderSettings = {
  911. ...apiConfiguration,
  912. apiProvider: "glama",
  913. glamaApiKey: apiKey,
  914. glamaModelId: apiConfiguration?.glamaModelId || glamaDefaultModelId,
  915. }
  916. await this.upsertProviderProfile(currentApiConfigName, newConfiguration)
  917. }
  918. // Requesty
  919. async handleRequestyCallback(code: string) {
  920. let { apiConfiguration, currentApiConfigName } = await this.getState()
  921. const newConfiguration: ProviderSettings = {
  922. ...apiConfiguration,
  923. apiProvider: "requesty",
  924. requestyApiKey: code,
  925. requestyModelId: apiConfiguration?.requestyModelId || requestyDefaultModelId,
  926. }
  927. await this.upsertProviderProfile(currentApiConfigName, newConfiguration)
  928. }
  929. // Task history
  930. async getTaskWithId(id: string): Promise<{
  931. historyItem: HistoryItem
  932. taskDirPath: string
  933. apiConversationHistoryFilePath: string
  934. uiMessagesFilePath: string
  935. apiConversationHistory: Anthropic.MessageParam[]
  936. }> {
  937. const history = this.getGlobalState("taskHistory") ?? []
  938. const historyItem = history.find((item) => item.id === id)
  939. if (historyItem) {
  940. const { getTaskDirectoryPath } = await import("../../utils/storage")
  941. const globalStoragePath = this.contextProxy.globalStorageUri.fsPath
  942. const taskDirPath = await getTaskDirectoryPath(globalStoragePath, id)
  943. const apiConversationHistoryFilePath = path.join(taskDirPath, GlobalFileNames.apiConversationHistory)
  944. const uiMessagesFilePath = path.join(taskDirPath, GlobalFileNames.uiMessages)
  945. const fileExists = await fileExistsAtPath(apiConversationHistoryFilePath)
  946. if (fileExists) {
  947. const apiConversationHistory = JSON.parse(await fs.readFile(apiConversationHistoryFilePath, "utf8"))
  948. return {
  949. historyItem,
  950. taskDirPath,
  951. apiConversationHistoryFilePath,
  952. uiMessagesFilePath,
  953. apiConversationHistory,
  954. }
  955. }
  956. }
  957. // if we tried to get a task that doesn't exist, remove it from state
  958. // FIXME: this seems to happen sometimes when the json file doesnt save to disk for some reason
  959. await this.deleteTaskFromState(id)
  960. throw new Error("Task not found")
  961. }
  962. async showTaskWithId(id: string) {
  963. if (id !== this.getCurrentCline()?.taskId) {
  964. // Non-current task.
  965. const { historyItem } = await this.getTaskWithId(id)
  966. await this.initClineWithHistoryItem(historyItem) // Clears existing task.
  967. }
  968. await this.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
  969. }
  970. async exportTaskWithId(id: string) {
  971. const { historyItem, apiConversationHistory } = await this.getTaskWithId(id)
  972. await downloadTask(historyItem.ts, apiConversationHistory)
  973. }
  974. /* Condenses a task's message history to use fewer tokens. */
  975. async condenseTaskContext(taskId: string) {
  976. let task: Task | undefined
  977. for (let i = this.clineStack.length - 1; i >= 0; i--) {
  978. if (this.clineStack[i].taskId === taskId) {
  979. task = this.clineStack[i]
  980. break
  981. }
  982. }
  983. if (!task) {
  984. throw new Error(`Task with id ${taskId} not found in stack`)
  985. }
  986. await task.condenseContext()
  987. await this.postMessageToWebview({ type: "condenseTaskContextResponse", text: taskId })
  988. }
  989. // this function deletes a task from task hidtory, and deletes it's checkpoints and delete the task folder
  990. async deleteTaskWithId(id: string) {
  991. try {
  992. // get the task directory full path
  993. const { taskDirPath } = await this.getTaskWithId(id)
  994. // remove task from stack if it's the current task
  995. if (id === this.getCurrentCline()?.taskId) {
  996. // if we found the taskid to delete - call finish to abort this task and allow a new task to be started,
  997. // if we are deleting a subtask and parent task is still waiting for subtask to finish - it allows the parent to resume (this case should neve exist)
  998. await this.finishSubTask(t("common:tasks.deleted"))
  999. }
  1000. // delete task from the task history state
  1001. await this.deleteTaskFromState(id)
  1002. // Delete associated shadow repository or branch.
  1003. // TODO: Store `workspaceDir` in the `HistoryItem` object.
  1004. const globalStorageDir = this.contextProxy.globalStorageUri.fsPath
  1005. const workspaceDir = this.cwd
  1006. try {
  1007. await ShadowCheckpointService.deleteTask({ taskId: id, globalStorageDir, workspaceDir })
  1008. } catch (error) {
  1009. console.error(
  1010. `[deleteTaskWithId${id}] failed to delete associated shadow repository or branch: ${error instanceof Error ? error.message : String(error)}`,
  1011. )
  1012. }
  1013. // delete the entire task directory including checkpoints and all content
  1014. try {
  1015. await fs.rm(taskDirPath, { recursive: true, force: true })
  1016. console.log(`[deleteTaskWithId${id}] removed task directory`)
  1017. } catch (error) {
  1018. console.error(
  1019. `[deleteTaskWithId${id}] failed to remove task directory: ${error instanceof Error ? error.message : String(error)}`,
  1020. )
  1021. }
  1022. } catch (error) {
  1023. // If task is not found, just remove it from state
  1024. if (error instanceof Error && error.message === "Task not found") {
  1025. await this.deleteTaskFromState(id)
  1026. return
  1027. }
  1028. throw error
  1029. }
  1030. }
  1031. async deleteTaskFromState(id: string) {
  1032. const taskHistory = this.getGlobalState("taskHistory") ?? []
  1033. const updatedTaskHistory = taskHistory.filter((task) => task.id !== id)
  1034. await this.updateGlobalState("taskHistory", updatedTaskHistory)
  1035. await this.postStateToWebview()
  1036. }
  1037. async postStateToWebview() {
  1038. const state = await this.getStateToPostToWebview()
  1039. this.postMessageToWebview({ type: "state", state })
  1040. }
  1041. /**
  1042. * Checks if there is a file-based system prompt override for the given mode
  1043. */
  1044. async hasFileBasedSystemPromptOverride(mode: Mode): Promise<boolean> {
  1045. const promptFilePath = getSystemPromptFilePath(this.cwd, mode)
  1046. return await fileExistsAtPath(promptFilePath)
  1047. }
  1048. async getStateToPostToWebview() {
  1049. const {
  1050. apiConfiguration,
  1051. lastShownAnnouncementId,
  1052. customInstructions,
  1053. alwaysAllowReadOnly,
  1054. alwaysAllowReadOnlyOutsideWorkspace,
  1055. alwaysAllowWrite,
  1056. alwaysAllowWriteOutsideWorkspace,
  1057. alwaysAllowExecute,
  1058. alwaysAllowBrowser,
  1059. alwaysAllowMcp,
  1060. alwaysAllowModeSwitch,
  1061. alwaysAllowSubtasks,
  1062. allowedMaxRequests,
  1063. autoCondenseContext,
  1064. autoCondenseContextPercent,
  1065. soundEnabled,
  1066. ttsEnabled,
  1067. ttsSpeed,
  1068. diffEnabled,
  1069. enableCheckpoints,
  1070. taskHistory,
  1071. soundVolume,
  1072. browserViewportSize,
  1073. screenshotQuality,
  1074. remoteBrowserHost,
  1075. remoteBrowserEnabled,
  1076. cachedChromeHostUrl,
  1077. writeDelayMs,
  1078. terminalOutputLineLimit,
  1079. terminalShellIntegrationTimeout,
  1080. terminalShellIntegrationDisabled,
  1081. terminalCommandDelay,
  1082. terminalPowershellCounter,
  1083. terminalZshClearEolMark,
  1084. terminalZshOhMy,
  1085. terminalZshP10k,
  1086. terminalZdotdir,
  1087. fuzzyMatchThreshold,
  1088. mcpEnabled,
  1089. enableMcpServerCreation,
  1090. alwaysApproveResubmit,
  1091. requestDelaySeconds,
  1092. currentApiConfigName,
  1093. listApiConfigMeta,
  1094. pinnedApiConfigs,
  1095. mode,
  1096. customModePrompts,
  1097. customSupportPrompts,
  1098. enhancementApiConfigId,
  1099. autoApprovalEnabled,
  1100. customModes,
  1101. experiments,
  1102. maxOpenTabsContext,
  1103. maxWorkspaceFiles,
  1104. browserToolEnabled,
  1105. telemetrySetting,
  1106. showRooIgnoredFiles,
  1107. language,
  1108. maxReadFileLine,
  1109. terminalCompressProgressBar,
  1110. historyPreviewCollapsed,
  1111. organizationAllowList,
  1112. condensingApiConfigId,
  1113. customCondensingPrompt,
  1114. codebaseIndexConfig,
  1115. codebaseIndexModels,
  1116. } = await this.getState()
  1117. const telemetryKey = process.env.POSTHOG_API_KEY
  1118. const machineId = vscode.env.machineId
  1119. const allowedCommands = vscode.workspace.getConfiguration(Package.name).get<string[]>("allowedCommands") || []
  1120. const cwd = this.cwd
  1121. // Check if there's a system prompt override for the current mode
  1122. const currentMode = mode ?? defaultModeSlug
  1123. const hasSystemPromptOverride = await this.hasFileBasedSystemPromptOverride(currentMode)
  1124. return {
  1125. version: this.context.extension?.packageJSON?.version ?? "",
  1126. apiConfiguration,
  1127. customInstructions,
  1128. alwaysAllowReadOnly: alwaysAllowReadOnly ?? false,
  1129. alwaysAllowReadOnlyOutsideWorkspace: alwaysAllowReadOnlyOutsideWorkspace ?? false,
  1130. alwaysAllowWrite: alwaysAllowWrite ?? false,
  1131. alwaysAllowWriteOutsideWorkspace: alwaysAllowWriteOutsideWorkspace ?? false,
  1132. alwaysAllowExecute: alwaysAllowExecute ?? false,
  1133. alwaysAllowBrowser: alwaysAllowBrowser ?? false,
  1134. alwaysAllowMcp: alwaysAllowMcp ?? false,
  1135. alwaysAllowModeSwitch: alwaysAllowModeSwitch ?? false,
  1136. alwaysAllowSubtasks: alwaysAllowSubtasks ?? false,
  1137. allowedMaxRequests,
  1138. autoCondenseContext: autoCondenseContext ?? true,
  1139. autoCondenseContextPercent: autoCondenseContextPercent ?? 100,
  1140. uriScheme: vscode.env.uriScheme,
  1141. currentTaskItem: this.getCurrentCline()?.taskId
  1142. ? (taskHistory || []).find((item: HistoryItem) => item.id === this.getCurrentCline()?.taskId)
  1143. : undefined,
  1144. clineMessages: this.getCurrentCline()?.clineMessages || [],
  1145. taskHistory: (taskHistory || [])
  1146. .filter((item: HistoryItem) => item.ts && item.task)
  1147. .sort((a: HistoryItem, b: HistoryItem) => b.ts - a.ts),
  1148. soundEnabled: soundEnabled ?? false,
  1149. ttsEnabled: ttsEnabled ?? false,
  1150. ttsSpeed: ttsSpeed ?? 1.0,
  1151. diffEnabled: diffEnabled ?? true,
  1152. enableCheckpoints: enableCheckpoints ?? true,
  1153. shouldShowAnnouncement:
  1154. telemetrySetting !== "unset" && lastShownAnnouncementId !== this.latestAnnouncementId,
  1155. allowedCommands,
  1156. soundVolume: soundVolume ?? 0.5,
  1157. browserViewportSize: browserViewportSize ?? "900x600",
  1158. screenshotQuality: screenshotQuality ?? 75,
  1159. remoteBrowserHost,
  1160. remoteBrowserEnabled: remoteBrowserEnabled ?? false,
  1161. cachedChromeHostUrl: cachedChromeHostUrl,
  1162. writeDelayMs: writeDelayMs ?? 1000,
  1163. terminalOutputLineLimit: terminalOutputLineLimit ?? 500,
  1164. terminalShellIntegrationTimeout: terminalShellIntegrationTimeout ?? Terminal.defaultShellIntegrationTimeout,
  1165. terminalShellIntegrationDisabled: terminalShellIntegrationDisabled ?? false,
  1166. terminalCommandDelay: terminalCommandDelay ?? 0,
  1167. terminalPowershellCounter: terminalPowershellCounter ?? false,
  1168. terminalZshClearEolMark: terminalZshClearEolMark ?? true,
  1169. terminalZshOhMy: terminalZshOhMy ?? false,
  1170. terminalZshP10k: terminalZshP10k ?? false,
  1171. terminalZdotdir: terminalZdotdir ?? false,
  1172. fuzzyMatchThreshold: fuzzyMatchThreshold ?? 1.0,
  1173. mcpEnabled: mcpEnabled ?? true,
  1174. enableMcpServerCreation: enableMcpServerCreation ?? true,
  1175. alwaysApproveResubmit: alwaysApproveResubmit ?? false,
  1176. requestDelaySeconds: requestDelaySeconds ?? 10,
  1177. currentApiConfigName: currentApiConfigName ?? "default",
  1178. listApiConfigMeta: listApiConfigMeta ?? [],
  1179. pinnedApiConfigs: pinnedApiConfigs ?? {},
  1180. mode: mode ?? defaultModeSlug,
  1181. customModePrompts: customModePrompts ?? {},
  1182. customSupportPrompts: customSupportPrompts ?? {},
  1183. enhancementApiConfigId,
  1184. autoApprovalEnabled: autoApprovalEnabled ?? false,
  1185. customModes,
  1186. experiments: experiments ?? experimentDefault,
  1187. mcpServers: this.mcpHub?.getAllServers() ?? [],
  1188. maxOpenTabsContext: maxOpenTabsContext ?? 20,
  1189. maxWorkspaceFiles: maxWorkspaceFiles ?? 200,
  1190. cwd,
  1191. browserToolEnabled: browserToolEnabled ?? true,
  1192. telemetrySetting,
  1193. telemetryKey,
  1194. machineId,
  1195. showRooIgnoredFiles: showRooIgnoredFiles ?? true,
  1196. language: language ?? formatLanguage(vscode.env.language),
  1197. renderContext: this.renderContext,
  1198. maxReadFileLine: maxReadFileLine ?? -1,
  1199. settingsImportedAt: this.settingsImportedAt,
  1200. terminalCompressProgressBar: terminalCompressProgressBar ?? true,
  1201. hasSystemPromptOverride,
  1202. historyPreviewCollapsed: historyPreviewCollapsed ?? false,
  1203. organizationAllowList,
  1204. condensingApiConfigId,
  1205. customCondensingPrompt,
  1206. codebaseIndexModels: codebaseIndexModels ?? EMBEDDING_MODEL_PROFILES,
  1207. codebaseIndexConfig: codebaseIndexConfig ?? {
  1208. codebaseIndexEnabled: false,
  1209. codebaseIndexQdrantUrl: "http://localhost:6333",
  1210. codebaseIndexEmbedderProvider: "openai",
  1211. codebaseIndexEmbedderBaseUrl: "",
  1212. codebaseIndexEmbedderModelId: "",
  1213. },
  1214. }
  1215. }
  1216. /**
  1217. * Storage
  1218. * https://dev.to/kompotkot/how-to-use-secretstorage-in-your-vscode-extensions-2hco
  1219. * https://www.eliostruyf.com/devhack-code-extension-storage-options/
  1220. */
  1221. async getState() {
  1222. const stateValues = this.contextProxy.getValues()
  1223. const customModes = await this.customModesManager.getCustomModes()
  1224. // Determine apiProvider with the same logic as before.
  1225. const apiProvider: ProviderName = stateValues.apiProvider ? stateValues.apiProvider : "anthropic"
  1226. // Build the apiConfiguration object combining state values and secrets.
  1227. const providerSettings = this.contextProxy.getProviderSettings()
  1228. // Ensure apiProvider is set properly if not already in state
  1229. if (!providerSettings.apiProvider) {
  1230. providerSettings.apiProvider = apiProvider
  1231. }
  1232. let organizationAllowList = ORGANIZATION_ALLOW_ALL
  1233. try {
  1234. organizationAllowList = await CloudService.instance.getAllowList()
  1235. } catch (error) {
  1236. console.error(
  1237. `[getState] failed to get organization allow list: ${error instanceof Error ? error.message : String(error)}`,
  1238. )
  1239. }
  1240. // Return the same structure as before
  1241. return {
  1242. apiConfiguration: providerSettings,
  1243. lastShownAnnouncementId: stateValues.lastShownAnnouncementId,
  1244. customInstructions: stateValues.customInstructions,
  1245. apiModelId: stateValues.apiModelId,
  1246. alwaysAllowReadOnly: stateValues.alwaysAllowReadOnly ?? false,
  1247. alwaysAllowReadOnlyOutsideWorkspace: stateValues.alwaysAllowReadOnlyOutsideWorkspace ?? false,
  1248. alwaysAllowWrite: stateValues.alwaysAllowWrite ?? false,
  1249. alwaysAllowWriteOutsideWorkspace: stateValues.alwaysAllowWriteOutsideWorkspace ?? false,
  1250. alwaysAllowExecute: stateValues.alwaysAllowExecute ?? false,
  1251. alwaysAllowBrowser: stateValues.alwaysAllowBrowser ?? false,
  1252. alwaysAllowMcp: stateValues.alwaysAllowMcp ?? false,
  1253. alwaysAllowModeSwitch: stateValues.alwaysAllowModeSwitch ?? false,
  1254. alwaysAllowSubtasks: stateValues.alwaysAllowSubtasks ?? false,
  1255. allowedMaxRequests: stateValues.allowedMaxRequests,
  1256. autoCondenseContext: stateValues.autoCondenseContext ?? true,
  1257. autoCondenseContextPercent: stateValues.autoCondenseContextPercent ?? 100,
  1258. taskHistory: stateValues.taskHistory,
  1259. allowedCommands: stateValues.allowedCommands,
  1260. soundEnabled: stateValues.soundEnabled ?? false,
  1261. ttsEnabled: stateValues.ttsEnabled ?? false,
  1262. ttsSpeed: stateValues.ttsSpeed ?? 1.0,
  1263. diffEnabled: stateValues.diffEnabled ?? true,
  1264. enableCheckpoints: stateValues.enableCheckpoints ?? true,
  1265. soundVolume: stateValues.soundVolume,
  1266. browserViewportSize: stateValues.browserViewportSize ?? "900x600",
  1267. screenshotQuality: stateValues.screenshotQuality ?? 75,
  1268. remoteBrowserHost: stateValues.remoteBrowserHost,
  1269. remoteBrowserEnabled: stateValues.remoteBrowserEnabled ?? false,
  1270. cachedChromeHostUrl: stateValues.cachedChromeHostUrl as string | undefined,
  1271. fuzzyMatchThreshold: stateValues.fuzzyMatchThreshold ?? 1.0,
  1272. writeDelayMs: stateValues.writeDelayMs ?? 1000,
  1273. terminalOutputLineLimit: stateValues.terminalOutputLineLimit ?? 500,
  1274. terminalShellIntegrationTimeout:
  1275. stateValues.terminalShellIntegrationTimeout ?? Terminal.defaultShellIntegrationTimeout,
  1276. terminalShellIntegrationDisabled: stateValues.terminalShellIntegrationDisabled ?? false,
  1277. terminalCommandDelay: stateValues.terminalCommandDelay ?? 0,
  1278. terminalPowershellCounter: stateValues.terminalPowershellCounter ?? false,
  1279. terminalZshClearEolMark: stateValues.terminalZshClearEolMark ?? true,
  1280. terminalZshOhMy: stateValues.terminalZshOhMy ?? false,
  1281. terminalZshP10k: stateValues.terminalZshP10k ?? false,
  1282. terminalZdotdir: stateValues.terminalZdotdir ?? false,
  1283. terminalCompressProgressBar: stateValues.terminalCompressProgressBar ?? true,
  1284. mode: stateValues.mode ?? defaultModeSlug,
  1285. language: stateValues.language ?? formatLanguage(vscode.env.language),
  1286. mcpEnabled: stateValues.mcpEnabled ?? true,
  1287. enableMcpServerCreation: stateValues.enableMcpServerCreation ?? true,
  1288. alwaysApproveResubmit: stateValues.alwaysApproveResubmit ?? false,
  1289. requestDelaySeconds: Math.max(5, stateValues.requestDelaySeconds ?? 10),
  1290. currentApiConfigName: stateValues.currentApiConfigName ?? "default",
  1291. listApiConfigMeta: stateValues.listApiConfigMeta ?? [],
  1292. pinnedApiConfigs: stateValues.pinnedApiConfigs ?? {},
  1293. modeApiConfigs: stateValues.modeApiConfigs ?? ({} as Record<Mode, string>),
  1294. customModePrompts: stateValues.customModePrompts ?? {},
  1295. customSupportPrompts: stateValues.customSupportPrompts ?? {},
  1296. enhancementApiConfigId: stateValues.enhancementApiConfigId,
  1297. experiments: stateValues.experiments ?? experimentDefault,
  1298. autoApprovalEnabled: stateValues.autoApprovalEnabled ?? false,
  1299. customModes,
  1300. maxOpenTabsContext: stateValues.maxOpenTabsContext ?? 20,
  1301. maxWorkspaceFiles: stateValues.maxWorkspaceFiles ?? 200,
  1302. openRouterUseMiddleOutTransform: stateValues.openRouterUseMiddleOutTransform ?? true,
  1303. browserToolEnabled: stateValues.browserToolEnabled ?? true,
  1304. telemetrySetting: stateValues.telemetrySetting || "unset",
  1305. showRooIgnoredFiles: stateValues.showRooIgnoredFiles ?? true,
  1306. maxReadFileLine: stateValues.maxReadFileLine ?? -1,
  1307. historyPreviewCollapsed: stateValues.historyPreviewCollapsed ?? false,
  1308. organizationAllowList,
  1309. // Explicitly add condensing settings
  1310. condensingApiConfigId: stateValues.condensingApiConfigId,
  1311. customCondensingPrompt: stateValues.customCondensingPrompt,
  1312. codebaseIndexModels: stateValues.codebaseIndexModels ?? EMBEDDING_MODEL_PROFILES,
  1313. codebaseIndexConfig: stateValues.codebaseIndexConfig ?? {
  1314. codebaseIndexEnabled: false,
  1315. codebaseIndexQdrantUrl: "http://localhost:6333",
  1316. codebaseIndexEmbedderProvider: "openai",
  1317. codebaseIndexEmbedderBaseUrl: "",
  1318. codebaseIndexEmbedderModelId: "",
  1319. },
  1320. }
  1321. }
  1322. async updateTaskHistory(item: HistoryItem): Promise<HistoryItem[]> {
  1323. const history = (this.getGlobalState("taskHistory") as HistoryItem[] | undefined) || []
  1324. const existingItemIndex = history.findIndex((h) => h.id === item.id)
  1325. if (existingItemIndex !== -1) {
  1326. history[existingItemIndex] = item
  1327. } else {
  1328. history.push(item)
  1329. }
  1330. await this.updateGlobalState("taskHistory", history)
  1331. return history
  1332. }
  1333. // ContextProxy
  1334. // @deprecated - Use `ContextProxy#setValue` instead.
  1335. private async updateGlobalState<K extends keyof GlobalState>(key: K, value: GlobalState[K]) {
  1336. await this.contextProxy.setValue(key, value)
  1337. }
  1338. // @deprecated - Use `ContextProxy#getValue` instead.
  1339. private getGlobalState<K extends keyof GlobalState>(key: K) {
  1340. return this.contextProxy.getValue(key)
  1341. }
  1342. public async setValue<K extends keyof RooCodeSettings>(key: K, value: RooCodeSettings[K]) {
  1343. await this.contextProxy.setValue(key, value)
  1344. }
  1345. public getValue<K extends keyof RooCodeSettings>(key: K) {
  1346. return this.contextProxy.getValue(key)
  1347. }
  1348. public getValues() {
  1349. return this.contextProxy.getValues()
  1350. }
  1351. public async setValues(values: RooCodeSettings) {
  1352. await this.contextProxy.setValues(values)
  1353. }
  1354. // cwd
  1355. get cwd() {
  1356. return getWorkspacePath()
  1357. }
  1358. // dev
  1359. async resetState() {
  1360. const answer = await vscode.window.showInformationMessage(
  1361. t("common:confirmation.reset_state"),
  1362. { modal: true },
  1363. t("common:answers.yes"),
  1364. )
  1365. if (answer !== t("common:answers.yes")) {
  1366. return
  1367. }
  1368. await this.contextProxy.resetAllState()
  1369. await this.providerSettingsManager.resetAllConfigs()
  1370. await this.customModesManager.resetCustomModes()
  1371. await this.removeClineFromStack()
  1372. await this.postStateToWebview()
  1373. await this.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
  1374. }
  1375. // logging
  1376. public log(message: string) {
  1377. this.outputChannel.appendLine(message)
  1378. console.log(message)
  1379. }
  1380. // integration tests
  1381. get viewLaunched() {
  1382. return this.isViewLaunched
  1383. }
  1384. get messages() {
  1385. return this.getCurrentCline()?.clineMessages || []
  1386. }
  1387. // Add public getter
  1388. public getMcpHub(): McpHub | undefined {
  1389. return this.mcpHub
  1390. }
  1391. /**
  1392. * Returns properties to be included in every telemetry event
  1393. * This method is called by the telemetry service to get context information
  1394. * like the current mode, API provider, etc.
  1395. */
  1396. public async getTelemetryProperties(): Promise<TelemetryProperties> {
  1397. const { mode, apiConfiguration, language } = await this.getState()
  1398. const task = this.getCurrentCline()
  1399. return {
  1400. appVersion: this.context.extension?.packageJSON?.version,
  1401. vscodeVersion: vscode.version,
  1402. platform: process.platform,
  1403. editorName: vscode.env.appName,
  1404. language,
  1405. mode,
  1406. apiProvider: apiConfiguration?.apiProvider,
  1407. modelId: task?.api?.getModel().id,
  1408. diffStrategy: task?.diffStrategy?.getName(),
  1409. isSubtask: task ? !!task.parentTask : undefined,
  1410. }
  1411. }
  1412. }