ClineProvider.ts 114 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263
  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 TaskProviderLike,
  12. type TaskProviderEvents,
  13. type GlobalState,
  14. type ProviderName,
  15. type ProviderSettings,
  16. type RooCodeSettings,
  17. type ProviderSettingsEntry,
  18. type StaticAppProperties,
  19. type DynamicAppProperties,
  20. type CloudAppProperties,
  21. type TaskProperties,
  22. type GitProperties,
  23. type TelemetryProperties,
  24. type TelemetryPropertiesProvider,
  25. type CodeActionId,
  26. type CodeActionName,
  27. type TerminalActionId,
  28. type TerminalActionPromptType,
  29. type HistoryItem,
  30. type CloudUserInfo,
  31. type CloudOrganizationMembership,
  32. type CreateTaskOptions,
  33. type TokenUsage,
  34. type ToolUsage,
  35. RooCodeEventName,
  36. requestyDefaultModelId,
  37. openRouterDefaultModelId,
  38. DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT,
  39. DEFAULT_WRITE_DELAY_MS,
  40. ORGANIZATION_ALLOW_ALL,
  41. DEFAULT_MODES,
  42. DEFAULT_CHECKPOINT_TIMEOUT_SECONDS,
  43. getModelId,
  44. } from "@roo-code/types"
  45. import { TelemetryService } from "@roo-code/telemetry"
  46. import { CloudService, BridgeOrchestrator, getRooCodeApiUrl } from "@roo-code/cloud"
  47. import { Package } from "../../shared/package"
  48. import { findLast } from "../../shared/array"
  49. import { supportPrompt } from "../../shared/support-prompt"
  50. import { GlobalFileNames } from "../../shared/globalFileNames"
  51. import type { ExtensionMessage, ExtensionState, MarketplaceInstalledMetadata } from "../../shared/ExtensionMessage"
  52. import { Mode, defaultModeSlug, getModeBySlug } from "../../shared/modes"
  53. import { experimentDefault } from "../../shared/experiments"
  54. import { formatLanguage } from "../../shared/language"
  55. import { WebviewMessage } from "../../shared/WebviewMessage"
  56. import { EMBEDDING_MODEL_PROFILES } from "../../shared/embeddingModels"
  57. import { ProfileValidator } from "../../shared/ProfileValidator"
  58. import { Terminal } from "../../integrations/terminal/Terminal"
  59. import { downloadTask } from "../../integrations/misc/export-markdown"
  60. import { getTheme } from "../../integrations/theme/getTheme"
  61. import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
  62. import { McpHub } from "../../services/mcp/McpHub"
  63. import { McpServerManager } from "../../services/mcp/McpServerManager"
  64. import { MarketplaceManager } from "../../services/marketplace"
  65. import { ShadowCheckpointService } from "../../services/checkpoints/ShadowCheckpointService"
  66. import { CodeIndexManager } from "../../services/code-index/manager"
  67. import type { IndexProgressUpdate } from "../../services/code-index/interfaces/manager"
  68. import { MdmService } from "../../services/mdm/MdmService"
  69. import { fileExistsAtPath } from "../../utils/fs"
  70. import { setTtsEnabled, setTtsSpeed } from "../../utils/tts"
  71. import { getWorkspaceGitInfo } from "../../utils/git"
  72. import { getWorkspacePath } from "../../utils/path"
  73. import { OrganizationAllowListViolationError } from "../../utils/errors"
  74. import { setPanel } from "../../activate/registerCommands"
  75. import { t } from "../../i18n"
  76. import { buildApiHandler } from "../../api"
  77. import { forceFullModelDetailsLoad, hasLoadedFullDetails } from "../../api/providers/fetchers/lmstudio"
  78. import { ContextProxy } from "../config/ContextProxy"
  79. import { ProviderSettingsManager } from "../config/ProviderSettingsManager"
  80. import { CustomModesManager } from "../config/CustomModesManager"
  81. import { Task } from "../task/Task"
  82. import { getSystemPromptFilePath } from "../prompts/sections/custom-system-prompt"
  83. import { webviewMessageHandler } from "./webviewMessageHandler"
  84. import type { ClineMessage, TodoItem } from "@roo-code/types"
  85. import { readApiMessages, saveApiMessages, saveTaskMessages } from "../task-persistence"
  86. import { readTaskMessages } from "../task-persistence/taskMessages"
  87. import { getNonce } from "./getNonce"
  88. import { getUri } from "./getUri"
  89. import { REQUESTY_BASE_URL } from "../../shared/utils/requesty"
  90. /**
  91. * https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
  92. * https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/customSidebarViewProvider.ts
  93. */
  94. export type ClineProviderEvents = {
  95. clineCreated: [cline: Task]
  96. }
  97. interface PendingEditOperation {
  98. messageTs: number
  99. editedContent: string
  100. images?: string[]
  101. messageIndex: number
  102. apiConversationHistoryIndex: number
  103. timeoutId: NodeJS.Timeout
  104. createdAt: number
  105. }
  106. export class ClineProvider
  107. extends EventEmitter<TaskProviderEvents>
  108. implements vscode.WebviewViewProvider, TelemetryPropertiesProvider, TaskProviderLike
  109. {
  110. // Used in package.json as the view's id. This value cannot be changed due
  111. // to how VSCode caches views based on their id, and updating the id would
  112. // break existing instances of the extension.
  113. public static readonly sideBarId = `${Package.name}.SidebarProvider`
  114. public static readonly tabPanelId = `${Package.name}.TabPanelProvider`
  115. private static activeInstances: Set<ClineProvider> = new Set()
  116. private disposables: vscode.Disposable[] = []
  117. private webviewDisposables: vscode.Disposable[] = []
  118. private view?: vscode.WebviewView | vscode.WebviewPanel
  119. private clineStack: Task[] = []
  120. private codeIndexStatusSubscription?: vscode.Disposable
  121. private codeIndexManager?: CodeIndexManager
  122. private _workspaceTracker?: WorkspaceTracker // workSpaceTracker read-only for access outside this class
  123. protected mcpHub?: McpHub // Change from private to protected
  124. private marketplaceManager: MarketplaceManager
  125. private mdmService?: MdmService
  126. private taskCreationCallback: (task: Task) => void
  127. private taskEventListeners: WeakMap<Task, Array<() => void>> = new WeakMap()
  128. private currentWorkspacePath: string | undefined
  129. private recentTasksCache?: string[]
  130. private pendingOperations: Map<string, PendingEditOperation> = new Map()
  131. private static readonly PENDING_OPERATION_TIMEOUT_MS = 30000 // 30 seconds
  132. private cloudOrganizationsCache: CloudOrganizationMembership[] | null = null
  133. private cloudOrganizationsCacheTimestamp: number | null = null
  134. private static readonly CLOUD_ORGANIZATIONS_CACHE_DURATION_MS = 5 * 1000 // 5 seconds
  135. public isViewLaunched = false
  136. public settingsImportedAt?: number
  137. public readonly latestAnnouncementId = "dec-2025-v3.36.0-context-rewind-roo-provider" // v3.36.0 Context Rewind & Roo Provider Improvements
  138. public readonly providerSettingsManager: ProviderSettingsManager
  139. public readonly customModesManager: CustomModesManager
  140. constructor(
  141. readonly context: vscode.ExtensionContext,
  142. private readonly outputChannel: vscode.OutputChannel,
  143. private readonly renderContext: "sidebar" | "editor" = "sidebar",
  144. public readonly contextProxy: ContextProxy,
  145. mdmService?: MdmService,
  146. ) {
  147. super()
  148. this.currentWorkspacePath = getWorkspacePath()
  149. ClineProvider.activeInstances.add(this)
  150. this.mdmService = mdmService
  151. this.updateGlobalState("codebaseIndexModels", EMBEDDING_MODEL_PROFILES)
  152. // Start configuration loading (which might trigger indexing) in the background.
  153. // Don't await, allowing activation to continue immediately.
  154. // Register this provider with the telemetry service to enable it to add
  155. // properties like mode and provider.
  156. TelemetryService.instance.setProvider(this)
  157. this._workspaceTracker = new WorkspaceTracker(this)
  158. this.providerSettingsManager = new ProviderSettingsManager(this.context)
  159. this.customModesManager = new CustomModesManager(this.context, async () => {
  160. await this.postStateToWebview()
  161. })
  162. // Initialize MCP Hub through the singleton manager
  163. McpServerManager.getInstance(this.context, this)
  164. .then((hub) => {
  165. this.mcpHub = hub
  166. this.mcpHub.registerClient()
  167. })
  168. .catch((error) => {
  169. this.log(`Failed to initialize MCP Hub: ${error}`)
  170. })
  171. this.marketplaceManager = new MarketplaceManager(this.context, this.customModesManager)
  172. // Forward <most> task events to the provider.
  173. // We do something fairly similar for the IPC-based API.
  174. this.taskCreationCallback = (instance: Task) => {
  175. this.emit(RooCodeEventName.TaskCreated, instance)
  176. // Create named listener functions so we can remove them later.
  177. const onTaskStarted = () => this.emit(RooCodeEventName.TaskStarted, instance.taskId)
  178. const onTaskCompleted = (taskId: string, tokenUsage: TokenUsage, toolUsage: ToolUsage) =>
  179. this.emit(RooCodeEventName.TaskCompleted, taskId, tokenUsage, toolUsage)
  180. const onTaskAborted = async () => {
  181. this.emit(RooCodeEventName.TaskAborted, instance.taskId)
  182. try {
  183. // Only rehydrate on genuine streaming failures.
  184. // User-initiated cancels are handled by cancelTask().
  185. if (instance.abortReason === "streaming_failed") {
  186. // Defensive safeguard: if another path already replaced this instance, skip
  187. const current = this.getCurrentTask()
  188. if (current && current.instanceId !== instance.instanceId) {
  189. this.log(
  190. `[onTaskAborted] Skipping rehydrate: current instance ${current.instanceId} != aborted ${instance.instanceId}`,
  191. )
  192. return
  193. }
  194. const { historyItem } = await this.getTaskWithId(instance.taskId)
  195. const rootTask = instance.rootTask
  196. const parentTask = instance.parentTask
  197. await this.createTaskWithHistoryItem({ ...historyItem, rootTask, parentTask })
  198. }
  199. } catch (error) {
  200. this.log(
  201. `[onTaskAborted] Failed to rehydrate after streaming failure: ${
  202. error instanceof Error ? error.message : String(error)
  203. }`,
  204. )
  205. }
  206. }
  207. const onTaskFocused = () => this.emit(RooCodeEventName.TaskFocused, instance.taskId)
  208. const onTaskUnfocused = () => this.emit(RooCodeEventName.TaskUnfocused, instance.taskId)
  209. const onTaskActive = (taskId: string) => this.emit(RooCodeEventName.TaskActive, taskId)
  210. const onTaskInteractive = (taskId: string) => this.emit(RooCodeEventName.TaskInteractive, taskId)
  211. const onTaskResumable = (taskId: string) => this.emit(RooCodeEventName.TaskResumable, taskId)
  212. const onTaskIdle = (taskId: string) => this.emit(RooCodeEventName.TaskIdle, taskId)
  213. const onTaskPaused = (taskId: string) => this.emit(RooCodeEventName.TaskPaused, taskId)
  214. const onTaskUnpaused = (taskId: string) => this.emit(RooCodeEventName.TaskUnpaused, taskId)
  215. const onTaskSpawned = (taskId: string) => this.emit(RooCodeEventName.TaskSpawned, taskId)
  216. const onTaskUserMessage = (taskId: string) => this.emit(RooCodeEventName.TaskUserMessage, taskId)
  217. const onTaskTokenUsageUpdated = (taskId: string, tokenUsage: TokenUsage, toolUsage: ToolUsage) =>
  218. this.emit(RooCodeEventName.TaskTokenUsageUpdated, taskId, tokenUsage, toolUsage)
  219. // Attach the listeners.
  220. instance.on(RooCodeEventName.TaskStarted, onTaskStarted)
  221. instance.on(RooCodeEventName.TaskCompleted, onTaskCompleted)
  222. instance.on(RooCodeEventName.TaskAborted, onTaskAborted)
  223. instance.on(RooCodeEventName.TaskFocused, onTaskFocused)
  224. instance.on(RooCodeEventName.TaskUnfocused, onTaskUnfocused)
  225. instance.on(RooCodeEventName.TaskActive, onTaskActive)
  226. instance.on(RooCodeEventName.TaskInteractive, onTaskInteractive)
  227. instance.on(RooCodeEventName.TaskResumable, onTaskResumable)
  228. instance.on(RooCodeEventName.TaskIdle, onTaskIdle)
  229. instance.on(RooCodeEventName.TaskPaused, onTaskPaused)
  230. instance.on(RooCodeEventName.TaskUnpaused, onTaskUnpaused)
  231. instance.on(RooCodeEventName.TaskSpawned, onTaskSpawned)
  232. instance.on(RooCodeEventName.TaskUserMessage, onTaskUserMessage)
  233. instance.on(RooCodeEventName.TaskTokenUsageUpdated, onTaskTokenUsageUpdated)
  234. // Store the cleanup functions for later removal.
  235. this.taskEventListeners.set(instance, [
  236. () => instance.off(RooCodeEventName.TaskStarted, onTaskStarted),
  237. () => instance.off(RooCodeEventName.TaskCompleted, onTaskCompleted),
  238. () => instance.off(RooCodeEventName.TaskAborted, onTaskAborted),
  239. () => instance.off(RooCodeEventName.TaskFocused, onTaskFocused),
  240. () => instance.off(RooCodeEventName.TaskUnfocused, onTaskUnfocused),
  241. () => instance.off(RooCodeEventName.TaskActive, onTaskActive),
  242. () => instance.off(RooCodeEventName.TaskInteractive, onTaskInteractive),
  243. () => instance.off(RooCodeEventName.TaskResumable, onTaskResumable),
  244. () => instance.off(RooCodeEventName.TaskIdle, onTaskIdle),
  245. () => instance.off(RooCodeEventName.TaskUserMessage, onTaskUserMessage),
  246. () => instance.off(RooCodeEventName.TaskPaused, onTaskPaused),
  247. () => instance.off(RooCodeEventName.TaskUnpaused, onTaskUnpaused),
  248. () => instance.off(RooCodeEventName.TaskSpawned, onTaskSpawned),
  249. () => instance.off(RooCodeEventName.TaskTokenUsageUpdated, onTaskTokenUsageUpdated),
  250. ])
  251. }
  252. // Initialize Roo Code Cloud profile sync.
  253. if (CloudService.hasInstance()) {
  254. this.initializeCloudProfileSync().catch((error) => {
  255. this.log(`Failed to initialize cloud profile sync: ${error}`)
  256. })
  257. } else {
  258. this.log("CloudService not ready, deferring cloud profile sync")
  259. }
  260. }
  261. /**
  262. * Override EventEmitter's on method to match TaskProviderLike interface
  263. */
  264. override on<K extends keyof TaskProviderEvents>(
  265. event: K,
  266. listener: (...args: TaskProviderEvents[K]) => void | Promise<void>,
  267. ): this {
  268. return super.on(event, listener as any)
  269. }
  270. /**
  271. * Override EventEmitter's off method to match TaskProviderLike interface
  272. */
  273. override off<K extends keyof TaskProviderEvents>(
  274. event: K,
  275. listener: (...args: TaskProviderEvents[K]) => void | Promise<void>,
  276. ): this {
  277. return super.off(event, listener as any)
  278. }
  279. /**
  280. * Initialize cloud profile synchronization
  281. */
  282. private async initializeCloudProfileSync() {
  283. try {
  284. // Check if authenticated and sync profiles
  285. if (CloudService.hasInstance() && CloudService.instance.isAuthenticated()) {
  286. await this.syncCloudProfiles()
  287. }
  288. // Set up listener for future updates
  289. if (CloudService.hasInstance()) {
  290. CloudService.instance.on("settings-updated", this.handleCloudSettingsUpdate)
  291. }
  292. } catch (error) {
  293. this.log(`Error in initializeCloudProfileSync: ${error}`)
  294. }
  295. }
  296. /**
  297. * Handle cloud settings updates
  298. */
  299. private handleCloudSettingsUpdate = async () => {
  300. try {
  301. await this.syncCloudProfiles()
  302. } catch (error) {
  303. this.log(`Error handling cloud settings update: ${error}`)
  304. }
  305. }
  306. /**
  307. * Synchronize cloud profiles with local profiles.
  308. */
  309. private async syncCloudProfiles() {
  310. try {
  311. const settings = CloudService.instance.getOrganizationSettings()
  312. if (!settings?.providerProfiles) {
  313. return
  314. }
  315. const currentApiConfigName = this.getGlobalState("currentApiConfigName")
  316. const result = await this.providerSettingsManager.syncCloudProfiles(
  317. settings.providerProfiles,
  318. currentApiConfigName,
  319. )
  320. if (result.hasChanges) {
  321. // Update list.
  322. await this.updateGlobalState("listApiConfigMeta", await this.providerSettingsManager.listConfig())
  323. if (result.activeProfileChanged && result.activeProfileId) {
  324. // Reload full settings for new active profile.
  325. const profile = await this.providerSettingsManager.getProfile({
  326. id: result.activeProfileId,
  327. })
  328. await this.activateProviderProfile({ name: profile.name })
  329. }
  330. await this.postStateToWebview()
  331. }
  332. } catch (error) {
  333. this.log(`Error syncing cloud profiles: ${error}`)
  334. }
  335. }
  336. /**
  337. * Initialize cloud profile synchronization when CloudService is ready
  338. * This method is called externally after CloudService has been initialized
  339. */
  340. public async initializeCloudProfileSyncWhenReady(): Promise<void> {
  341. try {
  342. if (CloudService.hasInstance() && CloudService.instance.isAuthenticated()) {
  343. await this.syncCloudProfiles()
  344. }
  345. if (CloudService.hasInstance()) {
  346. CloudService.instance.off("settings-updated", this.handleCloudSettingsUpdate)
  347. CloudService.instance.on("settings-updated", this.handleCloudSettingsUpdate)
  348. }
  349. } catch (error) {
  350. this.log(`Failed to initialize cloud profile sync when ready: ${error}`)
  351. }
  352. }
  353. // Adds a new Task instance to clineStack, marking the start of a new task.
  354. // The instance is pushed to the top of the stack (LIFO order).
  355. // When the task is completed, the top instance is removed, reactivating the
  356. // previous task.
  357. async addClineToStack(task: Task) {
  358. // Add this cline instance into the stack that represents the order of
  359. // all the called tasks.
  360. this.clineStack.push(task)
  361. task.emit(RooCodeEventName.TaskFocused)
  362. // Perform special setup provider specific tasks.
  363. await this.performPreparationTasks(task)
  364. // Ensure getState() resolves correctly.
  365. const state = await this.getState()
  366. if (!state || typeof state.mode !== "string") {
  367. throw new Error(t("common:errors.retrieve_current_mode"))
  368. }
  369. }
  370. async performPreparationTasks(cline: Task) {
  371. // LMStudio: We need to force model loading in order to read its context
  372. // size; we do it now since we're starting a task with that model selected.
  373. if (cline.apiConfiguration && cline.apiConfiguration.apiProvider === "lmstudio") {
  374. try {
  375. if (!hasLoadedFullDetails(cline.apiConfiguration.lmStudioModelId!)) {
  376. await forceFullModelDetailsLoad(
  377. cline.apiConfiguration.lmStudioBaseUrl ?? "http://localhost:1234",
  378. cline.apiConfiguration.lmStudioModelId!,
  379. )
  380. }
  381. } catch (error) {
  382. this.log(`Failed to load full model details for LM Studio: ${error}`)
  383. vscode.window.showErrorMessage(error.message)
  384. }
  385. }
  386. }
  387. // Removes and destroys the top Cline instance (the current finished task),
  388. // activating the previous one (resuming the parent task).
  389. async removeClineFromStack() {
  390. if (this.clineStack.length === 0) {
  391. return
  392. }
  393. // Pop the top Cline instance from the stack.
  394. let task = this.clineStack.pop()
  395. if (task) {
  396. task.emit(RooCodeEventName.TaskUnfocused)
  397. try {
  398. // Abort the running task and set isAbandoned to true so
  399. // all running promises will exit as well.
  400. await task.abortTask(true)
  401. } catch (e) {
  402. this.log(
  403. `[ClineProvider#removeClineFromStack] abortTask() failed ${task.taskId}.${task.instanceId}: ${e.message}`,
  404. )
  405. }
  406. // Remove event listeners before clearing the reference.
  407. const cleanupFunctions = this.taskEventListeners.get(task)
  408. if (cleanupFunctions) {
  409. cleanupFunctions.forEach((cleanup) => cleanup())
  410. this.taskEventListeners.delete(task)
  411. }
  412. // Make sure no reference kept, once promises end it will be
  413. // garbage collected.
  414. task = undefined
  415. }
  416. }
  417. getTaskStackSize(): number {
  418. return this.clineStack.length
  419. }
  420. public getCurrentTaskStack(): string[] {
  421. return this.clineStack.map((cline) => cline.taskId)
  422. }
  423. // Pending Edit Operations Management
  424. /**
  425. * Sets a pending edit operation with automatic timeout cleanup
  426. */
  427. public setPendingEditOperation(
  428. operationId: string,
  429. editData: {
  430. messageTs: number
  431. editedContent: string
  432. images?: string[]
  433. messageIndex: number
  434. apiConversationHistoryIndex: number
  435. },
  436. ): void {
  437. // Clear any existing operation with the same ID
  438. this.clearPendingEditOperation(operationId)
  439. // Create timeout for automatic cleanup
  440. const timeoutId = setTimeout(() => {
  441. this.clearPendingEditOperation(operationId)
  442. this.log(`[setPendingEditOperation] Automatically cleared stale pending operation: ${operationId}`)
  443. }, ClineProvider.PENDING_OPERATION_TIMEOUT_MS)
  444. // Store the operation
  445. this.pendingOperations.set(operationId, {
  446. ...editData,
  447. timeoutId,
  448. createdAt: Date.now(),
  449. })
  450. this.log(`[setPendingEditOperation] Set pending operation: ${operationId}`)
  451. }
  452. /**
  453. * Gets a pending edit operation by ID
  454. */
  455. private getPendingEditOperation(operationId: string): PendingEditOperation | undefined {
  456. return this.pendingOperations.get(operationId)
  457. }
  458. /**
  459. * Clears a specific pending edit operation
  460. */
  461. private clearPendingEditOperation(operationId: string): boolean {
  462. const operation = this.pendingOperations.get(operationId)
  463. if (operation) {
  464. clearTimeout(operation.timeoutId)
  465. this.pendingOperations.delete(operationId)
  466. this.log(`[clearPendingEditOperation] Cleared pending operation: ${operationId}`)
  467. return true
  468. }
  469. return false
  470. }
  471. /**
  472. * Clears all pending edit operations
  473. */
  474. private clearAllPendingEditOperations(): void {
  475. for (const [operationId, operation] of this.pendingOperations) {
  476. clearTimeout(operation.timeoutId)
  477. }
  478. this.pendingOperations.clear()
  479. this.log(`[clearAllPendingEditOperations] Cleared all pending operations`)
  480. }
  481. /*
  482. 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.
  483. - https://vscode-docs.readthedocs.io/en/stable/extensions/patterns-and-principles/
  484. - https://github.com/microsoft/vscode-extension-samples/blob/main/webview-sample/src/extension.ts
  485. */
  486. private clearWebviewResources() {
  487. while (this.webviewDisposables.length) {
  488. const x = this.webviewDisposables.pop()
  489. if (x) {
  490. x.dispose()
  491. }
  492. }
  493. }
  494. async dispose() {
  495. this.log("Disposing ClineProvider...")
  496. // Clear all tasks from the stack.
  497. while (this.clineStack.length > 0) {
  498. await this.removeClineFromStack()
  499. }
  500. this.log("Cleared all tasks")
  501. // Clear all pending edit operations to prevent memory leaks
  502. this.clearAllPendingEditOperations()
  503. this.log("Cleared pending operations")
  504. if (this.view && "dispose" in this.view) {
  505. this.view.dispose()
  506. this.log("Disposed webview")
  507. }
  508. this.clearWebviewResources()
  509. // Clean up cloud service event listener
  510. if (CloudService.hasInstance()) {
  511. CloudService.instance.off("settings-updated", this.handleCloudSettingsUpdate)
  512. }
  513. while (this.disposables.length) {
  514. const x = this.disposables.pop()
  515. if (x) {
  516. x.dispose()
  517. }
  518. }
  519. this._workspaceTracker?.dispose()
  520. this._workspaceTracker = undefined
  521. await this.mcpHub?.unregisterClient()
  522. this.mcpHub = undefined
  523. this.marketplaceManager?.cleanup()
  524. this.customModesManager?.dispose()
  525. this.log("Disposed all disposables")
  526. ClineProvider.activeInstances.delete(this)
  527. // Clean up any event listeners attached to this provider
  528. this.removeAllListeners()
  529. McpServerManager.unregisterProvider(this)
  530. }
  531. public static getVisibleInstance(): ClineProvider | undefined {
  532. return findLast(Array.from(this.activeInstances), (instance) => instance.view?.visible === true)
  533. }
  534. public static async getInstance(): Promise<ClineProvider | undefined> {
  535. let visibleProvider = ClineProvider.getVisibleInstance()
  536. // If no visible provider, try to show the sidebar view
  537. if (!visibleProvider) {
  538. await vscode.commands.executeCommand(`${Package.name}.SidebarProvider.focus`)
  539. // Wait briefly for the view to become visible
  540. await delay(100)
  541. visibleProvider = ClineProvider.getVisibleInstance()
  542. }
  543. // If still no visible provider, return
  544. if (!visibleProvider) {
  545. return
  546. }
  547. return visibleProvider
  548. }
  549. public static async isActiveTask(): Promise<boolean> {
  550. const visibleProvider = await ClineProvider.getInstance()
  551. if (!visibleProvider) {
  552. return false
  553. }
  554. // Check if there is a cline instance in the stack (if this provider has an active task)
  555. if (visibleProvider.getCurrentTask()) {
  556. return true
  557. }
  558. return false
  559. }
  560. public static async handleCodeAction(
  561. command: CodeActionId,
  562. promptType: CodeActionName,
  563. params: Record<string, string | any[]>,
  564. ): Promise<void> {
  565. // Capture telemetry for code action usage
  566. TelemetryService.instance.captureCodeActionUsed(promptType)
  567. const visibleProvider = await ClineProvider.getInstance()
  568. if (!visibleProvider) {
  569. return
  570. }
  571. const { customSupportPrompts } = await visibleProvider.getState()
  572. // TODO: Improve type safety for promptType.
  573. const prompt = supportPrompt.create(promptType, params, customSupportPrompts)
  574. if (command === "addToContext") {
  575. await visibleProvider.postMessageToWebview({
  576. type: "invoke",
  577. invoke: "setChatBoxMessage",
  578. text: `${prompt}\n\n`,
  579. })
  580. await visibleProvider.postMessageToWebview({ type: "action", action: "focusInput" })
  581. return
  582. }
  583. await visibleProvider.createTask(prompt)
  584. }
  585. public static async handleTerminalAction(
  586. command: TerminalActionId,
  587. promptType: TerminalActionPromptType,
  588. params: Record<string, string | any[]>,
  589. ): Promise<void> {
  590. TelemetryService.instance.captureCodeActionUsed(promptType)
  591. const visibleProvider = await ClineProvider.getInstance()
  592. if (!visibleProvider) {
  593. return
  594. }
  595. const { customSupportPrompts } = await visibleProvider.getState()
  596. const prompt = supportPrompt.create(promptType, params, customSupportPrompts)
  597. if (command === "terminalAddToContext") {
  598. await visibleProvider.postMessageToWebview({
  599. type: "invoke",
  600. invoke: "setChatBoxMessage",
  601. text: `${prompt}\n\n`,
  602. })
  603. await visibleProvider.postMessageToWebview({ type: "action", action: "focusInput" })
  604. return
  605. }
  606. try {
  607. await visibleProvider.createTask(prompt)
  608. } catch (error) {
  609. if (error instanceof OrganizationAllowListViolationError) {
  610. // Errors from terminal commands seem to get swallowed / ignored.
  611. vscode.window.showErrorMessage(error.message)
  612. }
  613. throw error
  614. }
  615. }
  616. async resolveWebviewView(webviewView: vscode.WebviewView | vscode.WebviewPanel) {
  617. this.view = webviewView
  618. const inTabMode = "onDidChangeViewState" in webviewView
  619. if (inTabMode) {
  620. setPanel(webviewView, "tab")
  621. } else if ("onDidChangeVisibility" in webviewView) {
  622. setPanel(webviewView, "sidebar")
  623. }
  624. // Initialize out-of-scope variables that need to receive persistent
  625. // global state values.
  626. this.getState().then(
  627. ({
  628. terminalShellIntegrationTimeout = Terminal.defaultShellIntegrationTimeout,
  629. terminalShellIntegrationDisabled = false,
  630. terminalCommandDelay = 0,
  631. terminalZshClearEolMark = true,
  632. terminalZshOhMy = false,
  633. terminalZshP10k = false,
  634. terminalPowershellCounter = false,
  635. terminalZdotdir = false,
  636. }) => {
  637. Terminal.setShellIntegrationTimeout(terminalShellIntegrationTimeout)
  638. Terminal.setShellIntegrationDisabled(terminalShellIntegrationDisabled)
  639. Terminal.setCommandDelay(terminalCommandDelay)
  640. Terminal.setTerminalZshClearEolMark(terminalZshClearEolMark)
  641. Terminal.setTerminalZshOhMy(terminalZshOhMy)
  642. Terminal.setTerminalZshP10k(terminalZshP10k)
  643. Terminal.setPowershellCounter(terminalPowershellCounter)
  644. Terminal.setTerminalZdotdir(terminalZdotdir)
  645. },
  646. )
  647. this.getState().then(({ ttsEnabled }) => {
  648. setTtsEnabled(ttsEnabled ?? false)
  649. })
  650. this.getState().then(({ ttsSpeed }) => {
  651. setTtsSpeed(ttsSpeed ?? 1)
  652. })
  653. // Set up webview options with proper resource roots
  654. const resourceRoots = [this.contextProxy.extensionUri]
  655. // Add workspace folders to allow access to workspace files
  656. if (vscode.workspace.workspaceFolders) {
  657. resourceRoots.push(...vscode.workspace.workspaceFolders.map((folder) => folder.uri))
  658. }
  659. webviewView.webview.options = {
  660. enableScripts: true,
  661. localResourceRoots: resourceRoots,
  662. }
  663. webviewView.webview.html =
  664. this.contextProxy.extensionMode === vscode.ExtensionMode.Development
  665. ? await this.getHMRHtmlContent(webviewView.webview)
  666. : await this.getHtmlContent(webviewView.webview)
  667. // Sets up an event listener to listen for messages passed from the webview view context
  668. // and executes code based on the message that is received.
  669. this.setWebviewMessageListener(webviewView.webview)
  670. // Initialize code index status subscription for the current workspace.
  671. this.updateCodeIndexStatusSubscription()
  672. // Listen for active editor changes to update code index status for the
  673. // current workspace.
  674. const activeEditorSubscription = vscode.window.onDidChangeActiveTextEditor(() => {
  675. // Update subscription when workspace might have changed.
  676. this.updateCodeIndexStatusSubscription()
  677. })
  678. this.webviewDisposables.push(activeEditorSubscription)
  679. // Listen for when the panel becomes visible.
  680. // https://github.com/microsoft/vscode-discussions/discussions/840
  681. if ("onDidChangeViewState" in webviewView) {
  682. // WebviewView and WebviewPanel have all the same properties except
  683. // for this visibility listener panel.
  684. const viewStateDisposable = webviewView.onDidChangeViewState(() => {
  685. if (this.view?.visible) {
  686. this.postMessageToWebview({ type: "action", action: "didBecomeVisible" })
  687. }
  688. })
  689. this.webviewDisposables.push(viewStateDisposable)
  690. } else if ("onDidChangeVisibility" in webviewView) {
  691. // sidebar
  692. const visibilityDisposable = webviewView.onDidChangeVisibility(() => {
  693. if (this.view?.visible) {
  694. this.postMessageToWebview({ type: "action", action: "didBecomeVisible" })
  695. }
  696. })
  697. this.webviewDisposables.push(visibilityDisposable)
  698. }
  699. // Listen for when the view is disposed
  700. // This happens when the user closes the view or when the view is closed programmatically
  701. webviewView.onDidDispose(
  702. async () => {
  703. if (inTabMode) {
  704. this.log("Disposing ClineProvider instance for tab view")
  705. await this.dispose()
  706. } else {
  707. this.log("Clearing webview resources for sidebar view")
  708. this.clearWebviewResources()
  709. // Reset current workspace manager reference when view is disposed
  710. this.codeIndexManager = undefined
  711. }
  712. },
  713. null,
  714. this.disposables,
  715. )
  716. // Listen for when color changes
  717. const configDisposable = vscode.workspace.onDidChangeConfiguration(async (e) => {
  718. if (e && e.affectsConfiguration("workbench.colorTheme")) {
  719. // Sends latest theme name to webview
  720. await this.postMessageToWebview({ type: "theme", text: JSON.stringify(await getTheme()) })
  721. }
  722. })
  723. this.webviewDisposables.push(configDisposable)
  724. // If the extension is starting a new session, clear previous task state.
  725. await this.removeClineFromStack()
  726. }
  727. public async createTaskWithHistoryItem(
  728. historyItem: HistoryItem & { rootTask?: Task; parentTask?: Task },
  729. options?: { startTask?: boolean },
  730. ) {
  731. // Check if we're rehydrating the current task to avoid flicker
  732. const currentTask = this.getCurrentTask()
  733. const isRehydratingCurrentTask = currentTask && currentTask.taskId === historyItem.id
  734. if (!isRehydratingCurrentTask) {
  735. await this.removeClineFromStack()
  736. }
  737. // If the history item has a saved mode, restore it and its associated API configuration.
  738. if (historyItem.mode) {
  739. // Validate that the mode still exists
  740. const customModes = await this.customModesManager.getCustomModes()
  741. const modeExists = getModeBySlug(historyItem.mode, customModes) !== undefined
  742. if (!modeExists) {
  743. // Mode no longer exists, fall back to default mode.
  744. this.log(
  745. `Mode '${historyItem.mode}' from history no longer exists. Falling back to default mode '${defaultModeSlug}'.`,
  746. )
  747. historyItem.mode = defaultModeSlug
  748. }
  749. await this.updateGlobalState("mode", historyItem.mode)
  750. // Load the saved API config for the restored mode if it exists.
  751. const savedConfigId = await this.providerSettingsManager.getModeConfigId(historyItem.mode)
  752. const listApiConfig = await this.providerSettingsManager.listConfig()
  753. // Update listApiConfigMeta first to ensure UI has latest data.
  754. await this.updateGlobalState("listApiConfigMeta", listApiConfig)
  755. // If this mode has a saved config, use it.
  756. if (savedConfigId) {
  757. const profile = listApiConfig.find(({ id }) => id === savedConfigId)
  758. if (profile?.name) {
  759. try {
  760. await this.activateProviderProfile({ name: profile.name })
  761. } catch (error) {
  762. // Log the error but continue with task restoration.
  763. this.log(
  764. `Failed to restore API configuration for mode '${historyItem.mode}': ${
  765. error instanceof Error ? error.message : String(error)
  766. }. Continuing with default configuration.`,
  767. )
  768. // The task will continue with the current/default configuration.
  769. }
  770. }
  771. }
  772. }
  773. const {
  774. apiConfiguration,
  775. diffEnabled: enableDiff,
  776. enableCheckpoints,
  777. checkpointTimeout,
  778. fuzzyMatchThreshold,
  779. experiments,
  780. cloudUserInfo,
  781. taskSyncEnabled,
  782. } = await this.getState()
  783. const task = new Task({
  784. provider: this,
  785. apiConfiguration,
  786. enableDiff,
  787. enableCheckpoints,
  788. checkpointTimeout,
  789. fuzzyMatchThreshold,
  790. consecutiveMistakeLimit: apiConfiguration.consecutiveMistakeLimit,
  791. historyItem,
  792. experiments,
  793. rootTask: historyItem.rootTask,
  794. parentTask: historyItem.parentTask,
  795. taskNumber: historyItem.number,
  796. workspacePath: historyItem.workspace,
  797. onCreated: this.taskCreationCallback,
  798. startTask: options?.startTask ?? true,
  799. enableBridge: BridgeOrchestrator.isEnabled(cloudUserInfo, taskSyncEnabled),
  800. // Preserve the status from the history item to avoid overwriting it when the task saves messages
  801. initialStatus: historyItem.status,
  802. })
  803. if (isRehydratingCurrentTask) {
  804. // Replace the current task in-place to avoid UI flicker
  805. const stackIndex = this.clineStack.length - 1
  806. // Properly dispose of the old task to ensure garbage collection
  807. const oldTask = this.clineStack[stackIndex]
  808. // Abort the old task to stop running processes and mark as abandoned
  809. try {
  810. await oldTask.abortTask(true)
  811. } catch (e) {
  812. this.log(
  813. `[createTaskWithHistoryItem] abortTask() failed for old task ${oldTask.taskId}.${oldTask.instanceId}: ${e.message}`,
  814. )
  815. }
  816. // Remove event listeners from the old task
  817. const cleanupFunctions = this.taskEventListeners.get(oldTask)
  818. if (cleanupFunctions) {
  819. cleanupFunctions.forEach((cleanup) => cleanup())
  820. this.taskEventListeners.delete(oldTask)
  821. }
  822. // Replace the task in the stack
  823. this.clineStack[stackIndex] = task
  824. task.emit(RooCodeEventName.TaskFocused)
  825. // Perform preparation tasks and set up event listeners
  826. await this.performPreparationTasks(task)
  827. this.log(
  828. `[createTaskWithHistoryItem] rehydrated task ${task.taskId}.${task.instanceId} in-place (flicker-free)`,
  829. )
  830. } else {
  831. await this.addClineToStack(task)
  832. this.log(
  833. `[createTaskWithHistoryItem] ${task.parentTask ? "child" : "parent"} task ${task.taskId}.${task.instanceId} instantiated`,
  834. )
  835. }
  836. // Check if there's a pending edit after checkpoint restoration
  837. const operationId = `task-${task.taskId}`
  838. const pendingEdit = this.getPendingEditOperation(operationId)
  839. if (pendingEdit) {
  840. this.clearPendingEditOperation(operationId) // Clear the pending edit
  841. this.log(`[createTaskWithHistoryItem] Processing pending edit after checkpoint restoration`)
  842. // Process the pending edit after a short delay to ensure the task is fully initialized
  843. setTimeout(async () => {
  844. try {
  845. // Find the message index in the restored state
  846. const { messageIndex, apiConversationHistoryIndex } = (() => {
  847. const messageIndex = task.clineMessages.findIndex((msg) => msg.ts === pendingEdit.messageTs)
  848. const apiConversationHistoryIndex = task.apiConversationHistory.findIndex(
  849. (msg) => msg.ts === pendingEdit.messageTs,
  850. )
  851. return { messageIndex, apiConversationHistoryIndex }
  852. })()
  853. if (messageIndex !== -1) {
  854. // Remove the target message and all subsequent messages
  855. await task.overwriteClineMessages(task.clineMessages.slice(0, messageIndex))
  856. if (apiConversationHistoryIndex !== -1) {
  857. await task.overwriteApiConversationHistory(
  858. task.apiConversationHistory.slice(0, apiConversationHistoryIndex),
  859. )
  860. }
  861. // Process the edited message
  862. await task.handleWebviewAskResponse(
  863. "messageResponse",
  864. pendingEdit.editedContent,
  865. pendingEdit.images,
  866. )
  867. }
  868. } catch (error) {
  869. this.log(`[createTaskWithHistoryItem] Error processing pending edit: ${error}`)
  870. }
  871. }, 100) // Small delay to ensure task is fully ready
  872. }
  873. return task
  874. }
  875. public async postMessageToWebview(message: ExtensionMessage) {
  876. await this.view?.webview.postMessage(message)
  877. }
  878. private async getHMRHtmlContent(webview: vscode.Webview): Promise<string> {
  879. let localPort = "5173"
  880. try {
  881. const fs = require("fs")
  882. const path = require("path")
  883. const portFilePath = path.resolve(__dirname, "../../.vite-port")
  884. if (fs.existsSync(portFilePath)) {
  885. localPort = fs.readFileSync(portFilePath, "utf8").trim()
  886. console.log(`[ClineProvider:Vite] Using Vite server port from ${portFilePath}: ${localPort}`)
  887. } else {
  888. console.log(
  889. `[ClineProvider:Vite] Port file not found at ${portFilePath}, using default port: ${localPort}`,
  890. )
  891. }
  892. } catch (err) {
  893. console.error("[ClineProvider:Vite] Failed to read Vite port file:", err)
  894. }
  895. const localServerUrl = `localhost:${localPort}`
  896. // Check if local dev server is running.
  897. try {
  898. await axios.get(`http://${localServerUrl}`)
  899. } catch (error) {
  900. vscode.window.showErrorMessage(t("common:errors.hmr_not_running"))
  901. return this.getHtmlContent(webview)
  902. }
  903. const nonce = getNonce()
  904. // Get the OpenRouter base URL from configuration
  905. const { apiConfiguration } = await this.getState()
  906. const openRouterBaseUrl = apiConfiguration.openRouterBaseUrl || "https://openrouter.ai"
  907. // Extract the domain for CSP
  908. const openRouterDomain = openRouterBaseUrl.match(/^(https?:\/\/[^\/]+)/)?.[1] || "https://openrouter.ai"
  909. const stylesUri = getUri(webview, this.contextProxy.extensionUri, [
  910. "webview-ui",
  911. "build",
  912. "assets",
  913. "index.css",
  914. ])
  915. const codiconsUri = getUri(webview, this.contextProxy.extensionUri, ["assets", "codicons", "codicon.css"])
  916. const materialIconsUri = getUri(webview, this.contextProxy.extensionUri, [
  917. "assets",
  918. "vscode-material-icons",
  919. "icons",
  920. ])
  921. const imagesUri = getUri(webview, this.contextProxy.extensionUri, ["assets", "images"])
  922. const audioUri = getUri(webview, this.contextProxy.extensionUri, ["webview-ui", "audio"])
  923. const file = "src/index.tsx"
  924. const scriptUri = `http://${localServerUrl}/${file}`
  925. const reactRefresh = /*html*/ `
  926. <script nonce="${nonce}" type="module">
  927. import RefreshRuntime from "http://localhost:${localPort}/@react-refresh"
  928. RefreshRuntime.injectIntoGlobalHook(window)
  929. window.$RefreshReg$ = () => {}
  930. window.$RefreshSig$ = () => (type) => type
  931. window.__vite_plugin_react_preamble_installed__ = true
  932. </script>
  933. `
  934. const csp = [
  935. "default-src 'none'",
  936. `font-src ${webview.cspSource} data:`,
  937. `style-src ${webview.cspSource} 'unsafe-inline' https://* http://${localServerUrl} http://0.0.0.0:${localPort}`,
  938. `img-src ${webview.cspSource} https://storage.googleapis.com https://img.clerk.com data:`,
  939. `media-src ${webview.cspSource}`,
  940. `script-src 'unsafe-eval' ${webview.cspSource} https://* https://*.posthog.com http://${localServerUrl} http://0.0.0.0:${localPort} 'nonce-${nonce}'`,
  941. `connect-src ${webview.cspSource} ${openRouterDomain} https://* https://*.posthog.com ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`,
  942. ]
  943. return /*html*/ `
  944. <!DOCTYPE html>
  945. <html lang="en">
  946. <head>
  947. <meta charset="utf-8">
  948. <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
  949. <meta http-equiv="Content-Security-Policy" content="${csp.join("; ")}">
  950. <link rel="stylesheet" type="text/css" href="${stylesUri}">
  951. <link href="${codiconsUri}" rel="stylesheet" />
  952. <script nonce="${nonce}">
  953. window.IMAGES_BASE_URI = "${imagesUri}"
  954. window.AUDIO_BASE_URI = "${audioUri}"
  955. window.MATERIAL_ICONS_BASE_URI = "${materialIconsUri}"
  956. </script>
  957. <title>Roo Code</title>
  958. </head>
  959. <body>
  960. <div id="root"></div>
  961. ${reactRefresh}
  962. <script type="module" src="${scriptUri}"></script>
  963. </body>
  964. </html>
  965. `
  966. }
  967. /**
  968. * Defines and returns the HTML that should be rendered within the webview panel.
  969. *
  970. * @remarks This is also the place where references to the React webview build files
  971. * are created and inserted into the webview HTML.
  972. *
  973. * @param webview A reference to the extension webview
  974. * @param extensionUri The URI of the directory containing the extension
  975. * @returns A template string literal containing the HTML that should be
  976. * rendered within the webview panel
  977. */
  978. private async getHtmlContent(webview: vscode.Webview): Promise<string> {
  979. // Get the local path to main script run in the webview,
  980. // then convert it to a uri we can use in the webview.
  981. // The CSS file from the React build output
  982. const stylesUri = getUri(webview, this.contextProxy.extensionUri, [
  983. "webview-ui",
  984. "build",
  985. "assets",
  986. "index.css",
  987. ])
  988. const scriptUri = getUri(webview, this.contextProxy.extensionUri, ["webview-ui", "build", "assets", "index.js"])
  989. const codiconsUri = getUri(webview, this.contextProxy.extensionUri, ["assets", "codicons", "codicon.css"])
  990. const materialIconsUri = getUri(webview, this.contextProxy.extensionUri, [
  991. "assets",
  992. "vscode-material-icons",
  993. "icons",
  994. ])
  995. const imagesUri = getUri(webview, this.contextProxy.extensionUri, ["assets", "images"])
  996. const audioUri = getUri(webview, this.contextProxy.extensionUri, ["webview-ui", "audio"])
  997. // Use a nonce to only allow a specific script to be run.
  998. /*
  999. content security policy of your webview to only allow scripts that have a specific nonce
  1000. create a content security policy meta tag so that only loading scripts with a nonce is allowed
  1001. 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 explicitly allow for these resources. E.g.
  1002. <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}';">
  1003. - 'unsafe-inline' is required for styles due to vscode-webview-toolkit's dynamic style injection
  1004. - since we pass base64 images to the webview, we need to specify img-src ${webview.cspSource} data:;
  1005. 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.
  1006. */
  1007. const nonce = getNonce()
  1008. // Get the OpenRouter base URL from configuration
  1009. const { apiConfiguration } = await this.getState()
  1010. const openRouterBaseUrl = apiConfiguration.openRouterBaseUrl || "https://openrouter.ai"
  1011. // Extract the domain for CSP
  1012. const openRouterDomain = openRouterBaseUrl.match(/^(https?:\/\/[^\/]+)/)?.[1] || "https://openrouter.ai"
  1013. // Tip: Install the es6-string-html VS Code extension to enable code highlighting below
  1014. return /*html*/ `
  1015. <!DOCTYPE html>
  1016. <html lang="en">
  1017. <head>
  1018. <meta charset="utf-8">
  1019. <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
  1020. <meta name="theme-color" content="#000000">
  1021. <meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src ${webview.cspSource} data:; 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://ph.roocode.com 'strict-dynamic'; connect-src ${webview.cspSource} ${openRouterDomain} https://api.requesty.ai https://ph.roocode.com;">
  1022. <link rel="stylesheet" type="text/css" href="${stylesUri}">
  1023. <link href="${codiconsUri}" rel="stylesheet" />
  1024. <script nonce="${nonce}">
  1025. window.IMAGES_BASE_URI = "${imagesUri}"
  1026. window.AUDIO_BASE_URI = "${audioUri}"
  1027. window.MATERIAL_ICONS_BASE_URI = "${materialIconsUri}"
  1028. </script>
  1029. <title>Roo Code</title>
  1030. </head>
  1031. <body>
  1032. <noscript>You need to enable JavaScript to run this app.</noscript>
  1033. <div id="root"></div>
  1034. <script nonce="${nonce}" type="module" src="${scriptUri}"></script>
  1035. </body>
  1036. </html>
  1037. `
  1038. }
  1039. /**
  1040. * Sets up an event listener to listen for messages passed from the webview context and
  1041. * executes code based on the message that is received.
  1042. *
  1043. * @param webview A reference to the extension webview
  1044. */
  1045. private setWebviewMessageListener(webview: vscode.Webview) {
  1046. const onReceiveMessage = async (message: WebviewMessage) =>
  1047. webviewMessageHandler(this, message, this.marketplaceManager)
  1048. const messageDisposable = webview.onDidReceiveMessage(onReceiveMessage)
  1049. this.webviewDisposables.push(messageDisposable)
  1050. }
  1051. /**
  1052. * Handle switching to a new mode, including updating the associated API configuration
  1053. * @param newMode The mode to switch to
  1054. */
  1055. public async handleModeSwitch(newMode: Mode) {
  1056. const task = this.getCurrentTask()
  1057. if (task) {
  1058. TelemetryService.instance.captureModeSwitch(task.taskId, newMode)
  1059. task.emit(RooCodeEventName.TaskModeSwitched, task.taskId, newMode)
  1060. try {
  1061. // Update the task history with the new mode first.
  1062. const history = this.getGlobalState("taskHistory") ?? []
  1063. const taskHistoryItem = history.find((item) => item.id === task.taskId)
  1064. if (taskHistoryItem) {
  1065. taskHistoryItem.mode = newMode
  1066. await this.updateTaskHistory(taskHistoryItem)
  1067. }
  1068. // Only update the task's mode after successful persistence.
  1069. ;(task as any)._taskMode = newMode
  1070. } catch (error) {
  1071. // If persistence fails, log the error but don't update the in-memory state.
  1072. this.log(
  1073. `Failed to persist mode switch for task ${task.taskId}: ${error instanceof Error ? error.message : String(error)}`,
  1074. )
  1075. // Optionally, we could emit an event to notify about the failure.
  1076. // This ensures the in-memory state remains consistent with persisted state.
  1077. throw error
  1078. }
  1079. }
  1080. await this.updateGlobalState("mode", newMode)
  1081. this.emit(RooCodeEventName.ModeChanged, newMode)
  1082. // Load the saved API config for the new mode if it exists.
  1083. const savedConfigId = await this.providerSettingsManager.getModeConfigId(newMode)
  1084. const listApiConfig = await this.providerSettingsManager.listConfig()
  1085. // Update listApiConfigMeta first to ensure UI has latest data.
  1086. await this.updateGlobalState("listApiConfigMeta", listApiConfig)
  1087. // If this mode has a saved config, use it.
  1088. if (savedConfigId) {
  1089. const profile = listApiConfig.find(({ id }) => id === savedConfigId)
  1090. if (profile?.name) {
  1091. await this.activateProviderProfile({ name: profile.name })
  1092. }
  1093. } else {
  1094. // If no saved config for this mode, save current config as default.
  1095. const currentApiConfigName = this.getGlobalState("currentApiConfigName")
  1096. if (currentApiConfigName) {
  1097. const config = listApiConfig.find((c) => c.name === currentApiConfigName)
  1098. if (config?.id) {
  1099. await this.providerSettingsManager.setModeConfig(newMode, config.id)
  1100. }
  1101. }
  1102. }
  1103. await this.postStateToWebview()
  1104. }
  1105. // Provider Profile Management
  1106. /**
  1107. * Updates the current task's API handler.
  1108. * Rebuilds when:
  1109. * - provider or model changes, OR
  1110. * - explicitly forced (e.g., user-initiated profile switch/save to apply changed settings like headers/baseUrl/tier).
  1111. * Always synchronizes task.apiConfiguration with latest provider settings.
  1112. * @param providerSettings The new provider settings to apply
  1113. * @param options.forceRebuild Force rebuilding the API handler regardless of provider/model equality
  1114. */
  1115. private updateTaskApiHandlerIfNeeded(
  1116. providerSettings: ProviderSettings,
  1117. options: { forceRebuild?: boolean } = {},
  1118. ): void {
  1119. const task = this.getCurrentTask()
  1120. if (!task) return
  1121. const { forceRebuild = false } = options
  1122. // Determine if we need to rebuild using the previous configuration snapshot
  1123. const prevConfig = task.apiConfiguration
  1124. const prevProvider = prevConfig?.apiProvider
  1125. const prevModelId = prevConfig ? getModelId(prevConfig) : undefined
  1126. const prevToolProtocol = prevConfig?.toolProtocol
  1127. const newProvider = providerSettings.apiProvider
  1128. const newModelId = getModelId(providerSettings)
  1129. const newToolProtocol = providerSettings.toolProtocol
  1130. const needsRebuild =
  1131. forceRebuild ||
  1132. prevProvider !== newProvider ||
  1133. prevModelId !== newModelId ||
  1134. prevToolProtocol !== newToolProtocol
  1135. if (needsRebuild) {
  1136. // Use updateApiConfiguration which handles both API handler rebuild and parser sync.
  1137. // This is important when toolProtocol changes - the assistantMessageParser needs to be
  1138. // created/destroyed to match the new protocol (XML vs native).
  1139. // Note: updateApiConfiguration is declared async but has no actual async operations,
  1140. // so we can safely call it without awaiting.
  1141. task.updateApiConfiguration(providerSettings)
  1142. } else {
  1143. // No rebuild needed, just sync apiConfiguration
  1144. ;(task as any).apiConfiguration = providerSettings
  1145. }
  1146. }
  1147. getProviderProfileEntries(): ProviderSettingsEntry[] {
  1148. return this.contextProxy.getValues().listApiConfigMeta || []
  1149. }
  1150. getProviderProfileEntry(name: string): ProviderSettingsEntry | undefined {
  1151. return this.getProviderProfileEntries().find((profile) => profile.name === name)
  1152. }
  1153. public hasProviderProfileEntry(name: string): boolean {
  1154. return !!this.getProviderProfileEntry(name)
  1155. }
  1156. async upsertProviderProfile(
  1157. name: string,
  1158. providerSettings: ProviderSettings,
  1159. activate: boolean = true,
  1160. ): Promise<string | undefined> {
  1161. try {
  1162. // TODO: Do we need to be calling `activateProfile`? It's not
  1163. // clear to me what the source of truth should be; in some cases
  1164. // we rely on the `ContextProxy`'s data store and in other cases
  1165. // we rely on the `ProviderSettingsManager`'s data store. It might
  1166. // be simpler to unify these two.
  1167. const id = await this.providerSettingsManager.saveConfig(name, providerSettings)
  1168. if (activate) {
  1169. const { mode } = await this.getState()
  1170. // These promises do the following:
  1171. // 1. Adds or updates the list of provider profiles.
  1172. // 2. Sets the current provider profile.
  1173. // 3. Sets the current mode's provider profile.
  1174. // 4. Copies the provider settings to the context.
  1175. //
  1176. // Note: 1, 2, and 4 can be done in one `ContextProxy` call:
  1177. // this.contextProxy.setValues({ ...providerSettings, listApiConfigMeta: ..., currentApiConfigName: ... })
  1178. // We should probably switch to that and verify that it works.
  1179. // I left the original implementation in just to be safe.
  1180. await Promise.all([
  1181. this.updateGlobalState("listApiConfigMeta", await this.providerSettingsManager.listConfig()),
  1182. this.updateGlobalState("currentApiConfigName", name),
  1183. this.providerSettingsManager.setModeConfig(mode, id),
  1184. this.contextProxy.setProviderSettings(providerSettings),
  1185. ])
  1186. // Change the provider for the current task.
  1187. // TODO: We should rename `buildApiHandler` for clarity (e.g. `getProviderClient`).
  1188. this.updateTaskApiHandlerIfNeeded(providerSettings, { forceRebuild: true })
  1189. } else {
  1190. await this.updateGlobalState("listApiConfigMeta", await this.providerSettingsManager.listConfig())
  1191. }
  1192. await this.postStateToWebview()
  1193. return id
  1194. } catch (error) {
  1195. this.log(
  1196. `Error create new api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1197. )
  1198. vscode.window.showErrorMessage(t("common:errors.create_api_config"))
  1199. return undefined
  1200. }
  1201. }
  1202. async deleteProviderProfile(profileToDelete: ProviderSettingsEntry) {
  1203. const globalSettings = this.contextProxy.getValues()
  1204. let profileToActivate: string | undefined = globalSettings.currentApiConfigName
  1205. if (profileToDelete.name === profileToActivate) {
  1206. profileToActivate = this.getProviderProfileEntries().find(({ name }) => name !== profileToDelete.name)?.name
  1207. }
  1208. if (!profileToActivate) {
  1209. throw new Error("You cannot delete the last profile")
  1210. }
  1211. const entries = this.getProviderProfileEntries().filter(({ name }) => name !== profileToDelete.name)
  1212. await this.contextProxy.setValues({
  1213. ...globalSettings,
  1214. currentApiConfigName: profileToActivate,
  1215. listApiConfigMeta: entries,
  1216. })
  1217. await this.postStateToWebview()
  1218. }
  1219. async activateProviderProfile(args: { name: string } | { id: string }) {
  1220. const { name, id, ...providerSettings } = await this.providerSettingsManager.activateProfile(args)
  1221. // See `upsertProviderProfile` for a description of what this is doing.
  1222. await Promise.all([
  1223. this.contextProxy.setValue("listApiConfigMeta", await this.providerSettingsManager.listConfig()),
  1224. this.contextProxy.setValue("currentApiConfigName", name),
  1225. this.contextProxy.setProviderSettings(providerSettings),
  1226. ])
  1227. const { mode } = await this.getState()
  1228. if (id) {
  1229. await this.providerSettingsManager.setModeConfig(mode, id)
  1230. }
  1231. // Change the provider for the current task.
  1232. this.updateTaskApiHandlerIfNeeded(providerSettings, { forceRebuild: true })
  1233. await this.postStateToWebview()
  1234. if (providerSettings.apiProvider) {
  1235. this.emit(RooCodeEventName.ProviderProfileChanged, { name, provider: providerSettings.apiProvider })
  1236. }
  1237. }
  1238. async updateCustomInstructions(instructions?: string) {
  1239. // User may be clearing the field.
  1240. await this.updateGlobalState("customInstructions", instructions || undefined)
  1241. await this.postStateToWebview()
  1242. }
  1243. // MCP
  1244. async ensureMcpServersDirectoryExists(): Promise<string> {
  1245. // Get platform-specific application data directory
  1246. let mcpServersDir: string
  1247. if (process.platform === "win32") {
  1248. // Windows: %APPDATA%\Roo-Code\MCP
  1249. mcpServersDir = path.join(os.homedir(), "AppData", "Roaming", "Roo-Code", "MCP")
  1250. } else if (process.platform === "darwin") {
  1251. // macOS: ~/Documents/Cline/MCP
  1252. mcpServersDir = path.join(os.homedir(), "Documents", "Cline", "MCP")
  1253. } else {
  1254. // Linux: ~/.local/share/Cline/MCP
  1255. mcpServersDir = path.join(os.homedir(), ".local", "share", "Roo-Code", "MCP")
  1256. }
  1257. try {
  1258. await fs.mkdir(mcpServersDir, { recursive: true })
  1259. } catch (error) {
  1260. // Fallback to a relative path if directory creation fails
  1261. return path.join(os.homedir(), ".roo-code", "mcp")
  1262. }
  1263. return mcpServersDir
  1264. }
  1265. async ensureSettingsDirectoryExists(): Promise<string> {
  1266. const { getSettingsDirectoryPath } = await import("../../utils/storage")
  1267. const globalStoragePath = this.contextProxy.globalStorageUri.fsPath
  1268. return getSettingsDirectoryPath(globalStoragePath)
  1269. }
  1270. // OpenRouter
  1271. async handleOpenRouterCallback(code: string) {
  1272. let { apiConfiguration, currentApiConfigName = "default" } = await this.getState()
  1273. let apiKey: string
  1274. try {
  1275. const baseUrl = apiConfiguration.openRouterBaseUrl || "https://openrouter.ai/api/v1"
  1276. // Extract the base domain for the auth endpoint.
  1277. const baseUrlDomain = baseUrl.match(/^(https?:\/\/[^\/]+)/)?.[1] || "https://openrouter.ai"
  1278. const response = await axios.post(`${baseUrlDomain}/api/v1/auth/keys`, { code })
  1279. if (response.data && response.data.key) {
  1280. apiKey = response.data.key
  1281. } else {
  1282. throw new Error("Invalid response from OpenRouter API")
  1283. }
  1284. } catch (error) {
  1285. this.log(
  1286. `Error exchanging code for API key: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1287. )
  1288. throw error
  1289. }
  1290. const newConfiguration: ProviderSettings = {
  1291. ...apiConfiguration,
  1292. apiProvider: "openrouter",
  1293. openRouterApiKey: apiKey,
  1294. openRouterModelId: apiConfiguration?.openRouterModelId || openRouterDefaultModelId,
  1295. }
  1296. await this.upsertProviderProfile(currentApiConfigName, newConfiguration)
  1297. }
  1298. // Requesty
  1299. async handleRequestyCallback(code: string, baseUrl: string | null) {
  1300. let { apiConfiguration } = await this.getState()
  1301. const newConfiguration: ProviderSettings = {
  1302. ...apiConfiguration,
  1303. apiProvider: "requesty",
  1304. requestyApiKey: code,
  1305. requestyModelId: apiConfiguration?.requestyModelId || requestyDefaultModelId,
  1306. }
  1307. // set baseUrl as undefined if we don't provide one
  1308. // or if it is the default requesty url
  1309. if (!baseUrl || baseUrl === REQUESTY_BASE_URL) {
  1310. newConfiguration.requestyBaseUrl = undefined
  1311. } else {
  1312. newConfiguration.requestyBaseUrl = baseUrl
  1313. }
  1314. const profileName = `Requesty (${new Date().toLocaleString()})`
  1315. await this.upsertProviderProfile(profileName, newConfiguration)
  1316. }
  1317. // Task history
  1318. async getTaskWithId(id: string): Promise<{
  1319. historyItem: HistoryItem
  1320. taskDirPath: string
  1321. apiConversationHistoryFilePath: string
  1322. uiMessagesFilePath: string
  1323. apiConversationHistory: Anthropic.MessageParam[]
  1324. }> {
  1325. const history = this.getGlobalState("taskHistory") ?? []
  1326. const historyItem = history.find((item) => item.id === id)
  1327. if (historyItem) {
  1328. const { getTaskDirectoryPath } = await import("../../utils/storage")
  1329. const globalStoragePath = this.contextProxy.globalStorageUri.fsPath
  1330. const taskDirPath = await getTaskDirectoryPath(globalStoragePath, id)
  1331. const apiConversationHistoryFilePath = path.join(taskDirPath, GlobalFileNames.apiConversationHistory)
  1332. const uiMessagesFilePath = path.join(taskDirPath, GlobalFileNames.uiMessages)
  1333. const fileExists = await fileExistsAtPath(apiConversationHistoryFilePath)
  1334. if (fileExists) {
  1335. const apiConversationHistory = JSON.parse(await fs.readFile(apiConversationHistoryFilePath, "utf8"))
  1336. return {
  1337. historyItem,
  1338. taskDirPath,
  1339. apiConversationHistoryFilePath,
  1340. uiMessagesFilePath,
  1341. apiConversationHistory,
  1342. }
  1343. }
  1344. }
  1345. // if we tried to get a task that doesn't exist, remove it from state
  1346. // FIXME: this seems to happen sometimes when the json file doesnt save to disk for some reason
  1347. await this.deleteTaskFromState(id)
  1348. throw new Error("Task not found")
  1349. }
  1350. async showTaskWithId(id: string) {
  1351. if (id !== this.getCurrentTask()?.taskId) {
  1352. // Non-current task.
  1353. const { historyItem } = await this.getTaskWithId(id)
  1354. await this.createTaskWithHistoryItem(historyItem) // Clears existing task.
  1355. }
  1356. await this.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
  1357. }
  1358. async exportTaskWithId(id: string) {
  1359. const { historyItem, apiConversationHistory } = await this.getTaskWithId(id)
  1360. await downloadTask(historyItem.ts, apiConversationHistory)
  1361. }
  1362. /* Condenses a task's message history to use fewer tokens. */
  1363. async condenseTaskContext(taskId: string) {
  1364. let task: Task | undefined
  1365. for (let i = this.clineStack.length - 1; i >= 0; i--) {
  1366. if (this.clineStack[i].taskId === taskId) {
  1367. task = this.clineStack[i]
  1368. break
  1369. }
  1370. }
  1371. if (!task) {
  1372. throw new Error(`Task with id ${taskId} not found in stack`)
  1373. }
  1374. await task.condenseContext()
  1375. await this.postMessageToWebview({ type: "condenseTaskContextResponse", text: taskId })
  1376. }
  1377. // this function deletes a task from task hidtory, and deletes it's checkpoints and delete the task folder
  1378. async deleteTaskWithId(id: string) {
  1379. try {
  1380. // get the task directory full path
  1381. const { taskDirPath } = await this.getTaskWithId(id)
  1382. // remove task from stack if it's the current task
  1383. if (id === this.getCurrentTask()?.taskId) {
  1384. // Close the current task instance; delegation flows will be handled via metadata if applicable.
  1385. await this.removeClineFromStack()
  1386. }
  1387. // delete task from the task history state
  1388. await this.deleteTaskFromState(id)
  1389. // Delete associated shadow repository or branch.
  1390. // TODO: Store `workspaceDir` in the `HistoryItem` object.
  1391. const globalStorageDir = this.contextProxy.globalStorageUri.fsPath
  1392. const workspaceDir = this.cwd
  1393. try {
  1394. await ShadowCheckpointService.deleteTask({ taskId: id, globalStorageDir, workspaceDir })
  1395. } catch (error) {
  1396. console.error(
  1397. `[deleteTaskWithId${id}] failed to delete associated shadow repository or branch: ${error instanceof Error ? error.message : String(error)}`,
  1398. )
  1399. }
  1400. // delete the entire task directory including checkpoints and all content
  1401. try {
  1402. await fs.rm(taskDirPath, { recursive: true, force: true })
  1403. console.log(`[deleteTaskWithId${id}] removed task directory`)
  1404. } catch (error) {
  1405. console.error(
  1406. `[deleteTaskWithId${id}] failed to remove task directory: ${error instanceof Error ? error.message : String(error)}`,
  1407. )
  1408. }
  1409. } catch (error) {
  1410. // If task is not found, just remove it from state
  1411. if (error instanceof Error && error.message === "Task not found") {
  1412. await this.deleteTaskFromState(id)
  1413. return
  1414. }
  1415. throw error
  1416. }
  1417. }
  1418. async deleteTaskFromState(id: string) {
  1419. const taskHistory = this.getGlobalState("taskHistory") ?? []
  1420. const updatedTaskHistory = taskHistory.filter((task) => task.id !== id)
  1421. await this.updateGlobalState("taskHistory", updatedTaskHistory)
  1422. this.recentTasksCache = undefined
  1423. await this.postStateToWebview()
  1424. }
  1425. async refreshWorkspace() {
  1426. this.currentWorkspacePath = getWorkspacePath()
  1427. await this.postStateToWebview()
  1428. }
  1429. async postStateToWebview() {
  1430. const state = await this.getStateToPostToWebview()
  1431. this.postMessageToWebview({ type: "state", state })
  1432. // Check MDM compliance and send user to account tab if not compliant
  1433. // Only redirect if there's an actual MDM policy requiring authentication
  1434. if (this.mdmService?.requiresCloudAuth() && !this.checkMdmCompliance()) {
  1435. await this.postMessageToWebview({ type: "action", action: "cloudButtonClicked" })
  1436. }
  1437. }
  1438. /**
  1439. * Fetches marketplace data on demand to avoid blocking main state updates
  1440. */
  1441. async fetchMarketplaceData() {
  1442. try {
  1443. const [marketplaceResult, marketplaceInstalledMetadata] = await Promise.all([
  1444. this.marketplaceManager.getMarketplaceItems().catch((error) => {
  1445. console.error("Failed to fetch marketplace items:", error)
  1446. return { organizationMcps: [], marketplaceItems: [], errors: [error.message] }
  1447. }),
  1448. this.marketplaceManager.getInstallationMetadata().catch((error) => {
  1449. console.error("Failed to fetch installation metadata:", error)
  1450. return { project: {}, global: {} } as MarketplaceInstalledMetadata
  1451. }),
  1452. ])
  1453. // Send marketplace data separately
  1454. this.postMessageToWebview({
  1455. type: "marketplaceData",
  1456. organizationMcps: marketplaceResult.organizationMcps || [],
  1457. marketplaceItems: marketplaceResult.marketplaceItems || [],
  1458. marketplaceInstalledMetadata: marketplaceInstalledMetadata || { project: {}, global: {} },
  1459. errors: marketplaceResult.errors,
  1460. })
  1461. } catch (error) {
  1462. console.error("Failed to fetch marketplace data:", error)
  1463. // Send empty data on error to prevent UI from hanging
  1464. this.postMessageToWebview({
  1465. type: "marketplaceData",
  1466. organizationMcps: [],
  1467. marketplaceItems: [],
  1468. marketplaceInstalledMetadata: { project: {}, global: {} },
  1469. errors: [error instanceof Error ? error.message : String(error)],
  1470. })
  1471. // Show user-friendly error notification for network issues
  1472. if (error instanceof Error && error.message.includes("timeout")) {
  1473. vscode.window.showWarningMessage(
  1474. "Marketplace data could not be loaded due to network restrictions. Core functionality remains available.",
  1475. )
  1476. }
  1477. }
  1478. }
  1479. /**
  1480. * Checks if there is a file-based system prompt override for the given mode
  1481. */
  1482. async hasFileBasedSystemPromptOverride(mode: Mode): Promise<boolean> {
  1483. const promptFilePath = getSystemPromptFilePath(this.cwd, mode)
  1484. return await fileExistsAtPath(promptFilePath)
  1485. }
  1486. /**
  1487. * Merges allowed commands from global state and workspace configuration
  1488. * with proper validation and deduplication
  1489. */
  1490. private mergeAllowedCommands(globalStateCommands?: string[]): string[] {
  1491. return this.mergeCommandLists("allowedCommands", "allowed", globalStateCommands)
  1492. }
  1493. /**
  1494. * Merges denied commands from global state and workspace configuration
  1495. * with proper validation and deduplication
  1496. */
  1497. private mergeDeniedCommands(globalStateCommands?: string[]): string[] {
  1498. return this.mergeCommandLists("deniedCommands", "denied", globalStateCommands)
  1499. }
  1500. /**
  1501. * Common utility for merging command lists from global state and workspace configuration.
  1502. * Implements the Command Denylist feature's merging strategy with proper validation.
  1503. *
  1504. * @param configKey - VSCode workspace configuration key
  1505. * @param commandType - Type of commands for error logging
  1506. * @param globalStateCommands - Commands from global state
  1507. * @returns Merged and deduplicated command list
  1508. */
  1509. private mergeCommandLists(
  1510. configKey: "allowedCommands" | "deniedCommands",
  1511. commandType: "allowed" | "denied",
  1512. globalStateCommands?: string[],
  1513. ): string[] {
  1514. try {
  1515. // Validate and sanitize global state commands
  1516. const validGlobalCommands = Array.isArray(globalStateCommands)
  1517. ? globalStateCommands.filter((cmd) => typeof cmd === "string" && cmd.trim().length > 0)
  1518. : []
  1519. // Get workspace configuration commands
  1520. const workspaceCommands = vscode.workspace.getConfiguration(Package.name).get<string[]>(configKey) || []
  1521. // Validate and sanitize workspace commands
  1522. const validWorkspaceCommands = Array.isArray(workspaceCommands)
  1523. ? workspaceCommands.filter((cmd) => typeof cmd === "string" && cmd.trim().length > 0)
  1524. : []
  1525. // Combine and deduplicate commands
  1526. // Global state takes precedence over workspace configuration
  1527. const mergedCommands = [...new Set([...validGlobalCommands, ...validWorkspaceCommands])]
  1528. return mergedCommands
  1529. } catch (error) {
  1530. console.error(`Error merging ${commandType} commands:`, error)
  1531. // Return empty array as fallback to prevent crashes
  1532. return []
  1533. }
  1534. }
  1535. async getStateToPostToWebview(): Promise<ExtensionState> {
  1536. const {
  1537. apiConfiguration,
  1538. lastShownAnnouncementId,
  1539. customInstructions,
  1540. alwaysAllowReadOnly,
  1541. alwaysAllowReadOnlyOutsideWorkspace,
  1542. alwaysAllowWrite,
  1543. alwaysAllowWriteOutsideWorkspace,
  1544. alwaysAllowWriteProtected,
  1545. alwaysAllowExecute,
  1546. allowedCommands,
  1547. deniedCommands,
  1548. alwaysAllowBrowser,
  1549. alwaysAllowMcp,
  1550. alwaysAllowModeSwitch,
  1551. alwaysAllowSubtasks,
  1552. allowedMaxRequests,
  1553. allowedMaxCost,
  1554. autoCondenseContext,
  1555. autoCondenseContextPercent,
  1556. soundEnabled,
  1557. ttsEnabled,
  1558. ttsSpeed,
  1559. diffEnabled,
  1560. enableCheckpoints,
  1561. checkpointTimeout,
  1562. taskHistory,
  1563. soundVolume,
  1564. browserViewportSize,
  1565. screenshotQuality,
  1566. remoteBrowserHost,
  1567. remoteBrowserEnabled,
  1568. cachedChromeHostUrl,
  1569. writeDelayMs,
  1570. terminalOutputLineLimit,
  1571. terminalOutputCharacterLimit,
  1572. terminalShellIntegrationTimeout,
  1573. terminalShellIntegrationDisabled,
  1574. terminalCommandDelay,
  1575. terminalPowershellCounter,
  1576. terminalZshClearEolMark,
  1577. terminalZshOhMy,
  1578. terminalZshP10k,
  1579. terminalZdotdir,
  1580. fuzzyMatchThreshold,
  1581. mcpEnabled,
  1582. enableMcpServerCreation,
  1583. currentApiConfigName,
  1584. listApiConfigMeta,
  1585. pinnedApiConfigs,
  1586. mode,
  1587. customModePrompts,
  1588. customSupportPrompts,
  1589. enhancementApiConfigId,
  1590. autoApprovalEnabled,
  1591. customModes,
  1592. experiments,
  1593. maxOpenTabsContext,
  1594. maxWorkspaceFiles,
  1595. browserToolEnabled,
  1596. telemetrySetting,
  1597. showRooIgnoredFiles,
  1598. language,
  1599. maxReadFileLine,
  1600. maxImageFileSize,
  1601. maxTotalImageSize,
  1602. terminalCompressProgressBar,
  1603. historyPreviewCollapsed,
  1604. reasoningBlockCollapsed,
  1605. enterBehavior,
  1606. cloudUserInfo,
  1607. cloudIsAuthenticated,
  1608. sharingEnabled,
  1609. organizationAllowList,
  1610. organizationSettingsVersion,
  1611. maxConcurrentFileReads,
  1612. condensingApiConfigId,
  1613. customCondensingPrompt,
  1614. codebaseIndexConfig,
  1615. codebaseIndexModels,
  1616. profileThresholds,
  1617. alwaysAllowFollowupQuestions,
  1618. followupAutoApproveTimeoutMs,
  1619. includeDiagnosticMessages,
  1620. maxDiagnosticMessages,
  1621. includeTaskHistoryInEnhance,
  1622. includeCurrentTime,
  1623. includeCurrentCost,
  1624. maxGitStatusFiles,
  1625. taskSyncEnabled,
  1626. remoteControlEnabled,
  1627. imageGenerationProvider,
  1628. openRouterImageApiKey,
  1629. openRouterImageGenerationSelectedModel,
  1630. openRouterUseMiddleOutTransform,
  1631. featureRoomoteControlEnabled,
  1632. isBrowserSessionActive,
  1633. } = await this.getState()
  1634. let cloudOrganizations: CloudOrganizationMembership[] = []
  1635. try {
  1636. if (!CloudService.instance.isCloudAgent) {
  1637. const now = Date.now()
  1638. if (
  1639. this.cloudOrganizationsCache !== null &&
  1640. this.cloudOrganizationsCacheTimestamp !== null &&
  1641. now - this.cloudOrganizationsCacheTimestamp < ClineProvider.CLOUD_ORGANIZATIONS_CACHE_DURATION_MS
  1642. ) {
  1643. cloudOrganizations = this.cloudOrganizationsCache!
  1644. } else {
  1645. cloudOrganizations = await CloudService.instance.getOrganizationMemberships()
  1646. this.cloudOrganizationsCache = cloudOrganizations
  1647. this.cloudOrganizationsCacheTimestamp = now
  1648. }
  1649. }
  1650. } catch (error) {
  1651. // Ignore this error.
  1652. }
  1653. const telemetryKey = process.env.POSTHOG_API_KEY
  1654. const machineId = vscode.env.machineId
  1655. const mergedAllowedCommands = this.mergeAllowedCommands(allowedCommands)
  1656. const mergedDeniedCommands = this.mergeDeniedCommands(deniedCommands)
  1657. const cwd = this.cwd
  1658. // Check if there's a system prompt override for the current mode
  1659. const currentMode = mode ?? defaultModeSlug
  1660. const hasSystemPromptOverride = await this.hasFileBasedSystemPromptOverride(currentMode)
  1661. return {
  1662. version: this.context.extension?.packageJSON?.version ?? "",
  1663. apiConfiguration,
  1664. customInstructions,
  1665. alwaysAllowReadOnly: alwaysAllowReadOnly ?? false,
  1666. alwaysAllowReadOnlyOutsideWorkspace: alwaysAllowReadOnlyOutsideWorkspace ?? false,
  1667. alwaysAllowWrite: alwaysAllowWrite ?? false,
  1668. alwaysAllowWriteOutsideWorkspace: alwaysAllowWriteOutsideWorkspace ?? false,
  1669. alwaysAllowWriteProtected: alwaysAllowWriteProtected ?? false,
  1670. alwaysAllowExecute: alwaysAllowExecute ?? false,
  1671. alwaysAllowBrowser: alwaysAllowBrowser ?? false,
  1672. alwaysAllowMcp: alwaysAllowMcp ?? false,
  1673. alwaysAllowModeSwitch: alwaysAllowModeSwitch ?? false,
  1674. alwaysAllowSubtasks: alwaysAllowSubtasks ?? false,
  1675. isBrowserSessionActive,
  1676. allowedMaxRequests,
  1677. allowedMaxCost,
  1678. autoCondenseContext: autoCondenseContext ?? true,
  1679. autoCondenseContextPercent: autoCondenseContextPercent ?? 100,
  1680. uriScheme: vscode.env.uriScheme,
  1681. currentTaskItem: this.getCurrentTask()?.taskId
  1682. ? (taskHistory || []).find((item: HistoryItem) => item.id === this.getCurrentTask()?.taskId)
  1683. : undefined,
  1684. clineMessages: this.getCurrentTask()?.clineMessages || [],
  1685. currentTaskTodos: this.getCurrentTask()?.todoList || [],
  1686. messageQueue: this.getCurrentTask()?.messageQueueService?.messages,
  1687. taskHistory: (taskHistory || [])
  1688. .filter((item: HistoryItem) => item.ts && item.task)
  1689. .sort((a: HistoryItem, b: HistoryItem) => b.ts - a.ts),
  1690. soundEnabled: soundEnabled ?? false,
  1691. ttsEnabled: ttsEnabled ?? false,
  1692. ttsSpeed: ttsSpeed ?? 1.0,
  1693. diffEnabled: diffEnabled ?? true,
  1694. enableCheckpoints: enableCheckpoints ?? true,
  1695. checkpointTimeout: checkpointTimeout ?? DEFAULT_CHECKPOINT_TIMEOUT_SECONDS,
  1696. shouldShowAnnouncement:
  1697. telemetrySetting !== "unset" && lastShownAnnouncementId !== this.latestAnnouncementId,
  1698. allowedCommands: mergedAllowedCommands,
  1699. deniedCommands: mergedDeniedCommands,
  1700. soundVolume: soundVolume ?? 0.5,
  1701. browserViewportSize: browserViewportSize ?? "900x600",
  1702. screenshotQuality: screenshotQuality ?? 75,
  1703. remoteBrowserHost,
  1704. remoteBrowserEnabled: remoteBrowserEnabled ?? false,
  1705. cachedChromeHostUrl: cachedChromeHostUrl,
  1706. writeDelayMs: writeDelayMs ?? DEFAULT_WRITE_DELAY_MS,
  1707. terminalOutputLineLimit: terminalOutputLineLimit ?? 500,
  1708. terminalOutputCharacterLimit: terminalOutputCharacterLimit ?? DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT,
  1709. terminalShellIntegrationTimeout: terminalShellIntegrationTimeout ?? Terminal.defaultShellIntegrationTimeout,
  1710. terminalShellIntegrationDisabled: terminalShellIntegrationDisabled ?? true,
  1711. terminalCommandDelay: terminalCommandDelay ?? 0,
  1712. terminalPowershellCounter: terminalPowershellCounter ?? false,
  1713. terminalZshClearEolMark: terminalZshClearEolMark ?? true,
  1714. terminalZshOhMy: terminalZshOhMy ?? false,
  1715. terminalZshP10k: terminalZshP10k ?? false,
  1716. terminalZdotdir: terminalZdotdir ?? false,
  1717. fuzzyMatchThreshold: fuzzyMatchThreshold ?? 1.0,
  1718. mcpEnabled: mcpEnabled ?? true,
  1719. enableMcpServerCreation: enableMcpServerCreation ?? true,
  1720. currentApiConfigName: currentApiConfigName ?? "default",
  1721. listApiConfigMeta: listApiConfigMeta ?? [],
  1722. pinnedApiConfigs: pinnedApiConfigs ?? {},
  1723. mode: mode ?? defaultModeSlug,
  1724. customModePrompts: customModePrompts ?? {},
  1725. customSupportPrompts: customSupportPrompts ?? {},
  1726. enhancementApiConfigId,
  1727. autoApprovalEnabled: autoApprovalEnabled ?? false,
  1728. customModes,
  1729. experiments: experiments ?? experimentDefault,
  1730. mcpServers: this.mcpHub?.getAllServers() ?? [],
  1731. maxOpenTabsContext: maxOpenTabsContext ?? 20,
  1732. maxWorkspaceFiles: maxWorkspaceFiles ?? 200,
  1733. cwd,
  1734. browserToolEnabled: browserToolEnabled ?? true,
  1735. telemetrySetting,
  1736. telemetryKey,
  1737. machineId,
  1738. showRooIgnoredFiles: showRooIgnoredFiles ?? false,
  1739. language: language ?? formatLanguage(vscode.env.language),
  1740. renderContext: this.renderContext,
  1741. maxReadFileLine: maxReadFileLine ?? -1,
  1742. maxImageFileSize: maxImageFileSize ?? 5,
  1743. maxTotalImageSize: maxTotalImageSize ?? 20,
  1744. maxConcurrentFileReads: maxConcurrentFileReads ?? 5,
  1745. settingsImportedAt: this.settingsImportedAt,
  1746. terminalCompressProgressBar: terminalCompressProgressBar ?? true,
  1747. hasSystemPromptOverride,
  1748. historyPreviewCollapsed: historyPreviewCollapsed ?? false,
  1749. reasoningBlockCollapsed: reasoningBlockCollapsed ?? true,
  1750. enterBehavior: enterBehavior ?? "send",
  1751. cloudUserInfo,
  1752. cloudIsAuthenticated: cloudIsAuthenticated ?? false,
  1753. cloudOrganizations,
  1754. sharingEnabled: sharingEnabled ?? false,
  1755. organizationAllowList,
  1756. organizationSettingsVersion,
  1757. condensingApiConfigId,
  1758. customCondensingPrompt,
  1759. codebaseIndexModels: codebaseIndexModels ?? EMBEDDING_MODEL_PROFILES,
  1760. codebaseIndexConfig: {
  1761. codebaseIndexEnabled: codebaseIndexConfig?.codebaseIndexEnabled ?? false,
  1762. codebaseIndexQdrantUrl: codebaseIndexConfig?.codebaseIndexQdrantUrl ?? "http://localhost:6333",
  1763. codebaseIndexEmbedderProvider: codebaseIndexConfig?.codebaseIndexEmbedderProvider ?? "openai",
  1764. codebaseIndexEmbedderBaseUrl: codebaseIndexConfig?.codebaseIndexEmbedderBaseUrl ?? "",
  1765. codebaseIndexEmbedderModelId: codebaseIndexConfig?.codebaseIndexEmbedderModelId ?? "",
  1766. codebaseIndexEmbedderModelDimension: codebaseIndexConfig?.codebaseIndexEmbedderModelDimension ?? 1536,
  1767. codebaseIndexOpenAiCompatibleBaseUrl: codebaseIndexConfig?.codebaseIndexOpenAiCompatibleBaseUrl,
  1768. codebaseIndexSearchMaxResults: codebaseIndexConfig?.codebaseIndexSearchMaxResults,
  1769. codebaseIndexSearchMinScore: codebaseIndexConfig?.codebaseIndexSearchMinScore,
  1770. codebaseIndexBedrockRegion: codebaseIndexConfig?.codebaseIndexBedrockRegion,
  1771. codebaseIndexBedrockProfile: codebaseIndexConfig?.codebaseIndexBedrockProfile,
  1772. codebaseIndexOpenRouterSpecificProvider: codebaseIndexConfig?.codebaseIndexOpenRouterSpecificProvider,
  1773. },
  1774. // Only set mdmCompliant if there's an actual MDM policy
  1775. // undefined means no MDM policy, true means compliant, false means non-compliant
  1776. mdmCompliant: this.mdmService?.requiresCloudAuth() ? this.checkMdmCompliance() : undefined,
  1777. profileThresholds: profileThresholds ?? {},
  1778. cloudApiUrl: getRooCodeApiUrl(),
  1779. hasOpenedModeSelector: this.getGlobalState("hasOpenedModeSelector") ?? false,
  1780. alwaysAllowFollowupQuestions: alwaysAllowFollowupQuestions ?? false,
  1781. followupAutoApproveTimeoutMs: followupAutoApproveTimeoutMs ?? 60000,
  1782. includeDiagnosticMessages: includeDiagnosticMessages ?? true,
  1783. maxDiagnosticMessages: maxDiagnosticMessages ?? 50,
  1784. includeTaskHistoryInEnhance: includeTaskHistoryInEnhance ?? true,
  1785. includeCurrentTime: includeCurrentTime ?? true,
  1786. includeCurrentCost: includeCurrentCost ?? true,
  1787. maxGitStatusFiles: maxGitStatusFiles ?? 0,
  1788. taskSyncEnabled,
  1789. remoteControlEnabled,
  1790. imageGenerationProvider,
  1791. openRouterImageApiKey,
  1792. openRouterImageGenerationSelectedModel,
  1793. openRouterUseMiddleOutTransform,
  1794. featureRoomoteControlEnabled,
  1795. debug: vscode.workspace.getConfiguration(Package.name).get<boolean>("debug", false),
  1796. }
  1797. }
  1798. /**
  1799. * Storage
  1800. * https://dev.to/kompotkot/how-to-use-secretstorage-in-your-vscode-extensions-2hco
  1801. * https://www.eliostruyf.com/devhack-code-extension-storage-options/
  1802. */
  1803. async getState(): Promise<
  1804. Omit<
  1805. ExtensionState,
  1806. | "clineMessages"
  1807. | "renderContext"
  1808. | "hasOpenedModeSelector"
  1809. | "version"
  1810. | "shouldShowAnnouncement"
  1811. | "hasSystemPromptOverride"
  1812. >
  1813. > {
  1814. const stateValues = this.contextProxy.getValues()
  1815. const customModes = await this.customModesManager.getCustomModes()
  1816. // Determine apiProvider with the same logic as before.
  1817. const apiProvider: ProviderName = stateValues.apiProvider ? stateValues.apiProvider : "anthropic"
  1818. // Build the apiConfiguration object combining state values and secrets.
  1819. const providerSettings = this.contextProxy.getProviderSettings()
  1820. // Ensure apiProvider is set properly if not already in state
  1821. if (!providerSettings.apiProvider) {
  1822. providerSettings.apiProvider = apiProvider
  1823. }
  1824. let organizationAllowList = ORGANIZATION_ALLOW_ALL
  1825. try {
  1826. organizationAllowList = await CloudService.instance.getAllowList()
  1827. } catch (error) {
  1828. console.error(
  1829. `[getState] failed to get organization allow list: ${error instanceof Error ? error.message : String(error)}`,
  1830. )
  1831. }
  1832. let cloudUserInfo: CloudUserInfo | null = null
  1833. try {
  1834. cloudUserInfo = CloudService.instance.getUserInfo()
  1835. } catch (error) {
  1836. console.error(
  1837. `[getState] failed to get cloud user info: ${error instanceof Error ? error.message : String(error)}`,
  1838. )
  1839. }
  1840. let cloudIsAuthenticated: boolean = false
  1841. try {
  1842. cloudIsAuthenticated = CloudService.instance.isAuthenticated()
  1843. } catch (error) {
  1844. console.error(
  1845. `[getState] failed to get cloud authentication state: ${error instanceof Error ? error.message : String(error)}`,
  1846. )
  1847. }
  1848. let sharingEnabled: boolean = false
  1849. try {
  1850. sharingEnabled = await CloudService.instance.canShareTask()
  1851. } catch (error) {
  1852. console.error(
  1853. `[getState] failed to get sharing enabled state: ${error instanceof Error ? error.message : String(error)}`,
  1854. )
  1855. }
  1856. let organizationSettingsVersion: number = -1
  1857. try {
  1858. if (CloudService.hasInstance()) {
  1859. const settings = CloudService.instance.getOrganizationSettings()
  1860. organizationSettingsVersion = settings?.version ?? -1
  1861. }
  1862. } catch (error) {
  1863. console.error(
  1864. `[getState] failed to get organization settings version: ${error instanceof Error ? error.message : String(error)}`,
  1865. )
  1866. }
  1867. let taskSyncEnabled: boolean = false
  1868. try {
  1869. taskSyncEnabled = CloudService.instance.isTaskSyncEnabled()
  1870. } catch (error) {
  1871. console.error(
  1872. `[getState] failed to get task sync enabled state: ${error instanceof Error ? error.message : String(error)}`,
  1873. )
  1874. }
  1875. // Get actual browser session state
  1876. const isBrowserSessionActive = this.getCurrentTask()?.browserSession?.isSessionActive() ?? false
  1877. // Return the same structure as before.
  1878. return {
  1879. apiConfiguration: providerSettings,
  1880. lastShownAnnouncementId: stateValues.lastShownAnnouncementId,
  1881. customInstructions: stateValues.customInstructions,
  1882. apiModelId: stateValues.apiModelId,
  1883. alwaysAllowReadOnly: stateValues.alwaysAllowReadOnly ?? false,
  1884. alwaysAllowReadOnlyOutsideWorkspace: stateValues.alwaysAllowReadOnlyOutsideWorkspace ?? false,
  1885. alwaysAllowWrite: stateValues.alwaysAllowWrite ?? false,
  1886. alwaysAllowWriteOutsideWorkspace: stateValues.alwaysAllowWriteOutsideWorkspace ?? false,
  1887. alwaysAllowWriteProtected: stateValues.alwaysAllowWriteProtected ?? false,
  1888. alwaysAllowExecute: stateValues.alwaysAllowExecute ?? false,
  1889. alwaysAllowBrowser: stateValues.alwaysAllowBrowser ?? false,
  1890. alwaysAllowMcp: stateValues.alwaysAllowMcp ?? false,
  1891. alwaysAllowModeSwitch: stateValues.alwaysAllowModeSwitch ?? false,
  1892. alwaysAllowSubtasks: stateValues.alwaysAllowSubtasks ?? false,
  1893. alwaysAllowFollowupQuestions: stateValues.alwaysAllowFollowupQuestions ?? false,
  1894. isBrowserSessionActive,
  1895. followupAutoApproveTimeoutMs: stateValues.followupAutoApproveTimeoutMs ?? 60000,
  1896. diagnosticsEnabled: stateValues.diagnosticsEnabled ?? true,
  1897. allowedMaxRequests: stateValues.allowedMaxRequests,
  1898. allowedMaxCost: stateValues.allowedMaxCost,
  1899. autoCondenseContext: stateValues.autoCondenseContext ?? true,
  1900. autoCondenseContextPercent: stateValues.autoCondenseContextPercent ?? 100,
  1901. taskHistory: stateValues.taskHistory ?? [],
  1902. allowedCommands: stateValues.allowedCommands,
  1903. deniedCommands: stateValues.deniedCommands,
  1904. soundEnabled: stateValues.soundEnabled ?? false,
  1905. ttsEnabled: stateValues.ttsEnabled ?? false,
  1906. ttsSpeed: stateValues.ttsSpeed ?? 1.0,
  1907. diffEnabled: stateValues.diffEnabled ?? true,
  1908. enableCheckpoints: stateValues.enableCheckpoints ?? true,
  1909. checkpointTimeout: stateValues.checkpointTimeout ?? DEFAULT_CHECKPOINT_TIMEOUT_SECONDS,
  1910. soundVolume: stateValues.soundVolume,
  1911. browserViewportSize: stateValues.browserViewportSize ?? "900x600",
  1912. screenshotQuality: stateValues.screenshotQuality ?? 75,
  1913. remoteBrowserHost: stateValues.remoteBrowserHost,
  1914. remoteBrowserEnabled: stateValues.remoteBrowserEnabled ?? false,
  1915. cachedChromeHostUrl: stateValues.cachedChromeHostUrl as string | undefined,
  1916. fuzzyMatchThreshold: stateValues.fuzzyMatchThreshold ?? 1.0,
  1917. writeDelayMs: stateValues.writeDelayMs ?? DEFAULT_WRITE_DELAY_MS,
  1918. terminalOutputLineLimit: stateValues.terminalOutputLineLimit ?? 500,
  1919. terminalOutputCharacterLimit:
  1920. stateValues.terminalOutputCharacterLimit ?? DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT,
  1921. terminalShellIntegrationTimeout:
  1922. stateValues.terminalShellIntegrationTimeout ?? Terminal.defaultShellIntegrationTimeout,
  1923. terminalShellIntegrationDisabled: stateValues.terminalShellIntegrationDisabled ?? true,
  1924. terminalCommandDelay: stateValues.terminalCommandDelay ?? 0,
  1925. terminalPowershellCounter: stateValues.terminalPowershellCounter ?? false,
  1926. terminalZshClearEolMark: stateValues.terminalZshClearEolMark ?? true,
  1927. terminalZshOhMy: stateValues.terminalZshOhMy ?? false,
  1928. terminalZshP10k: stateValues.terminalZshP10k ?? false,
  1929. terminalZdotdir: stateValues.terminalZdotdir ?? false,
  1930. terminalCompressProgressBar: stateValues.terminalCompressProgressBar ?? true,
  1931. mode: stateValues.mode ?? defaultModeSlug,
  1932. language: stateValues.language ?? formatLanguage(vscode.env.language),
  1933. mcpEnabled: stateValues.mcpEnabled ?? true,
  1934. enableMcpServerCreation: stateValues.enableMcpServerCreation ?? true,
  1935. mcpServers: this.mcpHub?.getAllServers() ?? [],
  1936. currentApiConfigName: stateValues.currentApiConfigName ?? "default",
  1937. listApiConfigMeta: stateValues.listApiConfigMeta ?? [],
  1938. pinnedApiConfigs: stateValues.pinnedApiConfigs ?? {},
  1939. modeApiConfigs: stateValues.modeApiConfigs ?? ({} as Record<Mode, string>),
  1940. customModePrompts: stateValues.customModePrompts ?? {},
  1941. customSupportPrompts: stateValues.customSupportPrompts ?? {},
  1942. enhancementApiConfigId: stateValues.enhancementApiConfigId,
  1943. experiments: stateValues.experiments ?? experimentDefault,
  1944. autoApprovalEnabled: stateValues.autoApprovalEnabled ?? false,
  1945. customModes,
  1946. maxOpenTabsContext: stateValues.maxOpenTabsContext ?? 20,
  1947. maxWorkspaceFiles: stateValues.maxWorkspaceFiles ?? 200,
  1948. openRouterUseMiddleOutTransform: stateValues.openRouterUseMiddleOutTransform,
  1949. browserToolEnabled: stateValues.browserToolEnabled ?? true,
  1950. telemetrySetting: stateValues.telemetrySetting || "unset",
  1951. showRooIgnoredFiles: stateValues.showRooIgnoredFiles ?? false,
  1952. maxReadFileLine: stateValues.maxReadFileLine ?? -1,
  1953. maxImageFileSize: stateValues.maxImageFileSize ?? 5,
  1954. maxTotalImageSize: stateValues.maxTotalImageSize ?? 20,
  1955. maxConcurrentFileReads: stateValues.maxConcurrentFileReads ?? 5,
  1956. historyPreviewCollapsed: stateValues.historyPreviewCollapsed ?? false,
  1957. reasoningBlockCollapsed: stateValues.reasoningBlockCollapsed ?? true,
  1958. enterBehavior: stateValues.enterBehavior ?? "send",
  1959. cloudUserInfo,
  1960. cloudIsAuthenticated,
  1961. sharingEnabled,
  1962. organizationAllowList,
  1963. organizationSettingsVersion,
  1964. condensingApiConfigId: stateValues.condensingApiConfigId,
  1965. customCondensingPrompt: stateValues.customCondensingPrompt,
  1966. codebaseIndexModels: stateValues.codebaseIndexModels ?? EMBEDDING_MODEL_PROFILES,
  1967. codebaseIndexConfig: {
  1968. codebaseIndexEnabled: stateValues.codebaseIndexConfig?.codebaseIndexEnabled ?? false,
  1969. codebaseIndexQdrantUrl:
  1970. stateValues.codebaseIndexConfig?.codebaseIndexQdrantUrl ?? "http://localhost:6333",
  1971. codebaseIndexEmbedderProvider:
  1972. stateValues.codebaseIndexConfig?.codebaseIndexEmbedderProvider ?? "openai",
  1973. codebaseIndexEmbedderBaseUrl: stateValues.codebaseIndexConfig?.codebaseIndexEmbedderBaseUrl ?? "",
  1974. codebaseIndexEmbedderModelId: stateValues.codebaseIndexConfig?.codebaseIndexEmbedderModelId ?? "",
  1975. codebaseIndexEmbedderModelDimension:
  1976. stateValues.codebaseIndexConfig?.codebaseIndexEmbedderModelDimension,
  1977. codebaseIndexOpenAiCompatibleBaseUrl:
  1978. stateValues.codebaseIndexConfig?.codebaseIndexOpenAiCompatibleBaseUrl,
  1979. codebaseIndexSearchMaxResults: stateValues.codebaseIndexConfig?.codebaseIndexSearchMaxResults,
  1980. codebaseIndexSearchMinScore: stateValues.codebaseIndexConfig?.codebaseIndexSearchMinScore,
  1981. codebaseIndexBedrockRegion: stateValues.codebaseIndexConfig?.codebaseIndexBedrockRegion,
  1982. codebaseIndexBedrockProfile: stateValues.codebaseIndexConfig?.codebaseIndexBedrockProfile,
  1983. codebaseIndexOpenRouterSpecificProvider:
  1984. stateValues.codebaseIndexConfig?.codebaseIndexOpenRouterSpecificProvider,
  1985. },
  1986. profileThresholds: stateValues.profileThresholds ?? {},
  1987. includeDiagnosticMessages: stateValues.includeDiagnosticMessages ?? true,
  1988. maxDiagnosticMessages: stateValues.maxDiagnosticMessages ?? 50,
  1989. includeTaskHistoryInEnhance: stateValues.includeTaskHistoryInEnhance ?? true,
  1990. includeCurrentTime: stateValues.includeCurrentTime ?? true,
  1991. includeCurrentCost: stateValues.includeCurrentCost ?? true,
  1992. maxGitStatusFiles: stateValues.maxGitStatusFiles ?? 0,
  1993. taskSyncEnabled,
  1994. remoteControlEnabled: (() => {
  1995. try {
  1996. const cloudSettings = CloudService.instance.getUserSettings()
  1997. return cloudSettings?.settings?.extensionBridgeEnabled ?? false
  1998. } catch (error) {
  1999. console.error(
  2000. `[getState] failed to get remote control setting from cloud: ${error instanceof Error ? error.message : String(error)}`,
  2001. )
  2002. return false
  2003. }
  2004. })(),
  2005. imageGenerationProvider: stateValues.imageGenerationProvider,
  2006. openRouterImageApiKey: stateValues.openRouterImageApiKey,
  2007. openRouterImageGenerationSelectedModel: stateValues.openRouterImageGenerationSelectedModel,
  2008. featureRoomoteControlEnabled: (() => {
  2009. try {
  2010. const userSettings = CloudService.instance.getUserSettings()
  2011. const hasOrganization = cloudUserInfo?.organizationId != null
  2012. return hasOrganization || (userSettings?.features?.roomoteControlEnabled ?? false)
  2013. } catch (error) {
  2014. console.error(
  2015. `[getState] failed to get featureRoomoteControlEnabled: ${error instanceof Error ? error.message : String(error)}`,
  2016. )
  2017. return false
  2018. }
  2019. })(),
  2020. }
  2021. }
  2022. async updateTaskHistory(item: HistoryItem): Promise<HistoryItem[]> {
  2023. const history = (this.getGlobalState("taskHistory") as HistoryItem[] | undefined) || []
  2024. const existingItemIndex = history.findIndex((h) => h.id === item.id)
  2025. if (existingItemIndex !== -1) {
  2026. // Preserve existing metadata (e.g., delegation fields) unless explicitly overwritten.
  2027. // This prevents loss of status/awaitingChildId/delegatedToId when tasks are reopened,
  2028. // terminated, or when routine message persistence occurs.
  2029. history[existingItemIndex] = {
  2030. ...history[existingItemIndex],
  2031. ...item,
  2032. }
  2033. } else {
  2034. history.push(item)
  2035. }
  2036. await this.updateGlobalState("taskHistory", history)
  2037. this.recentTasksCache = undefined
  2038. return history
  2039. }
  2040. // ContextProxy
  2041. // @deprecated - Use `ContextProxy#setValue` instead.
  2042. private async updateGlobalState<K extends keyof GlobalState>(key: K, value: GlobalState[K]) {
  2043. await this.contextProxy.setValue(key, value)
  2044. }
  2045. // @deprecated - Use `ContextProxy#getValue` instead.
  2046. private getGlobalState<K extends keyof GlobalState>(key: K) {
  2047. return this.contextProxy.getValue(key)
  2048. }
  2049. public async setValue<K extends keyof RooCodeSettings>(key: K, value: RooCodeSettings[K]) {
  2050. await this.contextProxy.setValue(key, value)
  2051. }
  2052. public getValue<K extends keyof RooCodeSettings>(key: K) {
  2053. return this.contextProxy.getValue(key)
  2054. }
  2055. public getValues() {
  2056. return this.contextProxy.getValues()
  2057. }
  2058. public async setValues(values: RooCodeSettings) {
  2059. await this.contextProxy.setValues(values)
  2060. }
  2061. // dev
  2062. async resetState() {
  2063. const answer = await vscode.window.showInformationMessage(
  2064. t("common:confirmation.reset_state"),
  2065. { modal: true },
  2066. t("common:answers.yes"),
  2067. )
  2068. if (answer !== t("common:answers.yes")) {
  2069. return
  2070. }
  2071. // Log out from cloud if authenticated
  2072. if (CloudService.hasInstance()) {
  2073. try {
  2074. await CloudService.instance.logout()
  2075. } catch (error) {
  2076. this.log(
  2077. `Failed to logout from cloud during reset: ${error instanceof Error ? error.message : String(error)}`,
  2078. )
  2079. // Continue with reset even if logout fails
  2080. }
  2081. }
  2082. await this.contextProxy.resetAllState()
  2083. await this.providerSettingsManager.resetAllConfigs()
  2084. await this.customModesManager.resetCustomModes()
  2085. await this.removeClineFromStack()
  2086. await this.postStateToWebview()
  2087. await this.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
  2088. }
  2089. // logging
  2090. public log(message: string) {
  2091. this.outputChannel.appendLine(message)
  2092. console.log(message)
  2093. }
  2094. // getters
  2095. public get workspaceTracker(): WorkspaceTracker | undefined {
  2096. return this._workspaceTracker
  2097. }
  2098. get viewLaunched() {
  2099. return this.isViewLaunched
  2100. }
  2101. get messages() {
  2102. return this.getCurrentTask()?.clineMessages || []
  2103. }
  2104. public getMcpHub(): McpHub | undefined {
  2105. return this.mcpHub
  2106. }
  2107. /**
  2108. * Check if the current state is compliant with MDM policy
  2109. * @returns true if compliant or no MDM policy exists, false if MDM policy exists and user is non-compliant
  2110. */
  2111. public checkMdmCompliance(): boolean {
  2112. if (!this.mdmService) {
  2113. return true // No MDM service, allow operation
  2114. }
  2115. const compliance = this.mdmService.isCompliant()
  2116. if (!compliance.compliant) {
  2117. return false
  2118. }
  2119. return true
  2120. }
  2121. public async remoteControlEnabled(enabled: boolean) {
  2122. if (!enabled) {
  2123. await BridgeOrchestrator.disconnect()
  2124. return
  2125. }
  2126. const userInfo = CloudService.instance.getUserInfo()
  2127. if (!userInfo) {
  2128. this.log("[ClineProvider#remoteControlEnabled] Failed to get user info, disconnecting")
  2129. await BridgeOrchestrator.disconnect()
  2130. return
  2131. }
  2132. const config = await CloudService.instance.cloudAPI?.bridgeConfig().catch(() => undefined)
  2133. if (!config) {
  2134. this.log("[ClineProvider#remoteControlEnabled] Failed to get bridge config")
  2135. return
  2136. }
  2137. await BridgeOrchestrator.connectOrDisconnect(userInfo, enabled, {
  2138. ...config,
  2139. provider: this,
  2140. sessionId: vscode.env.sessionId,
  2141. isCloudAgent: CloudService.instance.isCloudAgent,
  2142. })
  2143. const bridge = BridgeOrchestrator.getInstance()
  2144. if (bridge) {
  2145. const currentTask = this.getCurrentTask()
  2146. if (currentTask && !currentTask.enableBridge) {
  2147. try {
  2148. currentTask.enableBridge = true
  2149. await BridgeOrchestrator.subscribeToTask(currentTask)
  2150. } catch (error) {
  2151. const message = `[ClineProvider#remoteControlEnabled] BridgeOrchestrator.subscribeToTask() failed: ${error instanceof Error ? error.message : String(error)}`
  2152. this.log(message)
  2153. console.error(message)
  2154. }
  2155. }
  2156. } else {
  2157. for (const task of this.clineStack) {
  2158. if (task.enableBridge) {
  2159. try {
  2160. await BridgeOrchestrator.getInstance()?.unsubscribeFromTask(task.taskId)
  2161. } catch (error) {
  2162. const message = `[ClineProvider#remoteControlEnabled] BridgeOrchestrator#unsubscribeFromTask() failed: ${error instanceof Error ? error.message : String(error)}`
  2163. this.log(message)
  2164. console.error(message)
  2165. }
  2166. }
  2167. }
  2168. }
  2169. }
  2170. /**
  2171. * Gets the CodeIndexManager for the current active workspace
  2172. * @returns CodeIndexManager instance for the current workspace or the default one
  2173. */
  2174. public getCurrentWorkspaceCodeIndexManager(): CodeIndexManager | undefined {
  2175. return CodeIndexManager.getInstance(this.context)
  2176. }
  2177. /**
  2178. * Updates the code index status subscription to listen to the current workspace manager
  2179. */
  2180. private updateCodeIndexStatusSubscription(): void {
  2181. // Get the current workspace manager
  2182. const currentManager = this.getCurrentWorkspaceCodeIndexManager()
  2183. // If the manager hasn't changed, no need to update subscription
  2184. if (currentManager === this.codeIndexManager) {
  2185. return
  2186. }
  2187. // Dispose the old subscription if it exists
  2188. if (this.codeIndexStatusSubscription) {
  2189. this.codeIndexStatusSubscription.dispose()
  2190. this.codeIndexStatusSubscription = undefined
  2191. }
  2192. // Update the current workspace manager reference
  2193. this.codeIndexManager = currentManager
  2194. // Subscribe to the new manager's progress updates if it exists
  2195. if (currentManager) {
  2196. this.codeIndexStatusSubscription = currentManager.onProgressUpdate((update: IndexProgressUpdate) => {
  2197. // Only send updates if this manager is still the current one
  2198. if (currentManager === this.getCurrentWorkspaceCodeIndexManager()) {
  2199. // Get the full status from the manager to ensure we have all fields correctly formatted
  2200. const fullStatus = currentManager.getCurrentStatus()
  2201. this.postMessageToWebview({
  2202. type: "indexingStatusUpdate",
  2203. values: fullStatus,
  2204. })
  2205. }
  2206. })
  2207. if (this.view) {
  2208. this.webviewDisposables.push(this.codeIndexStatusSubscription)
  2209. }
  2210. // Send initial status for the current workspace
  2211. this.postMessageToWebview({
  2212. type: "indexingStatusUpdate",
  2213. values: currentManager.getCurrentStatus(),
  2214. })
  2215. }
  2216. }
  2217. /**
  2218. * TaskProviderLike, TelemetryPropertiesProvider
  2219. */
  2220. public getCurrentTask(): Task | undefined {
  2221. if (this.clineStack.length === 0) {
  2222. return undefined
  2223. }
  2224. return this.clineStack[this.clineStack.length - 1]
  2225. }
  2226. public getRecentTasks(): string[] {
  2227. if (this.recentTasksCache) {
  2228. return this.recentTasksCache
  2229. }
  2230. const history = this.getGlobalState("taskHistory") ?? []
  2231. const workspaceTasks: HistoryItem[] = []
  2232. for (const item of history) {
  2233. if (!item.ts || !item.task || item.workspace !== this.cwd) {
  2234. continue
  2235. }
  2236. workspaceTasks.push(item)
  2237. }
  2238. if (workspaceTasks.length === 0) {
  2239. this.recentTasksCache = []
  2240. return this.recentTasksCache
  2241. }
  2242. workspaceTasks.sort((a, b) => b.ts - a.ts)
  2243. let recentTaskIds: string[] = []
  2244. if (workspaceTasks.length >= 100) {
  2245. // If we have at least 100 tasks, return tasks from the last 7 days.
  2246. const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000
  2247. for (const item of workspaceTasks) {
  2248. // Stop when we hit tasks older than 7 days.
  2249. if (item.ts < sevenDaysAgo) {
  2250. break
  2251. }
  2252. recentTaskIds.push(item.id)
  2253. }
  2254. } else {
  2255. // Otherwise, return the most recent 100 tasks (or all if less than 100).
  2256. recentTaskIds = workspaceTasks.slice(0, Math.min(100, workspaceTasks.length)).map((item) => item.id)
  2257. }
  2258. this.recentTasksCache = recentTaskIds
  2259. return this.recentTasksCache
  2260. }
  2261. // When initializing a new task, (not from history but from a tool command
  2262. // new_task) there is no need to remove the previous task since the new
  2263. // task is a subtask of the previous one, and when it finishes it is removed
  2264. // from the stack and the caller is resumed in this way we can have a chain
  2265. // of tasks, each one being a sub task of the previous one until the main
  2266. // task is finished.
  2267. public async createTask(
  2268. text?: string,
  2269. images?: string[],
  2270. parentTask?: Task,
  2271. options: CreateTaskOptions = {},
  2272. configuration: RooCodeSettings = {},
  2273. ): Promise<Task> {
  2274. if (configuration) {
  2275. await this.setValues(configuration)
  2276. if (configuration.allowedCommands) {
  2277. await vscode.workspace
  2278. .getConfiguration(Package.name)
  2279. .update("allowedCommands", configuration.allowedCommands, vscode.ConfigurationTarget.Global)
  2280. }
  2281. if (configuration.deniedCommands) {
  2282. await vscode.workspace
  2283. .getConfiguration(Package.name)
  2284. .update("deniedCommands", configuration.deniedCommands, vscode.ConfigurationTarget.Global)
  2285. }
  2286. if (configuration.commandExecutionTimeout !== undefined) {
  2287. await vscode.workspace
  2288. .getConfiguration(Package.name)
  2289. .update(
  2290. "commandExecutionTimeout",
  2291. configuration.commandExecutionTimeout,
  2292. vscode.ConfigurationTarget.Global,
  2293. )
  2294. }
  2295. if (configuration.currentApiConfigName) {
  2296. await this.setProviderProfile(configuration.currentApiConfigName)
  2297. }
  2298. }
  2299. const {
  2300. apiConfiguration,
  2301. organizationAllowList,
  2302. diffEnabled: enableDiff,
  2303. enableCheckpoints,
  2304. checkpointTimeout,
  2305. fuzzyMatchThreshold,
  2306. experiments,
  2307. cloudUserInfo,
  2308. remoteControlEnabled,
  2309. } = await this.getState()
  2310. // Single-open-task invariant: always enforce for user-initiated top-level tasks
  2311. if (!parentTask) {
  2312. try {
  2313. await this.removeClineFromStack()
  2314. } catch {
  2315. // Non-fatal
  2316. }
  2317. }
  2318. if (!ProfileValidator.isProfileAllowed(apiConfiguration, organizationAllowList)) {
  2319. throw new OrganizationAllowListViolationError(t("common:errors.violated_organization_allowlist"))
  2320. }
  2321. const task = new Task({
  2322. provider: this,
  2323. apiConfiguration,
  2324. enableDiff,
  2325. enableCheckpoints,
  2326. checkpointTimeout,
  2327. fuzzyMatchThreshold,
  2328. consecutiveMistakeLimit: apiConfiguration.consecutiveMistakeLimit,
  2329. task: text,
  2330. images,
  2331. experiments,
  2332. rootTask: this.clineStack.length > 0 ? this.clineStack[0] : undefined,
  2333. parentTask,
  2334. taskNumber: this.clineStack.length + 1,
  2335. onCreated: this.taskCreationCallback,
  2336. enableBridge: BridgeOrchestrator.isEnabled(cloudUserInfo, remoteControlEnabled),
  2337. initialTodos: options.initialTodos,
  2338. ...options,
  2339. })
  2340. await this.addClineToStack(task)
  2341. this.log(
  2342. `[createTask] ${task.parentTask ? "child" : "parent"} task ${task.taskId}.${task.instanceId} instantiated`,
  2343. )
  2344. return task
  2345. }
  2346. public async cancelTask(): Promise<void> {
  2347. const task = this.getCurrentTask()
  2348. if (!task) {
  2349. return
  2350. }
  2351. console.log(`[cancelTask] cancelling task ${task.taskId}.${task.instanceId}`)
  2352. const { historyItem, uiMessagesFilePath } = await this.getTaskWithId(task.taskId)
  2353. // Preserve parent and root task information for history item.
  2354. const rootTask = task.rootTask
  2355. const parentTask = task.parentTask
  2356. // Mark this as a user-initiated cancellation so provider-only rehydration can occur
  2357. task.abortReason = "user_cancelled"
  2358. // Capture the current instance to detect if rehydrate already occurred elsewhere
  2359. const originalInstanceId = task.instanceId
  2360. // Immediately cancel the underlying HTTP request if one is in progress
  2361. // This ensures the stream fails quickly rather than waiting for network timeout
  2362. task.cancelCurrentRequest()
  2363. // Begin abort (non-blocking)
  2364. task.abortTask()
  2365. // Immediately mark the original instance as abandoned to prevent any residual activity
  2366. task.abandoned = true
  2367. await pWaitFor(
  2368. () =>
  2369. this.getCurrentTask()! === undefined ||
  2370. this.getCurrentTask()!.isStreaming === false ||
  2371. this.getCurrentTask()!.didFinishAbortingStream ||
  2372. // If only the first chunk is processed, then there's no
  2373. // need to wait for graceful abort (closes edits, browser,
  2374. // etc).
  2375. this.getCurrentTask()!.isWaitingForFirstChunk,
  2376. {
  2377. timeout: 3_000,
  2378. },
  2379. ).catch(() => {
  2380. console.error("Failed to abort task")
  2381. })
  2382. // Defensive safeguard: if current instance already changed, skip rehydrate
  2383. const current = this.getCurrentTask()
  2384. if (current && current.instanceId !== originalInstanceId) {
  2385. this.log(
  2386. `[cancelTask] Skipping rehydrate: current instance ${current.instanceId} != original ${originalInstanceId}`,
  2387. )
  2388. return
  2389. }
  2390. // Final race check before rehydrate to avoid duplicate rehydration
  2391. {
  2392. const currentAfterCheck = this.getCurrentTask()
  2393. if (currentAfterCheck && currentAfterCheck.instanceId !== originalInstanceId) {
  2394. this.log(
  2395. `[cancelTask] Skipping rehydrate after final check: current instance ${currentAfterCheck.instanceId} != original ${originalInstanceId}`,
  2396. )
  2397. return
  2398. }
  2399. }
  2400. // Clears task again, so we need to abortTask manually above.
  2401. await this.createTaskWithHistoryItem({ ...historyItem, rootTask, parentTask })
  2402. }
  2403. // Clear the current task without treating it as a subtask.
  2404. // This is used when the user cancels a task that is not a subtask.
  2405. public async clearTask(): Promise<void> {
  2406. if (this.clineStack.length > 0) {
  2407. const task = this.clineStack[this.clineStack.length - 1]
  2408. console.log(`[clearTask] clearing task ${task.taskId}.${task.instanceId}`)
  2409. await this.removeClineFromStack()
  2410. }
  2411. }
  2412. public resumeTask(taskId: string): void {
  2413. // Use the existing showTaskWithId method which handles both current and
  2414. // historical tasks.
  2415. this.showTaskWithId(taskId).catch((error) => {
  2416. this.log(`Failed to resume task ${taskId}: ${error.message}`)
  2417. })
  2418. }
  2419. // Modes
  2420. public async getModes(): Promise<{ slug: string; name: string }[]> {
  2421. try {
  2422. const customModes = await this.customModesManager.getCustomModes()
  2423. return [...DEFAULT_MODES, ...customModes].map(({ slug, name }) => ({ slug, name }))
  2424. } catch (error) {
  2425. return DEFAULT_MODES.map(({ slug, name }) => ({ slug, name }))
  2426. }
  2427. }
  2428. public async getMode(): Promise<string> {
  2429. const { mode } = await this.getState()
  2430. return mode
  2431. }
  2432. public async setMode(mode: string): Promise<void> {
  2433. await this.setValues({ mode })
  2434. }
  2435. // Provider Profiles
  2436. public async getProviderProfiles(): Promise<{ name: string; provider?: string }[]> {
  2437. const { listApiConfigMeta = [] } = await this.getState()
  2438. return listApiConfigMeta.map((profile) => ({ name: profile.name, provider: profile.apiProvider }))
  2439. }
  2440. public async getProviderProfile(): Promise<string> {
  2441. const { currentApiConfigName = "default" } = await this.getState()
  2442. return currentApiConfigName
  2443. }
  2444. public async setProviderProfile(name: string): Promise<void> {
  2445. await this.activateProviderProfile({ name })
  2446. }
  2447. // Telemetry
  2448. private _appProperties?: StaticAppProperties
  2449. private _gitProperties?: GitProperties
  2450. private getAppProperties(): StaticAppProperties {
  2451. if (!this._appProperties) {
  2452. const packageJSON = this.context.extension?.packageJSON
  2453. this._appProperties = {
  2454. appName: packageJSON?.name ?? Package.name,
  2455. appVersion: packageJSON?.version ?? Package.version,
  2456. vscodeVersion: vscode.version,
  2457. platform: process.platform,
  2458. editorName: vscode.env.appName,
  2459. }
  2460. }
  2461. return this._appProperties
  2462. }
  2463. public get appProperties(): StaticAppProperties {
  2464. return this._appProperties ?? this.getAppProperties()
  2465. }
  2466. private getCloudProperties(): CloudAppProperties {
  2467. let cloudIsAuthenticated: boolean | undefined
  2468. try {
  2469. if (CloudService.hasInstance()) {
  2470. cloudIsAuthenticated = CloudService.instance.isAuthenticated()
  2471. }
  2472. } catch (error) {
  2473. // Silently handle errors to avoid breaking telemetry collection.
  2474. this.log(`[getTelemetryProperties] Failed to get cloud auth state: ${error}`)
  2475. }
  2476. return {
  2477. cloudIsAuthenticated,
  2478. }
  2479. }
  2480. private async getTaskProperties(): Promise<DynamicAppProperties & TaskProperties> {
  2481. const { language = "en", mode, apiConfiguration } = await this.getState()
  2482. const task = this.getCurrentTask()
  2483. const todoList = task?.todoList
  2484. let todos: { total: number; completed: number; inProgress: number; pending: number } | undefined
  2485. if (todoList && todoList.length > 0) {
  2486. todos = {
  2487. total: todoList.length,
  2488. completed: todoList.filter((todo) => todo.status === "completed").length,
  2489. inProgress: todoList.filter((todo) => todo.status === "in_progress").length,
  2490. pending: todoList.filter((todo) => todo.status === "pending").length,
  2491. }
  2492. }
  2493. return {
  2494. language,
  2495. mode,
  2496. taskId: task?.taskId,
  2497. parentTaskId: task?.parentTaskId,
  2498. apiProvider: apiConfiguration?.apiProvider,
  2499. modelId: task?.api?.getModel().id,
  2500. diffStrategy: task?.diffStrategy?.getName(),
  2501. isSubtask: task ? !!task.parentTaskId : undefined,
  2502. ...(todos && { todos }),
  2503. }
  2504. }
  2505. private async getGitProperties(): Promise<GitProperties> {
  2506. if (!this._gitProperties) {
  2507. this._gitProperties = await getWorkspaceGitInfo()
  2508. }
  2509. return this._gitProperties
  2510. }
  2511. public get gitProperties(): GitProperties | undefined {
  2512. return this._gitProperties
  2513. }
  2514. public async getTelemetryProperties(): Promise<TelemetryProperties> {
  2515. return {
  2516. ...this.getAppProperties(),
  2517. ...this.getCloudProperties(),
  2518. ...(await this.getTaskProperties()),
  2519. ...(await this.getGitProperties()),
  2520. }
  2521. }
  2522. public get cwd() {
  2523. return this.currentWorkspacePath || getWorkspacePath()
  2524. }
  2525. /**
  2526. * Delegate parent task and open child task.
  2527. *
  2528. * - Enforce single-open invariant
  2529. * - Persist parent delegation metadata
  2530. * - Emit TaskDelegated (task-level; API forwards to provider/bridge)
  2531. * - Create child as sole active and switch mode to child's mode
  2532. */
  2533. public async delegateParentAndOpenChild(params: {
  2534. parentTaskId: string
  2535. message: string
  2536. initialTodos: TodoItem[]
  2537. mode: string
  2538. }): Promise<Task> {
  2539. const { parentTaskId, message, initialTodos, mode } = params
  2540. // Metadata-driven delegation is always enabled
  2541. // 1) Get parent (must be current task)
  2542. const parent = this.getCurrentTask()
  2543. if (!parent) {
  2544. throw new Error("[delegateParentAndOpenChild] No current task")
  2545. }
  2546. if (parent.taskId !== parentTaskId) {
  2547. throw new Error(
  2548. `[delegateParentAndOpenChild] Parent mismatch: expected ${parentTaskId}, current ${parent.taskId}`,
  2549. )
  2550. }
  2551. // 2) Flush pending tool results to API history BEFORE disposing the parent.
  2552. // This is critical for native tool protocol: when tools are called before new_task,
  2553. // their tool_result blocks are in userMessageContent but not yet saved to API history.
  2554. // If we don't flush them, the parent's API conversation will be incomplete and
  2555. // cause 400 errors when resumed (missing tool_result for tool_use blocks).
  2556. //
  2557. // NOTE: We do NOT pass the assistant message here because the assistant message
  2558. // is already added to apiConversationHistory by the normal flow in
  2559. // recursivelyMakeClineRequests BEFORE tools start executing. We only need to
  2560. // flush the pending user message with tool_results.
  2561. try {
  2562. await parent.flushPendingToolResultsToHistory()
  2563. } catch (error) {
  2564. this.log(
  2565. `[delegateParentAndOpenChild] Error flushing pending tool results (non-fatal): ${
  2566. error instanceof Error ? error.message : String(error)
  2567. }`,
  2568. )
  2569. }
  2570. // 3) Enforce single-open invariant by closing/disposing the parent first
  2571. // This ensures we never have >1 tasks open at any time during delegation.
  2572. // Await abort completion to ensure clean disposal and prevent unhandled rejections.
  2573. try {
  2574. await this.removeClineFromStack()
  2575. } catch (error) {
  2576. this.log(
  2577. `[delegateParentAndOpenChild] Error during parent disposal (non-fatal): ${
  2578. error instanceof Error ? error.message : String(error)
  2579. }`,
  2580. )
  2581. // Non-fatal: proceed with child creation even if parent cleanup had issues
  2582. }
  2583. // 3) Switch provider mode to child's requested mode BEFORE creating the child task
  2584. // This ensures the child's system prompt and configuration are based on the correct mode.
  2585. // The mode switch must happen before createTask() because the Task constructor
  2586. // initializes its mode from provider.getState() during initializeTaskMode().
  2587. try {
  2588. await this.handleModeSwitch(mode as any)
  2589. } catch (e) {
  2590. this.log(
  2591. `[delegateParentAndOpenChild] handleModeSwitch failed for mode '${mode}': ${
  2592. (e as Error)?.message ?? String(e)
  2593. }`,
  2594. )
  2595. }
  2596. // 4) Create child as sole active (parent reference preserved for lineage)
  2597. // Pass initialStatus: "active" to ensure the child task's historyItem is created
  2598. // with status from the start, avoiding race conditions where the task might
  2599. // call attempt_completion before status is persisted separately.
  2600. const child = await this.createTask(message, undefined, parent as any, {
  2601. initialTodos,
  2602. initialStatus: "active",
  2603. })
  2604. // 5) Persist parent delegation metadata
  2605. try {
  2606. const { historyItem } = await this.getTaskWithId(parentTaskId)
  2607. const childIds = Array.from(new Set([...(historyItem.childIds ?? []), child.taskId]))
  2608. const updatedHistory: typeof historyItem = {
  2609. ...historyItem,
  2610. status: "delegated",
  2611. delegatedToId: child.taskId,
  2612. awaitingChildId: child.taskId,
  2613. childIds,
  2614. }
  2615. await this.updateTaskHistory(updatedHistory)
  2616. } catch (err) {
  2617. this.log(
  2618. `[delegateParentAndOpenChild] Failed to persist parent metadata for ${parentTaskId} -> ${child.taskId}: ${
  2619. (err as Error)?.message ?? String(err)
  2620. }`,
  2621. )
  2622. }
  2623. // 6) Emit TaskDelegated (provider-level)
  2624. try {
  2625. this.emit(RooCodeEventName.TaskDelegated, parentTaskId, child.taskId)
  2626. } catch {
  2627. // non-fatal
  2628. }
  2629. return child
  2630. }
  2631. /**
  2632. * Reopen parent task from delegation with write-back and events.
  2633. */
  2634. public async reopenParentFromDelegation(params: {
  2635. parentTaskId: string
  2636. childTaskId: string
  2637. completionResultSummary: string
  2638. }): Promise<void> {
  2639. const { parentTaskId, childTaskId, completionResultSummary } = params
  2640. const globalStoragePath = this.contextProxy.globalStorageUri.fsPath
  2641. // 1) Load parent from history and current persisted messages
  2642. const { historyItem } = await this.getTaskWithId(parentTaskId)
  2643. let parentClineMessages: ClineMessage[] = []
  2644. try {
  2645. parentClineMessages = await readTaskMessages({
  2646. taskId: parentTaskId,
  2647. globalStoragePath,
  2648. })
  2649. } catch {
  2650. parentClineMessages = []
  2651. }
  2652. let parentApiMessages: any[] = []
  2653. try {
  2654. parentApiMessages = (await readApiMessages({
  2655. taskId: parentTaskId,
  2656. globalStoragePath,
  2657. })) as any[]
  2658. } catch {
  2659. parentApiMessages = []
  2660. }
  2661. // 2) Inject synthetic records: UI subtask_result and update API tool_result
  2662. const ts = Date.now()
  2663. // Defensive: ensure arrays
  2664. if (!Array.isArray(parentClineMessages)) parentClineMessages = []
  2665. if (!Array.isArray(parentApiMessages)) parentApiMessages = []
  2666. const subtaskUiMessage: ClineMessage = {
  2667. type: "say",
  2668. say: "subtask_result",
  2669. text: completionResultSummary,
  2670. ts,
  2671. }
  2672. parentClineMessages.push(subtaskUiMessage)
  2673. await saveTaskMessages({ messages: parentClineMessages, taskId: parentTaskId, globalStoragePath })
  2674. // Find the tool_use_id from the last assistant message's new_task tool_use
  2675. let toolUseId: string | undefined
  2676. for (let i = parentApiMessages.length - 1; i >= 0; i--) {
  2677. const msg = parentApiMessages[i]
  2678. if (msg.role === "assistant" && Array.isArray(msg.content)) {
  2679. for (const block of msg.content) {
  2680. if (block.type === "tool_use" && block.name === "new_task") {
  2681. toolUseId = block.id
  2682. break
  2683. }
  2684. }
  2685. if (toolUseId) break
  2686. }
  2687. }
  2688. // The API expects: user → assistant (with tool_use) → user (with tool_result)
  2689. // We need to add a NEW user message with the tool_result AFTER the assistant's tool_use
  2690. // NOT add it to an existing user message
  2691. if (toolUseId) {
  2692. // Check if the last message is already a user message with a tool_result for this tool_use_id
  2693. // (in case this is a retry or the history was already updated)
  2694. const lastMsg = parentApiMessages[parentApiMessages.length - 1]
  2695. let alreadyHasToolResult = false
  2696. if (lastMsg?.role === "user" && Array.isArray(lastMsg.content)) {
  2697. for (const block of lastMsg.content) {
  2698. if (block.type === "tool_result" && block.tool_use_id === toolUseId) {
  2699. // Update the existing tool_result content
  2700. block.content = `Subtask ${childTaskId} completed.\n\nResult:\n${completionResultSummary}`
  2701. alreadyHasToolResult = true
  2702. break
  2703. }
  2704. }
  2705. }
  2706. // If no existing tool_result found, create a NEW user message with the tool_result
  2707. if (!alreadyHasToolResult) {
  2708. parentApiMessages.push({
  2709. role: "user",
  2710. content: [
  2711. {
  2712. type: "tool_result" as const,
  2713. tool_use_id: toolUseId,
  2714. content: `Subtask ${childTaskId} completed.\n\nResult:\n${completionResultSummary}`,
  2715. },
  2716. ],
  2717. ts,
  2718. })
  2719. }
  2720. } else {
  2721. // Fallback for XML protocol or when toolUseId couldn't be found:
  2722. // Add a text block (not ideal but maintains backward compatibility)
  2723. parentApiMessages.push({
  2724. role: "user",
  2725. content: [
  2726. {
  2727. type: "text",
  2728. text: `Subtask ${childTaskId} completed.\n\nResult:\n${completionResultSummary}`,
  2729. },
  2730. ],
  2731. ts,
  2732. })
  2733. }
  2734. await saveApiMessages({ messages: parentApiMessages as any, taskId: parentTaskId, globalStoragePath })
  2735. // 3) Update child metadata to "completed" status
  2736. try {
  2737. const { historyItem: childHistory } = await this.getTaskWithId(childTaskId)
  2738. await this.updateTaskHistory({
  2739. ...childHistory,
  2740. status: "completed",
  2741. })
  2742. } catch (err) {
  2743. this.log(
  2744. `[reopenParentFromDelegation] Failed to persist child completed status for ${childTaskId}: ${
  2745. (err as Error)?.message ?? String(err)
  2746. }`,
  2747. )
  2748. }
  2749. // 4) Update parent metadata and persist BEFORE emitting completion event
  2750. const childIds = Array.from(new Set([...(historyItem.childIds ?? []), childTaskId]))
  2751. const updatedHistory: typeof historyItem = {
  2752. ...historyItem,
  2753. status: "active",
  2754. completedByChildId: childTaskId,
  2755. completionResultSummary,
  2756. awaitingChildId: undefined,
  2757. childIds,
  2758. }
  2759. await this.updateTaskHistory(updatedHistory)
  2760. // 5) Emit TaskDelegationCompleted (provider-level)
  2761. try {
  2762. this.emit(RooCodeEventName.TaskDelegationCompleted, parentTaskId, childTaskId, completionResultSummary)
  2763. } catch {
  2764. // non-fatal
  2765. }
  2766. // 6) Close child instance if still open (single-open-task invariant)
  2767. const current = this.getCurrentTask()
  2768. if (current?.taskId === childTaskId) {
  2769. await this.removeClineFromStack()
  2770. }
  2771. // 7) Reopen the parent from history as the sole active task (restores saved mode)
  2772. // IMPORTANT: startTask=false to suppress resume-from-history ask scheduling
  2773. const parentInstance = await this.createTaskWithHistoryItem(updatedHistory, { startTask: false })
  2774. // 8) Inject restored histories into the in-memory instance before resuming
  2775. if (parentInstance) {
  2776. try {
  2777. await parentInstance.overwriteClineMessages(parentClineMessages)
  2778. } catch {
  2779. // non-fatal
  2780. }
  2781. try {
  2782. await parentInstance.overwriteApiConversationHistory(parentApiMessages as any)
  2783. } catch {
  2784. // non-fatal
  2785. }
  2786. // Auto-resume parent without ask("resume_task")
  2787. await parentInstance.resumeAfterDelegation()
  2788. }
  2789. // 9) Emit TaskDelegationResumed (provider-level)
  2790. try {
  2791. this.emit(RooCodeEventName.TaskDelegationResumed, parentTaskId, childTaskId)
  2792. } catch {
  2793. // non-fatal
  2794. }
  2795. }
  2796. /**
  2797. * Convert a file path to a webview-accessible URI
  2798. * This method safely converts file paths to URIs that can be loaded in the webview
  2799. *
  2800. * @param filePath - The absolute file path to convert
  2801. * @returns The webview URI string, or the original file URI if conversion fails
  2802. * @throws {Error} When webview is not available
  2803. * @throws {TypeError} When file path is invalid
  2804. */
  2805. public convertToWebviewUri(filePath: string): string {
  2806. try {
  2807. const fileUri = vscode.Uri.file(filePath)
  2808. // Check if we have a webview available
  2809. if (this.view?.webview) {
  2810. const webviewUri = this.view.webview.asWebviewUri(fileUri)
  2811. return webviewUri.toString()
  2812. }
  2813. // Specific error for no webview available
  2814. const error = new Error("No webview available for URI conversion")
  2815. console.error(error.message)
  2816. // Fallback to file URI if no webview available
  2817. return fileUri.toString()
  2818. } catch (error) {
  2819. // More specific error handling
  2820. if (error instanceof TypeError) {
  2821. console.error("Invalid file path provided for URI conversion:", error)
  2822. } else {
  2823. console.error("Failed to convert to webview URI:", error)
  2824. }
  2825. // Return file URI as fallback
  2826. return vscode.Uri.file(filePath).toString()
  2827. }
  2828. }
  2829. }