| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263 |
- import os from "os"
- import * as path from "path"
- import fs from "fs/promises"
- import EventEmitter from "events"
- import { Anthropic } from "@anthropic-ai/sdk"
- import delay from "delay"
- import axios from "axios"
- import pWaitFor from "p-wait-for"
- import * as vscode from "vscode"
- import {
- type TaskProviderLike,
- type TaskProviderEvents,
- type GlobalState,
- type ProviderName,
- type ProviderSettings,
- type RooCodeSettings,
- type ProviderSettingsEntry,
- type StaticAppProperties,
- type DynamicAppProperties,
- type CloudAppProperties,
- type TaskProperties,
- type GitProperties,
- type TelemetryProperties,
- type TelemetryPropertiesProvider,
- type CodeActionId,
- type CodeActionName,
- type TerminalActionId,
- type TerminalActionPromptType,
- type HistoryItem,
- type CloudUserInfo,
- type CloudOrganizationMembership,
- type CreateTaskOptions,
- type TokenUsage,
- type ToolUsage,
- RooCodeEventName,
- requestyDefaultModelId,
- openRouterDefaultModelId,
- DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT,
- DEFAULT_WRITE_DELAY_MS,
- ORGANIZATION_ALLOW_ALL,
- DEFAULT_MODES,
- DEFAULT_CHECKPOINT_TIMEOUT_SECONDS,
- getModelId,
- } from "@roo-code/types"
- import { TelemetryService } from "@roo-code/telemetry"
- import { CloudService, BridgeOrchestrator, getRooCodeApiUrl } from "@roo-code/cloud"
- import { Package } from "../../shared/package"
- import { findLast } from "../../shared/array"
- import { supportPrompt } from "../../shared/support-prompt"
- import { GlobalFileNames } from "../../shared/globalFileNames"
- import type { ExtensionMessage, ExtensionState, MarketplaceInstalledMetadata } from "../../shared/ExtensionMessage"
- import { Mode, defaultModeSlug, getModeBySlug } from "../../shared/modes"
- import { experimentDefault } from "../../shared/experiments"
- import { formatLanguage } from "../../shared/language"
- import { WebviewMessage } from "../../shared/WebviewMessage"
- import { EMBEDDING_MODEL_PROFILES } from "../../shared/embeddingModels"
- import { ProfileValidator } from "../../shared/ProfileValidator"
- import { Terminal } from "../../integrations/terminal/Terminal"
- import { downloadTask } from "../../integrations/misc/export-markdown"
- import { getTheme } from "../../integrations/theme/getTheme"
- import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
- import { McpHub } from "../../services/mcp/McpHub"
- import { McpServerManager } from "../../services/mcp/McpServerManager"
- import { MarketplaceManager } from "../../services/marketplace"
- import { ShadowCheckpointService } from "../../services/checkpoints/ShadowCheckpointService"
- import { CodeIndexManager } from "../../services/code-index/manager"
- import type { IndexProgressUpdate } from "../../services/code-index/interfaces/manager"
- import { MdmService } from "../../services/mdm/MdmService"
- import { fileExistsAtPath } from "../../utils/fs"
- import { setTtsEnabled, setTtsSpeed } from "../../utils/tts"
- import { getWorkspaceGitInfo } from "../../utils/git"
- import { getWorkspacePath } from "../../utils/path"
- import { OrganizationAllowListViolationError } from "../../utils/errors"
- import { setPanel } from "../../activate/registerCommands"
- import { t } from "../../i18n"
- import { buildApiHandler } from "../../api"
- import { forceFullModelDetailsLoad, hasLoadedFullDetails } from "../../api/providers/fetchers/lmstudio"
- import { ContextProxy } from "../config/ContextProxy"
- import { ProviderSettingsManager } from "../config/ProviderSettingsManager"
- import { CustomModesManager } from "../config/CustomModesManager"
- import { Task } from "../task/Task"
- import { getSystemPromptFilePath } from "../prompts/sections/custom-system-prompt"
- import { webviewMessageHandler } from "./webviewMessageHandler"
- import type { ClineMessage, TodoItem } from "@roo-code/types"
- import { readApiMessages, saveApiMessages, saveTaskMessages } from "../task-persistence"
- import { readTaskMessages } from "../task-persistence/taskMessages"
- import { getNonce } from "./getNonce"
- import { getUri } from "./getUri"
- import { REQUESTY_BASE_URL } from "../../shared/utils/requesty"
- /**
- * https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
- * https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/customSidebarViewProvider.ts
- */
- export type ClineProviderEvents = {
- clineCreated: [cline: Task]
- }
- interface PendingEditOperation {
- messageTs: number
- editedContent: string
- images?: string[]
- messageIndex: number
- apiConversationHistoryIndex: number
- timeoutId: NodeJS.Timeout
- createdAt: number
- }
- export class ClineProvider
- extends EventEmitter<TaskProviderEvents>
- implements vscode.WebviewViewProvider, TelemetryPropertiesProvider, TaskProviderLike
- {
- // Used in package.json as the view's id. This value cannot be changed due
- // to how VSCode caches views based on their id, and updating the id would
- // break existing instances of the extension.
- public static readonly sideBarId = `${Package.name}.SidebarProvider`
- public static readonly tabPanelId = `${Package.name}.TabPanelProvider`
- private static activeInstances: Set<ClineProvider> = new Set()
- private disposables: vscode.Disposable[] = []
- private webviewDisposables: vscode.Disposable[] = []
- private view?: vscode.WebviewView | vscode.WebviewPanel
- private clineStack: Task[] = []
- private codeIndexStatusSubscription?: vscode.Disposable
- private codeIndexManager?: CodeIndexManager
- private _workspaceTracker?: WorkspaceTracker // workSpaceTracker read-only for access outside this class
- protected mcpHub?: McpHub // Change from private to protected
- private marketplaceManager: MarketplaceManager
- private mdmService?: MdmService
- private taskCreationCallback: (task: Task) => void
- private taskEventListeners: WeakMap<Task, Array<() => void>> = new WeakMap()
- private currentWorkspacePath: string | undefined
- private recentTasksCache?: string[]
- private pendingOperations: Map<string, PendingEditOperation> = new Map()
- private static readonly PENDING_OPERATION_TIMEOUT_MS = 30000 // 30 seconds
- private cloudOrganizationsCache: CloudOrganizationMembership[] | null = null
- private cloudOrganizationsCacheTimestamp: number | null = null
- private static readonly CLOUD_ORGANIZATIONS_CACHE_DURATION_MS = 5 * 1000 // 5 seconds
- public isViewLaunched = false
- public settingsImportedAt?: number
- public readonly latestAnnouncementId = "dec-2025-v3.36.0-context-rewind-roo-provider" // v3.36.0 Context Rewind & Roo Provider Improvements
- public readonly providerSettingsManager: ProviderSettingsManager
- public readonly customModesManager: CustomModesManager
- constructor(
- readonly context: vscode.ExtensionContext,
- private readonly outputChannel: vscode.OutputChannel,
- private readonly renderContext: "sidebar" | "editor" = "sidebar",
- public readonly contextProxy: ContextProxy,
- mdmService?: MdmService,
- ) {
- super()
- this.currentWorkspacePath = getWorkspacePath()
- ClineProvider.activeInstances.add(this)
- this.mdmService = mdmService
- this.updateGlobalState("codebaseIndexModels", EMBEDDING_MODEL_PROFILES)
- // Start configuration loading (which might trigger indexing) in the background.
- // Don't await, allowing activation to continue immediately.
- // Register this provider with the telemetry service to enable it to add
- // properties like mode and provider.
- TelemetryService.instance.setProvider(this)
- this._workspaceTracker = new WorkspaceTracker(this)
- this.providerSettingsManager = new ProviderSettingsManager(this.context)
- this.customModesManager = new CustomModesManager(this.context, async () => {
- await this.postStateToWebview()
- })
- // Initialize MCP Hub through the singleton manager
- McpServerManager.getInstance(this.context, this)
- .then((hub) => {
- this.mcpHub = hub
- this.mcpHub.registerClient()
- })
- .catch((error) => {
- this.log(`Failed to initialize MCP Hub: ${error}`)
- })
- this.marketplaceManager = new MarketplaceManager(this.context, this.customModesManager)
- // Forward <most> task events to the provider.
- // We do something fairly similar for the IPC-based API.
- this.taskCreationCallback = (instance: Task) => {
- this.emit(RooCodeEventName.TaskCreated, instance)
- // Create named listener functions so we can remove them later.
- const onTaskStarted = () => this.emit(RooCodeEventName.TaskStarted, instance.taskId)
- const onTaskCompleted = (taskId: string, tokenUsage: TokenUsage, toolUsage: ToolUsage) =>
- this.emit(RooCodeEventName.TaskCompleted, taskId, tokenUsage, toolUsage)
- const onTaskAborted = async () => {
- this.emit(RooCodeEventName.TaskAborted, instance.taskId)
- try {
- // Only rehydrate on genuine streaming failures.
- // User-initiated cancels are handled by cancelTask().
- if (instance.abortReason === "streaming_failed") {
- // Defensive safeguard: if another path already replaced this instance, skip
- const current = this.getCurrentTask()
- if (current && current.instanceId !== instance.instanceId) {
- this.log(
- `[onTaskAborted] Skipping rehydrate: current instance ${current.instanceId} != aborted ${instance.instanceId}`,
- )
- return
- }
- const { historyItem } = await this.getTaskWithId(instance.taskId)
- const rootTask = instance.rootTask
- const parentTask = instance.parentTask
- await this.createTaskWithHistoryItem({ ...historyItem, rootTask, parentTask })
- }
- } catch (error) {
- this.log(
- `[onTaskAborted] Failed to rehydrate after streaming failure: ${
- error instanceof Error ? error.message : String(error)
- }`,
- )
- }
- }
- const onTaskFocused = () => this.emit(RooCodeEventName.TaskFocused, instance.taskId)
- const onTaskUnfocused = () => this.emit(RooCodeEventName.TaskUnfocused, instance.taskId)
- const onTaskActive = (taskId: string) => this.emit(RooCodeEventName.TaskActive, taskId)
- const onTaskInteractive = (taskId: string) => this.emit(RooCodeEventName.TaskInteractive, taskId)
- const onTaskResumable = (taskId: string) => this.emit(RooCodeEventName.TaskResumable, taskId)
- const onTaskIdle = (taskId: string) => this.emit(RooCodeEventName.TaskIdle, taskId)
- const onTaskPaused = (taskId: string) => this.emit(RooCodeEventName.TaskPaused, taskId)
- const onTaskUnpaused = (taskId: string) => this.emit(RooCodeEventName.TaskUnpaused, taskId)
- const onTaskSpawned = (taskId: string) => this.emit(RooCodeEventName.TaskSpawned, taskId)
- const onTaskUserMessage = (taskId: string) => this.emit(RooCodeEventName.TaskUserMessage, taskId)
- const onTaskTokenUsageUpdated = (taskId: string, tokenUsage: TokenUsage, toolUsage: ToolUsage) =>
- this.emit(RooCodeEventName.TaskTokenUsageUpdated, taskId, tokenUsage, toolUsage)
- // Attach the listeners.
- instance.on(RooCodeEventName.TaskStarted, onTaskStarted)
- instance.on(RooCodeEventName.TaskCompleted, onTaskCompleted)
- instance.on(RooCodeEventName.TaskAborted, onTaskAborted)
- instance.on(RooCodeEventName.TaskFocused, onTaskFocused)
- instance.on(RooCodeEventName.TaskUnfocused, onTaskUnfocused)
- instance.on(RooCodeEventName.TaskActive, onTaskActive)
- instance.on(RooCodeEventName.TaskInteractive, onTaskInteractive)
- instance.on(RooCodeEventName.TaskResumable, onTaskResumable)
- instance.on(RooCodeEventName.TaskIdle, onTaskIdle)
- instance.on(RooCodeEventName.TaskPaused, onTaskPaused)
- instance.on(RooCodeEventName.TaskUnpaused, onTaskUnpaused)
- instance.on(RooCodeEventName.TaskSpawned, onTaskSpawned)
- instance.on(RooCodeEventName.TaskUserMessage, onTaskUserMessage)
- instance.on(RooCodeEventName.TaskTokenUsageUpdated, onTaskTokenUsageUpdated)
- // Store the cleanup functions for later removal.
- this.taskEventListeners.set(instance, [
- () => instance.off(RooCodeEventName.TaskStarted, onTaskStarted),
- () => instance.off(RooCodeEventName.TaskCompleted, onTaskCompleted),
- () => instance.off(RooCodeEventName.TaskAborted, onTaskAborted),
- () => instance.off(RooCodeEventName.TaskFocused, onTaskFocused),
- () => instance.off(RooCodeEventName.TaskUnfocused, onTaskUnfocused),
- () => instance.off(RooCodeEventName.TaskActive, onTaskActive),
- () => instance.off(RooCodeEventName.TaskInteractive, onTaskInteractive),
- () => instance.off(RooCodeEventName.TaskResumable, onTaskResumable),
- () => instance.off(RooCodeEventName.TaskIdle, onTaskIdle),
- () => instance.off(RooCodeEventName.TaskUserMessage, onTaskUserMessage),
- () => instance.off(RooCodeEventName.TaskPaused, onTaskPaused),
- () => instance.off(RooCodeEventName.TaskUnpaused, onTaskUnpaused),
- () => instance.off(RooCodeEventName.TaskSpawned, onTaskSpawned),
- () => instance.off(RooCodeEventName.TaskTokenUsageUpdated, onTaskTokenUsageUpdated),
- ])
- }
- // Initialize Roo Code Cloud profile sync.
- if (CloudService.hasInstance()) {
- this.initializeCloudProfileSync().catch((error) => {
- this.log(`Failed to initialize cloud profile sync: ${error}`)
- })
- } else {
- this.log("CloudService not ready, deferring cloud profile sync")
- }
- }
- /**
- * Override EventEmitter's on method to match TaskProviderLike interface
- */
- override on<K extends keyof TaskProviderEvents>(
- event: K,
- listener: (...args: TaskProviderEvents[K]) => void | Promise<void>,
- ): this {
- return super.on(event, listener as any)
- }
- /**
- * Override EventEmitter's off method to match TaskProviderLike interface
- */
- override off<K extends keyof TaskProviderEvents>(
- event: K,
- listener: (...args: TaskProviderEvents[K]) => void | Promise<void>,
- ): this {
- return super.off(event, listener as any)
- }
- /**
- * Initialize cloud profile synchronization
- */
- private async initializeCloudProfileSync() {
- try {
- // Check if authenticated and sync profiles
- if (CloudService.hasInstance() && CloudService.instance.isAuthenticated()) {
- await this.syncCloudProfiles()
- }
- // Set up listener for future updates
- if (CloudService.hasInstance()) {
- CloudService.instance.on("settings-updated", this.handleCloudSettingsUpdate)
- }
- } catch (error) {
- this.log(`Error in initializeCloudProfileSync: ${error}`)
- }
- }
- /**
- * Handle cloud settings updates
- */
- private handleCloudSettingsUpdate = async () => {
- try {
- await this.syncCloudProfiles()
- } catch (error) {
- this.log(`Error handling cloud settings update: ${error}`)
- }
- }
- /**
- * Synchronize cloud profiles with local profiles.
- */
- private async syncCloudProfiles() {
- try {
- const settings = CloudService.instance.getOrganizationSettings()
- if (!settings?.providerProfiles) {
- return
- }
- const currentApiConfigName = this.getGlobalState("currentApiConfigName")
- const result = await this.providerSettingsManager.syncCloudProfiles(
- settings.providerProfiles,
- currentApiConfigName,
- )
- if (result.hasChanges) {
- // Update list.
- await this.updateGlobalState("listApiConfigMeta", await this.providerSettingsManager.listConfig())
- if (result.activeProfileChanged && result.activeProfileId) {
- // Reload full settings for new active profile.
- const profile = await this.providerSettingsManager.getProfile({
- id: result.activeProfileId,
- })
- await this.activateProviderProfile({ name: profile.name })
- }
- await this.postStateToWebview()
- }
- } catch (error) {
- this.log(`Error syncing cloud profiles: ${error}`)
- }
- }
- /**
- * Initialize cloud profile synchronization when CloudService is ready
- * This method is called externally after CloudService has been initialized
- */
- public async initializeCloudProfileSyncWhenReady(): Promise<void> {
- try {
- if (CloudService.hasInstance() && CloudService.instance.isAuthenticated()) {
- await this.syncCloudProfiles()
- }
- if (CloudService.hasInstance()) {
- CloudService.instance.off("settings-updated", this.handleCloudSettingsUpdate)
- CloudService.instance.on("settings-updated", this.handleCloudSettingsUpdate)
- }
- } catch (error) {
- this.log(`Failed to initialize cloud profile sync when ready: ${error}`)
- }
- }
- // Adds a new Task instance to clineStack, marking the start of a new task.
- // The instance is pushed to the top of the stack (LIFO order).
- // When the task is completed, the top instance is removed, reactivating the
- // previous task.
- async addClineToStack(task: Task) {
- // Add this cline instance into the stack that represents the order of
- // all the called tasks.
- this.clineStack.push(task)
- task.emit(RooCodeEventName.TaskFocused)
- // Perform special setup provider specific tasks.
- await this.performPreparationTasks(task)
- // Ensure getState() resolves correctly.
- const state = await this.getState()
- if (!state || typeof state.mode !== "string") {
- throw new Error(t("common:errors.retrieve_current_mode"))
- }
- }
- async performPreparationTasks(cline: Task) {
- // LMStudio: We need to force model loading in order to read its context
- // size; we do it now since we're starting a task with that model selected.
- if (cline.apiConfiguration && cline.apiConfiguration.apiProvider === "lmstudio") {
- try {
- if (!hasLoadedFullDetails(cline.apiConfiguration.lmStudioModelId!)) {
- await forceFullModelDetailsLoad(
- cline.apiConfiguration.lmStudioBaseUrl ?? "http://localhost:1234",
- cline.apiConfiguration.lmStudioModelId!,
- )
- }
- } catch (error) {
- this.log(`Failed to load full model details for LM Studio: ${error}`)
- vscode.window.showErrorMessage(error.message)
- }
- }
- }
- // Removes and destroys the top Cline instance (the current finished task),
- // activating the previous one (resuming the parent task).
- async removeClineFromStack() {
- if (this.clineStack.length === 0) {
- return
- }
- // Pop the top Cline instance from the stack.
- let task = this.clineStack.pop()
- if (task) {
- task.emit(RooCodeEventName.TaskUnfocused)
- try {
- // Abort the running task and set isAbandoned to true so
- // all running promises will exit as well.
- await task.abortTask(true)
- } catch (e) {
- this.log(
- `[ClineProvider#removeClineFromStack] abortTask() failed ${task.taskId}.${task.instanceId}: ${e.message}`,
- )
- }
- // Remove event listeners before clearing the reference.
- const cleanupFunctions = this.taskEventListeners.get(task)
- if (cleanupFunctions) {
- cleanupFunctions.forEach((cleanup) => cleanup())
- this.taskEventListeners.delete(task)
- }
- // Make sure no reference kept, once promises end it will be
- // garbage collected.
- task = undefined
- }
- }
- getTaskStackSize(): number {
- return this.clineStack.length
- }
- public getCurrentTaskStack(): string[] {
- return this.clineStack.map((cline) => cline.taskId)
- }
- // Pending Edit Operations Management
- /**
- * Sets a pending edit operation with automatic timeout cleanup
- */
- public setPendingEditOperation(
- operationId: string,
- editData: {
- messageTs: number
- editedContent: string
- images?: string[]
- messageIndex: number
- apiConversationHistoryIndex: number
- },
- ): void {
- // Clear any existing operation with the same ID
- this.clearPendingEditOperation(operationId)
- // Create timeout for automatic cleanup
- const timeoutId = setTimeout(() => {
- this.clearPendingEditOperation(operationId)
- this.log(`[setPendingEditOperation] Automatically cleared stale pending operation: ${operationId}`)
- }, ClineProvider.PENDING_OPERATION_TIMEOUT_MS)
- // Store the operation
- this.pendingOperations.set(operationId, {
- ...editData,
- timeoutId,
- createdAt: Date.now(),
- })
- this.log(`[setPendingEditOperation] Set pending operation: ${operationId}`)
- }
- /**
- * Gets a pending edit operation by ID
- */
- private getPendingEditOperation(operationId: string): PendingEditOperation | undefined {
- return this.pendingOperations.get(operationId)
- }
- /**
- * Clears a specific pending edit operation
- */
- private clearPendingEditOperation(operationId: string): boolean {
- const operation = this.pendingOperations.get(operationId)
- if (operation) {
- clearTimeout(operation.timeoutId)
- this.pendingOperations.delete(operationId)
- this.log(`[clearPendingEditOperation] Cleared pending operation: ${operationId}`)
- return true
- }
- return false
- }
- /**
- * Clears all pending edit operations
- */
- private clearAllPendingEditOperations(): void {
- for (const [operationId, operation] of this.pendingOperations) {
- clearTimeout(operation.timeoutId)
- }
- this.pendingOperations.clear()
- this.log(`[clearAllPendingEditOperations] Cleared all pending operations`)
- }
- /*
- 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.
- - https://vscode-docs.readthedocs.io/en/stable/extensions/patterns-and-principles/
- - https://github.com/microsoft/vscode-extension-samples/blob/main/webview-sample/src/extension.ts
- */
- private clearWebviewResources() {
- while (this.webviewDisposables.length) {
- const x = this.webviewDisposables.pop()
- if (x) {
- x.dispose()
- }
- }
- }
- async dispose() {
- this.log("Disposing ClineProvider...")
- // Clear all tasks from the stack.
- while (this.clineStack.length > 0) {
- await this.removeClineFromStack()
- }
- this.log("Cleared all tasks")
- // Clear all pending edit operations to prevent memory leaks
- this.clearAllPendingEditOperations()
- this.log("Cleared pending operations")
- if (this.view && "dispose" in this.view) {
- this.view.dispose()
- this.log("Disposed webview")
- }
- this.clearWebviewResources()
- // Clean up cloud service event listener
- if (CloudService.hasInstance()) {
- CloudService.instance.off("settings-updated", this.handleCloudSettingsUpdate)
- }
- while (this.disposables.length) {
- const x = this.disposables.pop()
- if (x) {
- x.dispose()
- }
- }
- this._workspaceTracker?.dispose()
- this._workspaceTracker = undefined
- await this.mcpHub?.unregisterClient()
- this.mcpHub = undefined
- this.marketplaceManager?.cleanup()
- this.customModesManager?.dispose()
- this.log("Disposed all disposables")
- ClineProvider.activeInstances.delete(this)
- // Clean up any event listeners attached to this provider
- this.removeAllListeners()
- McpServerManager.unregisterProvider(this)
- }
- public static getVisibleInstance(): ClineProvider | undefined {
- return findLast(Array.from(this.activeInstances), (instance) => instance.view?.visible === true)
- }
- public static async getInstance(): Promise<ClineProvider | undefined> {
- let visibleProvider = ClineProvider.getVisibleInstance()
- // If no visible provider, try to show the sidebar view
- if (!visibleProvider) {
- await vscode.commands.executeCommand(`${Package.name}.SidebarProvider.focus`)
- // Wait briefly for the view to become visible
- await delay(100)
- visibleProvider = ClineProvider.getVisibleInstance()
- }
- // If still no visible provider, return
- if (!visibleProvider) {
- return
- }
- return visibleProvider
- }
- public static async isActiveTask(): Promise<boolean> {
- const visibleProvider = await ClineProvider.getInstance()
- if (!visibleProvider) {
- return false
- }
- // Check if there is a cline instance in the stack (if this provider has an active task)
- if (visibleProvider.getCurrentTask()) {
- return true
- }
- return false
- }
- public static async handleCodeAction(
- command: CodeActionId,
- promptType: CodeActionName,
- params: Record<string, string | any[]>,
- ): Promise<void> {
- // Capture telemetry for code action usage
- TelemetryService.instance.captureCodeActionUsed(promptType)
- const visibleProvider = await ClineProvider.getInstance()
- if (!visibleProvider) {
- return
- }
- const { customSupportPrompts } = await visibleProvider.getState()
- // TODO: Improve type safety for promptType.
- const prompt = supportPrompt.create(promptType, params, customSupportPrompts)
- if (command === "addToContext") {
- await visibleProvider.postMessageToWebview({
- type: "invoke",
- invoke: "setChatBoxMessage",
- text: `${prompt}\n\n`,
- })
- await visibleProvider.postMessageToWebview({ type: "action", action: "focusInput" })
- return
- }
- await visibleProvider.createTask(prompt)
- }
- public static async handleTerminalAction(
- command: TerminalActionId,
- promptType: TerminalActionPromptType,
- params: Record<string, string | any[]>,
- ): Promise<void> {
- TelemetryService.instance.captureCodeActionUsed(promptType)
- const visibleProvider = await ClineProvider.getInstance()
- if (!visibleProvider) {
- return
- }
- const { customSupportPrompts } = await visibleProvider.getState()
- const prompt = supportPrompt.create(promptType, params, customSupportPrompts)
- if (command === "terminalAddToContext") {
- await visibleProvider.postMessageToWebview({
- type: "invoke",
- invoke: "setChatBoxMessage",
- text: `${prompt}\n\n`,
- })
- await visibleProvider.postMessageToWebview({ type: "action", action: "focusInput" })
- return
- }
- try {
- await visibleProvider.createTask(prompt)
- } catch (error) {
- if (error instanceof OrganizationAllowListViolationError) {
- // Errors from terminal commands seem to get swallowed / ignored.
- vscode.window.showErrorMessage(error.message)
- }
- throw error
- }
- }
- async resolveWebviewView(webviewView: vscode.WebviewView | vscode.WebviewPanel) {
- this.view = webviewView
- const inTabMode = "onDidChangeViewState" in webviewView
- if (inTabMode) {
- setPanel(webviewView, "tab")
- } else if ("onDidChangeVisibility" in webviewView) {
- setPanel(webviewView, "sidebar")
- }
- // Initialize out-of-scope variables that need to receive persistent
- // global state values.
- this.getState().then(
- ({
- terminalShellIntegrationTimeout = Terminal.defaultShellIntegrationTimeout,
- terminalShellIntegrationDisabled = false,
- terminalCommandDelay = 0,
- terminalZshClearEolMark = true,
- terminalZshOhMy = false,
- terminalZshP10k = false,
- terminalPowershellCounter = false,
- terminalZdotdir = false,
- }) => {
- Terminal.setShellIntegrationTimeout(terminalShellIntegrationTimeout)
- Terminal.setShellIntegrationDisabled(terminalShellIntegrationDisabled)
- Terminal.setCommandDelay(terminalCommandDelay)
- Terminal.setTerminalZshClearEolMark(terminalZshClearEolMark)
- Terminal.setTerminalZshOhMy(terminalZshOhMy)
- Terminal.setTerminalZshP10k(terminalZshP10k)
- Terminal.setPowershellCounter(terminalPowershellCounter)
- Terminal.setTerminalZdotdir(terminalZdotdir)
- },
- )
- this.getState().then(({ ttsEnabled }) => {
- setTtsEnabled(ttsEnabled ?? false)
- })
- this.getState().then(({ ttsSpeed }) => {
- setTtsSpeed(ttsSpeed ?? 1)
- })
- // Set up webview options with proper resource roots
- const resourceRoots = [this.contextProxy.extensionUri]
- // Add workspace folders to allow access to workspace files
- if (vscode.workspace.workspaceFolders) {
- resourceRoots.push(...vscode.workspace.workspaceFolders.map((folder) => folder.uri))
- }
- webviewView.webview.options = {
- enableScripts: true,
- localResourceRoots: resourceRoots,
- }
- webviewView.webview.html =
- this.contextProxy.extensionMode === vscode.ExtensionMode.Development
- ? await this.getHMRHtmlContent(webviewView.webview)
- : await this.getHtmlContent(webviewView.webview)
- // Sets up an event listener to listen for messages passed from the webview view context
- // and executes code based on the message that is received.
- this.setWebviewMessageListener(webviewView.webview)
- // Initialize code index status subscription for the current workspace.
- this.updateCodeIndexStatusSubscription()
- // Listen for active editor changes to update code index status for the
- // current workspace.
- const activeEditorSubscription = vscode.window.onDidChangeActiveTextEditor(() => {
- // Update subscription when workspace might have changed.
- this.updateCodeIndexStatusSubscription()
- })
- this.webviewDisposables.push(activeEditorSubscription)
- // Listen for when the panel becomes visible.
- // https://github.com/microsoft/vscode-discussions/discussions/840
- if ("onDidChangeViewState" in webviewView) {
- // WebviewView and WebviewPanel have all the same properties except
- // for this visibility listener panel.
- const viewStateDisposable = webviewView.onDidChangeViewState(() => {
- if (this.view?.visible) {
- this.postMessageToWebview({ type: "action", action: "didBecomeVisible" })
- }
- })
- this.webviewDisposables.push(viewStateDisposable)
- } else if ("onDidChangeVisibility" in webviewView) {
- // sidebar
- const visibilityDisposable = webviewView.onDidChangeVisibility(() => {
- if (this.view?.visible) {
- this.postMessageToWebview({ type: "action", action: "didBecomeVisible" })
- }
- })
- this.webviewDisposables.push(visibilityDisposable)
- }
- // Listen for when the view is disposed
- // This happens when the user closes the view or when the view is closed programmatically
- webviewView.onDidDispose(
- async () => {
- if (inTabMode) {
- this.log("Disposing ClineProvider instance for tab view")
- await this.dispose()
- } else {
- this.log("Clearing webview resources for sidebar view")
- this.clearWebviewResources()
- // Reset current workspace manager reference when view is disposed
- this.codeIndexManager = undefined
- }
- },
- null,
- this.disposables,
- )
- // Listen for when color changes
- const configDisposable = vscode.workspace.onDidChangeConfiguration(async (e) => {
- if (e && e.affectsConfiguration("workbench.colorTheme")) {
- // Sends latest theme name to webview
- await this.postMessageToWebview({ type: "theme", text: JSON.stringify(await getTheme()) })
- }
- })
- this.webviewDisposables.push(configDisposable)
- // If the extension is starting a new session, clear previous task state.
- await this.removeClineFromStack()
- }
- public async createTaskWithHistoryItem(
- historyItem: HistoryItem & { rootTask?: Task; parentTask?: Task },
- options?: { startTask?: boolean },
- ) {
- // Check if we're rehydrating the current task to avoid flicker
- const currentTask = this.getCurrentTask()
- const isRehydratingCurrentTask = currentTask && currentTask.taskId === historyItem.id
- if (!isRehydratingCurrentTask) {
- await this.removeClineFromStack()
- }
- // If the history item has a saved mode, restore it and its associated API configuration.
- if (historyItem.mode) {
- // Validate that the mode still exists
- const customModes = await this.customModesManager.getCustomModes()
- const modeExists = getModeBySlug(historyItem.mode, customModes) !== undefined
- if (!modeExists) {
- // Mode no longer exists, fall back to default mode.
- this.log(
- `Mode '${historyItem.mode}' from history no longer exists. Falling back to default mode '${defaultModeSlug}'.`,
- )
- historyItem.mode = defaultModeSlug
- }
- await this.updateGlobalState("mode", historyItem.mode)
- // Load the saved API config for the restored mode if it exists.
- const savedConfigId = await this.providerSettingsManager.getModeConfigId(historyItem.mode)
- const listApiConfig = await this.providerSettingsManager.listConfig()
- // Update listApiConfigMeta first to ensure UI has latest data.
- await this.updateGlobalState("listApiConfigMeta", listApiConfig)
- // If this mode has a saved config, use it.
- if (savedConfigId) {
- const profile = listApiConfig.find(({ id }) => id === savedConfigId)
- if (profile?.name) {
- try {
- await this.activateProviderProfile({ name: profile.name })
- } catch (error) {
- // Log the error but continue with task restoration.
- this.log(
- `Failed to restore API configuration for mode '${historyItem.mode}': ${
- error instanceof Error ? error.message : String(error)
- }. Continuing with default configuration.`,
- )
- // The task will continue with the current/default configuration.
- }
- }
- }
- }
- const {
- apiConfiguration,
- diffEnabled: enableDiff,
- enableCheckpoints,
- checkpointTimeout,
- fuzzyMatchThreshold,
- experiments,
- cloudUserInfo,
- taskSyncEnabled,
- } = await this.getState()
- const task = new Task({
- provider: this,
- apiConfiguration,
- enableDiff,
- enableCheckpoints,
- checkpointTimeout,
- fuzzyMatchThreshold,
- consecutiveMistakeLimit: apiConfiguration.consecutiveMistakeLimit,
- historyItem,
- experiments,
- rootTask: historyItem.rootTask,
- parentTask: historyItem.parentTask,
- taskNumber: historyItem.number,
- workspacePath: historyItem.workspace,
- onCreated: this.taskCreationCallback,
- startTask: options?.startTask ?? true,
- enableBridge: BridgeOrchestrator.isEnabled(cloudUserInfo, taskSyncEnabled),
- // Preserve the status from the history item to avoid overwriting it when the task saves messages
- initialStatus: historyItem.status,
- })
- if (isRehydratingCurrentTask) {
- // Replace the current task in-place to avoid UI flicker
- const stackIndex = this.clineStack.length - 1
- // Properly dispose of the old task to ensure garbage collection
- const oldTask = this.clineStack[stackIndex]
- // Abort the old task to stop running processes and mark as abandoned
- try {
- await oldTask.abortTask(true)
- } catch (e) {
- this.log(
- `[createTaskWithHistoryItem] abortTask() failed for old task ${oldTask.taskId}.${oldTask.instanceId}: ${e.message}`,
- )
- }
- // Remove event listeners from the old task
- const cleanupFunctions = this.taskEventListeners.get(oldTask)
- if (cleanupFunctions) {
- cleanupFunctions.forEach((cleanup) => cleanup())
- this.taskEventListeners.delete(oldTask)
- }
- // Replace the task in the stack
- this.clineStack[stackIndex] = task
- task.emit(RooCodeEventName.TaskFocused)
- // Perform preparation tasks and set up event listeners
- await this.performPreparationTasks(task)
- this.log(
- `[createTaskWithHistoryItem] rehydrated task ${task.taskId}.${task.instanceId} in-place (flicker-free)`,
- )
- } else {
- await this.addClineToStack(task)
- this.log(
- `[createTaskWithHistoryItem] ${task.parentTask ? "child" : "parent"} task ${task.taskId}.${task.instanceId} instantiated`,
- )
- }
- // Check if there's a pending edit after checkpoint restoration
- const operationId = `task-${task.taskId}`
- const pendingEdit = this.getPendingEditOperation(operationId)
- if (pendingEdit) {
- this.clearPendingEditOperation(operationId) // Clear the pending edit
- this.log(`[createTaskWithHistoryItem] Processing pending edit after checkpoint restoration`)
- // Process the pending edit after a short delay to ensure the task is fully initialized
- setTimeout(async () => {
- try {
- // Find the message index in the restored state
- const { messageIndex, apiConversationHistoryIndex } = (() => {
- const messageIndex = task.clineMessages.findIndex((msg) => msg.ts === pendingEdit.messageTs)
- const apiConversationHistoryIndex = task.apiConversationHistory.findIndex(
- (msg) => msg.ts === pendingEdit.messageTs,
- )
- return { messageIndex, apiConversationHistoryIndex }
- })()
- if (messageIndex !== -1) {
- // Remove the target message and all subsequent messages
- await task.overwriteClineMessages(task.clineMessages.slice(0, messageIndex))
- if (apiConversationHistoryIndex !== -1) {
- await task.overwriteApiConversationHistory(
- task.apiConversationHistory.slice(0, apiConversationHistoryIndex),
- )
- }
- // Process the edited message
- await task.handleWebviewAskResponse(
- "messageResponse",
- pendingEdit.editedContent,
- pendingEdit.images,
- )
- }
- } catch (error) {
- this.log(`[createTaskWithHistoryItem] Error processing pending edit: ${error}`)
- }
- }, 100) // Small delay to ensure task is fully ready
- }
- return task
- }
- public async postMessageToWebview(message: ExtensionMessage) {
- await this.view?.webview.postMessage(message)
- }
- private async getHMRHtmlContent(webview: vscode.Webview): Promise<string> {
- let localPort = "5173"
- try {
- const fs = require("fs")
- const path = require("path")
- const portFilePath = path.resolve(__dirname, "../../.vite-port")
- if (fs.existsSync(portFilePath)) {
- localPort = fs.readFileSync(portFilePath, "utf8").trim()
- console.log(`[ClineProvider:Vite] Using Vite server port from ${portFilePath}: ${localPort}`)
- } else {
- console.log(
- `[ClineProvider:Vite] Port file not found at ${portFilePath}, using default port: ${localPort}`,
- )
- }
- } catch (err) {
- console.error("[ClineProvider:Vite] Failed to read Vite port file:", err)
- }
- const localServerUrl = `localhost:${localPort}`
- // Check if local dev server is running.
- try {
- await axios.get(`http://${localServerUrl}`)
- } catch (error) {
- vscode.window.showErrorMessage(t("common:errors.hmr_not_running"))
- return this.getHtmlContent(webview)
- }
- const nonce = getNonce()
- // Get the OpenRouter base URL from configuration
- const { apiConfiguration } = await this.getState()
- const openRouterBaseUrl = apiConfiguration.openRouterBaseUrl || "https://openrouter.ai"
- // Extract the domain for CSP
- const openRouterDomain = openRouterBaseUrl.match(/^(https?:\/\/[^\/]+)/)?.[1] || "https://openrouter.ai"
- const stylesUri = getUri(webview, this.contextProxy.extensionUri, [
- "webview-ui",
- "build",
- "assets",
- "index.css",
- ])
- const codiconsUri = getUri(webview, this.contextProxy.extensionUri, ["assets", "codicons", "codicon.css"])
- const materialIconsUri = getUri(webview, this.contextProxy.extensionUri, [
- "assets",
- "vscode-material-icons",
- "icons",
- ])
- const imagesUri = getUri(webview, this.contextProxy.extensionUri, ["assets", "images"])
- const audioUri = getUri(webview, this.contextProxy.extensionUri, ["webview-ui", "audio"])
- const file = "src/index.tsx"
- const scriptUri = `http://${localServerUrl}/${file}`
- const reactRefresh = /*html*/ `
- <script nonce="${nonce}" type="module">
- import RefreshRuntime from "http://localhost:${localPort}/@react-refresh"
- RefreshRuntime.injectIntoGlobalHook(window)
- window.$RefreshReg$ = () => {}
- window.$RefreshSig$ = () => (type) => type
- window.__vite_plugin_react_preamble_installed__ = true
- </script>
- `
- const csp = [
- "default-src 'none'",
- `font-src ${webview.cspSource} data:`,
- `style-src ${webview.cspSource} 'unsafe-inline' https://* http://${localServerUrl} http://0.0.0.0:${localPort}`,
- `img-src ${webview.cspSource} https://storage.googleapis.com https://img.clerk.com data:`,
- `media-src ${webview.cspSource}`,
- `script-src 'unsafe-eval' ${webview.cspSource} https://* https://*.posthog.com http://${localServerUrl} http://0.0.0.0:${localPort} 'nonce-${nonce}'`,
- `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}`,
- ]
- return /*html*/ `
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="utf-8">
- <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
- <meta http-equiv="Content-Security-Policy" content="${csp.join("; ")}">
- <link rel="stylesheet" type="text/css" href="${stylesUri}">
- <link href="${codiconsUri}" rel="stylesheet" />
- <script nonce="${nonce}">
- window.IMAGES_BASE_URI = "${imagesUri}"
- window.AUDIO_BASE_URI = "${audioUri}"
- window.MATERIAL_ICONS_BASE_URI = "${materialIconsUri}"
- </script>
- <title>Roo Code</title>
- </head>
- <body>
- <div id="root"></div>
- ${reactRefresh}
- <script type="module" src="${scriptUri}"></script>
- </body>
- </html>
- `
- }
- /**
- * Defines and returns the HTML that should be rendered within the webview panel.
- *
- * @remarks This is also the place where references to the React webview build files
- * are created and inserted into the webview HTML.
- *
- * @param webview A reference to the extension webview
- * @param extensionUri The URI of the directory containing the extension
- * @returns A template string literal containing the HTML that should be
- * rendered within the webview panel
- */
- private async getHtmlContent(webview: vscode.Webview): Promise<string> {
- // Get the local path to main script run in the webview,
- // then convert it to a uri we can use in the webview.
- // The CSS file from the React build output
- const stylesUri = getUri(webview, this.contextProxy.extensionUri, [
- "webview-ui",
- "build",
- "assets",
- "index.css",
- ])
- const scriptUri = getUri(webview, this.contextProxy.extensionUri, ["webview-ui", "build", "assets", "index.js"])
- const codiconsUri = getUri(webview, this.contextProxy.extensionUri, ["assets", "codicons", "codicon.css"])
- const materialIconsUri = getUri(webview, this.contextProxy.extensionUri, [
- "assets",
- "vscode-material-icons",
- "icons",
- ])
- const imagesUri = getUri(webview, this.contextProxy.extensionUri, ["assets", "images"])
- const audioUri = getUri(webview, this.contextProxy.extensionUri, ["webview-ui", "audio"])
- // Use a nonce to only allow a specific script to be run.
- /*
- content security policy of your webview to only allow scripts that have a specific nonce
- create a content security policy meta tag so that only loading scripts with a nonce is allowed
- 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.
- <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}';">
- - 'unsafe-inline' is required for styles due to vscode-webview-toolkit's dynamic style injection
- - since we pass base64 images to the webview, we need to specify img-src ${webview.cspSource} data:;
- 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.
- */
- const nonce = getNonce()
- // Get the OpenRouter base URL from configuration
- const { apiConfiguration } = await this.getState()
- const openRouterBaseUrl = apiConfiguration.openRouterBaseUrl || "https://openrouter.ai"
- // Extract the domain for CSP
- const openRouterDomain = openRouterBaseUrl.match(/^(https?:\/\/[^\/]+)/)?.[1] || "https://openrouter.ai"
- // Tip: Install the es6-string-html VS Code extension to enable code highlighting below
- return /*html*/ `
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="utf-8">
- <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
- <meta name="theme-color" content="#000000">
- <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;">
- <link rel="stylesheet" type="text/css" href="${stylesUri}">
- <link href="${codiconsUri}" rel="stylesheet" />
- <script nonce="${nonce}">
- window.IMAGES_BASE_URI = "${imagesUri}"
- window.AUDIO_BASE_URI = "${audioUri}"
- window.MATERIAL_ICONS_BASE_URI = "${materialIconsUri}"
- </script>
- <title>Roo Code</title>
- </head>
- <body>
- <noscript>You need to enable JavaScript to run this app.</noscript>
- <div id="root"></div>
- <script nonce="${nonce}" type="module" src="${scriptUri}"></script>
- </body>
- </html>
- `
- }
- /**
- * Sets up an event listener to listen for messages passed from the webview context and
- * executes code based on the message that is received.
- *
- * @param webview A reference to the extension webview
- */
- private setWebviewMessageListener(webview: vscode.Webview) {
- const onReceiveMessage = async (message: WebviewMessage) =>
- webviewMessageHandler(this, message, this.marketplaceManager)
- const messageDisposable = webview.onDidReceiveMessage(onReceiveMessage)
- this.webviewDisposables.push(messageDisposable)
- }
- /**
- * Handle switching to a new mode, including updating the associated API configuration
- * @param newMode The mode to switch to
- */
- public async handleModeSwitch(newMode: Mode) {
- const task = this.getCurrentTask()
- if (task) {
- TelemetryService.instance.captureModeSwitch(task.taskId, newMode)
- task.emit(RooCodeEventName.TaskModeSwitched, task.taskId, newMode)
- try {
- // Update the task history with the new mode first.
- const history = this.getGlobalState("taskHistory") ?? []
- const taskHistoryItem = history.find((item) => item.id === task.taskId)
- if (taskHistoryItem) {
- taskHistoryItem.mode = newMode
- await this.updateTaskHistory(taskHistoryItem)
- }
- // Only update the task's mode after successful persistence.
- ;(task as any)._taskMode = newMode
- } catch (error) {
- // If persistence fails, log the error but don't update the in-memory state.
- this.log(
- `Failed to persist mode switch for task ${task.taskId}: ${error instanceof Error ? error.message : String(error)}`,
- )
- // Optionally, we could emit an event to notify about the failure.
- // This ensures the in-memory state remains consistent with persisted state.
- throw error
- }
- }
- await this.updateGlobalState("mode", newMode)
- this.emit(RooCodeEventName.ModeChanged, newMode)
- // Load the saved API config for the new mode if it exists.
- const savedConfigId = await this.providerSettingsManager.getModeConfigId(newMode)
- const listApiConfig = await this.providerSettingsManager.listConfig()
- // Update listApiConfigMeta first to ensure UI has latest data.
- await this.updateGlobalState("listApiConfigMeta", listApiConfig)
- // If this mode has a saved config, use it.
- if (savedConfigId) {
- const profile = listApiConfig.find(({ id }) => id === savedConfigId)
- if (profile?.name) {
- await this.activateProviderProfile({ name: profile.name })
- }
- } else {
- // If no saved config for this mode, save current config as default.
- const currentApiConfigName = this.getGlobalState("currentApiConfigName")
- if (currentApiConfigName) {
- const config = listApiConfig.find((c) => c.name === currentApiConfigName)
- if (config?.id) {
- await this.providerSettingsManager.setModeConfig(newMode, config.id)
- }
- }
- }
- await this.postStateToWebview()
- }
- // Provider Profile Management
- /**
- * Updates the current task's API handler.
- * Rebuilds when:
- * - provider or model changes, OR
- * - explicitly forced (e.g., user-initiated profile switch/save to apply changed settings like headers/baseUrl/tier).
- * Always synchronizes task.apiConfiguration with latest provider settings.
- * @param providerSettings The new provider settings to apply
- * @param options.forceRebuild Force rebuilding the API handler regardless of provider/model equality
- */
- private updateTaskApiHandlerIfNeeded(
- providerSettings: ProviderSettings,
- options: { forceRebuild?: boolean } = {},
- ): void {
- const task = this.getCurrentTask()
- if (!task) return
- const { forceRebuild = false } = options
- // Determine if we need to rebuild using the previous configuration snapshot
- const prevConfig = task.apiConfiguration
- const prevProvider = prevConfig?.apiProvider
- const prevModelId = prevConfig ? getModelId(prevConfig) : undefined
- const prevToolProtocol = prevConfig?.toolProtocol
- const newProvider = providerSettings.apiProvider
- const newModelId = getModelId(providerSettings)
- const newToolProtocol = providerSettings.toolProtocol
- const needsRebuild =
- forceRebuild ||
- prevProvider !== newProvider ||
- prevModelId !== newModelId ||
- prevToolProtocol !== newToolProtocol
- if (needsRebuild) {
- // Use updateApiConfiguration which handles both API handler rebuild and parser sync.
- // This is important when toolProtocol changes - the assistantMessageParser needs to be
- // created/destroyed to match the new protocol (XML vs native).
- // Note: updateApiConfiguration is declared async but has no actual async operations,
- // so we can safely call it without awaiting.
- task.updateApiConfiguration(providerSettings)
- } else {
- // No rebuild needed, just sync apiConfiguration
- ;(task as any).apiConfiguration = providerSettings
- }
- }
- getProviderProfileEntries(): ProviderSettingsEntry[] {
- return this.contextProxy.getValues().listApiConfigMeta || []
- }
- getProviderProfileEntry(name: string): ProviderSettingsEntry | undefined {
- return this.getProviderProfileEntries().find((profile) => profile.name === name)
- }
- public hasProviderProfileEntry(name: string): boolean {
- return !!this.getProviderProfileEntry(name)
- }
- async upsertProviderProfile(
- name: string,
- providerSettings: ProviderSettings,
- activate: boolean = true,
- ): Promise<string | undefined> {
- try {
- // TODO: Do we need to be calling `activateProfile`? It's not
- // clear to me what the source of truth should be; in some cases
- // we rely on the `ContextProxy`'s data store and in other cases
- // we rely on the `ProviderSettingsManager`'s data store. It might
- // be simpler to unify these two.
- const id = await this.providerSettingsManager.saveConfig(name, providerSettings)
- if (activate) {
- const { mode } = await this.getState()
- // These promises do the following:
- // 1. Adds or updates the list of provider profiles.
- // 2. Sets the current provider profile.
- // 3. Sets the current mode's provider profile.
- // 4. Copies the provider settings to the context.
- //
- // Note: 1, 2, and 4 can be done in one `ContextProxy` call:
- // this.contextProxy.setValues({ ...providerSettings, listApiConfigMeta: ..., currentApiConfigName: ... })
- // We should probably switch to that and verify that it works.
- // I left the original implementation in just to be safe.
- await Promise.all([
- this.updateGlobalState("listApiConfigMeta", await this.providerSettingsManager.listConfig()),
- this.updateGlobalState("currentApiConfigName", name),
- this.providerSettingsManager.setModeConfig(mode, id),
- this.contextProxy.setProviderSettings(providerSettings),
- ])
- // Change the provider for the current task.
- // TODO: We should rename `buildApiHandler` for clarity (e.g. `getProviderClient`).
- this.updateTaskApiHandlerIfNeeded(providerSettings, { forceRebuild: true })
- } else {
- await this.updateGlobalState("listApiConfigMeta", await this.providerSettingsManager.listConfig())
- }
- await this.postStateToWebview()
- return id
- } catch (error) {
- this.log(
- `Error create new api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
- )
- vscode.window.showErrorMessage(t("common:errors.create_api_config"))
- return undefined
- }
- }
- async deleteProviderProfile(profileToDelete: ProviderSettingsEntry) {
- const globalSettings = this.contextProxy.getValues()
- let profileToActivate: string | undefined = globalSettings.currentApiConfigName
- if (profileToDelete.name === profileToActivate) {
- profileToActivate = this.getProviderProfileEntries().find(({ name }) => name !== profileToDelete.name)?.name
- }
- if (!profileToActivate) {
- throw new Error("You cannot delete the last profile")
- }
- const entries = this.getProviderProfileEntries().filter(({ name }) => name !== profileToDelete.name)
- await this.contextProxy.setValues({
- ...globalSettings,
- currentApiConfigName: profileToActivate,
- listApiConfigMeta: entries,
- })
- await this.postStateToWebview()
- }
- async activateProviderProfile(args: { name: string } | { id: string }) {
- const { name, id, ...providerSettings } = await this.providerSettingsManager.activateProfile(args)
- // See `upsertProviderProfile` for a description of what this is doing.
- await Promise.all([
- this.contextProxy.setValue("listApiConfigMeta", await this.providerSettingsManager.listConfig()),
- this.contextProxy.setValue("currentApiConfigName", name),
- this.contextProxy.setProviderSettings(providerSettings),
- ])
- const { mode } = await this.getState()
- if (id) {
- await this.providerSettingsManager.setModeConfig(mode, id)
- }
- // Change the provider for the current task.
- this.updateTaskApiHandlerIfNeeded(providerSettings, { forceRebuild: true })
- await this.postStateToWebview()
- if (providerSettings.apiProvider) {
- this.emit(RooCodeEventName.ProviderProfileChanged, { name, provider: providerSettings.apiProvider })
- }
- }
- async updateCustomInstructions(instructions?: string) {
- // User may be clearing the field.
- await this.updateGlobalState("customInstructions", instructions || undefined)
- await this.postStateToWebview()
- }
- // MCP
- async ensureMcpServersDirectoryExists(): Promise<string> {
- // Get platform-specific application data directory
- let mcpServersDir: string
- if (process.platform === "win32") {
- // Windows: %APPDATA%\Roo-Code\MCP
- mcpServersDir = path.join(os.homedir(), "AppData", "Roaming", "Roo-Code", "MCP")
- } else if (process.platform === "darwin") {
- // macOS: ~/Documents/Cline/MCP
- mcpServersDir = path.join(os.homedir(), "Documents", "Cline", "MCP")
- } else {
- // Linux: ~/.local/share/Cline/MCP
- mcpServersDir = path.join(os.homedir(), ".local", "share", "Roo-Code", "MCP")
- }
- try {
- await fs.mkdir(mcpServersDir, { recursive: true })
- } catch (error) {
- // Fallback to a relative path if directory creation fails
- return path.join(os.homedir(), ".roo-code", "mcp")
- }
- return mcpServersDir
- }
- async ensureSettingsDirectoryExists(): Promise<string> {
- const { getSettingsDirectoryPath } = await import("../../utils/storage")
- const globalStoragePath = this.contextProxy.globalStorageUri.fsPath
- return getSettingsDirectoryPath(globalStoragePath)
- }
- // OpenRouter
- async handleOpenRouterCallback(code: string) {
- let { apiConfiguration, currentApiConfigName = "default" } = await this.getState()
- let apiKey: string
- try {
- const baseUrl = apiConfiguration.openRouterBaseUrl || "https://openrouter.ai/api/v1"
- // Extract the base domain for the auth endpoint.
- const baseUrlDomain = baseUrl.match(/^(https?:\/\/[^\/]+)/)?.[1] || "https://openrouter.ai"
- const response = await axios.post(`${baseUrlDomain}/api/v1/auth/keys`, { code })
- if (response.data && response.data.key) {
- apiKey = response.data.key
- } else {
- throw new Error("Invalid response from OpenRouter API")
- }
- } catch (error) {
- this.log(
- `Error exchanging code for API key: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
- )
- throw error
- }
- const newConfiguration: ProviderSettings = {
- ...apiConfiguration,
- apiProvider: "openrouter",
- openRouterApiKey: apiKey,
- openRouterModelId: apiConfiguration?.openRouterModelId || openRouterDefaultModelId,
- }
- await this.upsertProviderProfile(currentApiConfigName, newConfiguration)
- }
- // Requesty
- async handleRequestyCallback(code: string, baseUrl: string | null) {
- let { apiConfiguration } = await this.getState()
- const newConfiguration: ProviderSettings = {
- ...apiConfiguration,
- apiProvider: "requesty",
- requestyApiKey: code,
- requestyModelId: apiConfiguration?.requestyModelId || requestyDefaultModelId,
- }
- // set baseUrl as undefined if we don't provide one
- // or if it is the default requesty url
- if (!baseUrl || baseUrl === REQUESTY_BASE_URL) {
- newConfiguration.requestyBaseUrl = undefined
- } else {
- newConfiguration.requestyBaseUrl = baseUrl
- }
- const profileName = `Requesty (${new Date().toLocaleString()})`
- await this.upsertProviderProfile(profileName, newConfiguration)
- }
- // Task history
- async getTaskWithId(id: string): Promise<{
- historyItem: HistoryItem
- taskDirPath: string
- apiConversationHistoryFilePath: string
- uiMessagesFilePath: string
- apiConversationHistory: Anthropic.MessageParam[]
- }> {
- const history = this.getGlobalState("taskHistory") ?? []
- const historyItem = history.find((item) => item.id === id)
- if (historyItem) {
- const { getTaskDirectoryPath } = await import("../../utils/storage")
- const globalStoragePath = this.contextProxy.globalStorageUri.fsPath
- const taskDirPath = await getTaskDirectoryPath(globalStoragePath, id)
- const apiConversationHistoryFilePath = path.join(taskDirPath, GlobalFileNames.apiConversationHistory)
- const uiMessagesFilePath = path.join(taskDirPath, GlobalFileNames.uiMessages)
- const fileExists = await fileExistsAtPath(apiConversationHistoryFilePath)
- if (fileExists) {
- const apiConversationHistory = JSON.parse(await fs.readFile(apiConversationHistoryFilePath, "utf8"))
- return {
- historyItem,
- taskDirPath,
- apiConversationHistoryFilePath,
- uiMessagesFilePath,
- apiConversationHistory,
- }
- }
- }
- // if we tried to get a task that doesn't exist, remove it from state
- // FIXME: this seems to happen sometimes when the json file doesnt save to disk for some reason
- await this.deleteTaskFromState(id)
- throw new Error("Task not found")
- }
- async showTaskWithId(id: string) {
- if (id !== this.getCurrentTask()?.taskId) {
- // Non-current task.
- const { historyItem } = await this.getTaskWithId(id)
- await this.createTaskWithHistoryItem(historyItem) // Clears existing task.
- }
- await this.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
- }
- async exportTaskWithId(id: string) {
- const { historyItem, apiConversationHistory } = await this.getTaskWithId(id)
- await downloadTask(historyItem.ts, apiConversationHistory)
- }
- /* Condenses a task's message history to use fewer tokens. */
- async condenseTaskContext(taskId: string) {
- let task: Task | undefined
- for (let i = this.clineStack.length - 1; i >= 0; i--) {
- if (this.clineStack[i].taskId === taskId) {
- task = this.clineStack[i]
- break
- }
- }
- if (!task) {
- throw new Error(`Task with id ${taskId} not found in stack`)
- }
- await task.condenseContext()
- await this.postMessageToWebview({ type: "condenseTaskContextResponse", text: taskId })
- }
- // this function deletes a task from task hidtory, and deletes it's checkpoints and delete the task folder
- async deleteTaskWithId(id: string) {
- try {
- // get the task directory full path
- const { taskDirPath } = await this.getTaskWithId(id)
- // remove task from stack if it's the current task
- if (id === this.getCurrentTask()?.taskId) {
- // Close the current task instance; delegation flows will be handled via metadata if applicable.
- await this.removeClineFromStack()
- }
- // delete task from the task history state
- await this.deleteTaskFromState(id)
- // Delete associated shadow repository or branch.
- // TODO: Store `workspaceDir` in the `HistoryItem` object.
- const globalStorageDir = this.contextProxy.globalStorageUri.fsPath
- const workspaceDir = this.cwd
- try {
- await ShadowCheckpointService.deleteTask({ taskId: id, globalStorageDir, workspaceDir })
- } catch (error) {
- console.error(
- `[deleteTaskWithId${id}] failed to delete associated shadow repository or branch: ${error instanceof Error ? error.message : String(error)}`,
- )
- }
- // delete the entire task directory including checkpoints and all content
- try {
- await fs.rm(taskDirPath, { recursive: true, force: true })
- console.log(`[deleteTaskWithId${id}] removed task directory`)
- } catch (error) {
- console.error(
- `[deleteTaskWithId${id}] failed to remove task directory: ${error instanceof Error ? error.message : String(error)}`,
- )
- }
- } catch (error) {
- // If task is not found, just remove it from state
- if (error instanceof Error && error.message === "Task not found") {
- await this.deleteTaskFromState(id)
- return
- }
- throw error
- }
- }
- async deleteTaskFromState(id: string) {
- const taskHistory = this.getGlobalState("taskHistory") ?? []
- const updatedTaskHistory = taskHistory.filter((task) => task.id !== id)
- await this.updateGlobalState("taskHistory", updatedTaskHistory)
- this.recentTasksCache = undefined
- await this.postStateToWebview()
- }
- async refreshWorkspace() {
- this.currentWorkspacePath = getWorkspacePath()
- await this.postStateToWebview()
- }
- async postStateToWebview() {
- const state = await this.getStateToPostToWebview()
- this.postMessageToWebview({ type: "state", state })
- // Check MDM compliance and send user to account tab if not compliant
- // Only redirect if there's an actual MDM policy requiring authentication
- if (this.mdmService?.requiresCloudAuth() && !this.checkMdmCompliance()) {
- await this.postMessageToWebview({ type: "action", action: "cloudButtonClicked" })
- }
- }
- /**
- * Fetches marketplace data on demand to avoid blocking main state updates
- */
- async fetchMarketplaceData() {
- try {
- const [marketplaceResult, marketplaceInstalledMetadata] = await Promise.all([
- this.marketplaceManager.getMarketplaceItems().catch((error) => {
- console.error("Failed to fetch marketplace items:", error)
- return { organizationMcps: [], marketplaceItems: [], errors: [error.message] }
- }),
- this.marketplaceManager.getInstallationMetadata().catch((error) => {
- console.error("Failed to fetch installation metadata:", error)
- return { project: {}, global: {} } as MarketplaceInstalledMetadata
- }),
- ])
- // Send marketplace data separately
- this.postMessageToWebview({
- type: "marketplaceData",
- organizationMcps: marketplaceResult.organizationMcps || [],
- marketplaceItems: marketplaceResult.marketplaceItems || [],
- marketplaceInstalledMetadata: marketplaceInstalledMetadata || { project: {}, global: {} },
- errors: marketplaceResult.errors,
- })
- } catch (error) {
- console.error("Failed to fetch marketplace data:", error)
- // Send empty data on error to prevent UI from hanging
- this.postMessageToWebview({
- type: "marketplaceData",
- organizationMcps: [],
- marketplaceItems: [],
- marketplaceInstalledMetadata: { project: {}, global: {} },
- errors: [error instanceof Error ? error.message : String(error)],
- })
- // Show user-friendly error notification for network issues
- if (error instanceof Error && error.message.includes("timeout")) {
- vscode.window.showWarningMessage(
- "Marketplace data could not be loaded due to network restrictions. Core functionality remains available.",
- )
- }
- }
- }
- /**
- * Checks if there is a file-based system prompt override for the given mode
- */
- async hasFileBasedSystemPromptOverride(mode: Mode): Promise<boolean> {
- const promptFilePath = getSystemPromptFilePath(this.cwd, mode)
- return await fileExistsAtPath(promptFilePath)
- }
- /**
- * Merges allowed commands from global state and workspace configuration
- * with proper validation and deduplication
- */
- private mergeAllowedCommands(globalStateCommands?: string[]): string[] {
- return this.mergeCommandLists("allowedCommands", "allowed", globalStateCommands)
- }
- /**
- * Merges denied commands from global state and workspace configuration
- * with proper validation and deduplication
- */
- private mergeDeniedCommands(globalStateCommands?: string[]): string[] {
- return this.mergeCommandLists("deniedCommands", "denied", globalStateCommands)
- }
- /**
- * Common utility for merging command lists from global state and workspace configuration.
- * Implements the Command Denylist feature's merging strategy with proper validation.
- *
- * @param configKey - VSCode workspace configuration key
- * @param commandType - Type of commands for error logging
- * @param globalStateCommands - Commands from global state
- * @returns Merged and deduplicated command list
- */
- private mergeCommandLists(
- configKey: "allowedCommands" | "deniedCommands",
- commandType: "allowed" | "denied",
- globalStateCommands?: string[],
- ): string[] {
- try {
- // Validate and sanitize global state commands
- const validGlobalCommands = Array.isArray(globalStateCommands)
- ? globalStateCommands.filter((cmd) => typeof cmd === "string" && cmd.trim().length > 0)
- : []
- // Get workspace configuration commands
- const workspaceCommands = vscode.workspace.getConfiguration(Package.name).get<string[]>(configKey) || []
- // Validate and sanitize workspace commands
- const validWorkspaceCommands = Array.isArray(workspaceCommands)
- ? workspaceCommands.filter((cmd) => typeof cmd === "string" && cmd.trim().length > 0)
- : []
- // Combine and deduplicate commands
- // Global state takes precedence over workspace configuration
- const mergedCommands = [...new Set([...validGlobalCommands, ...validWorkspaceCommands])]
- return mergedCommands
- } catch (error) {
- console.error(`Error merging ${commandType} commands:`, error)
- // Return empty array as fallback to prevent crashes
- return []
- }
- }
- async getStateToPostToWebview(): Promise<ExtensionState> {
- const {
- apiConfiguration,
- lastShownAnnouncementId,
- customInstructions,
- alwaysAllowReadOnly,
- alwaysAllowReadOnlyOutsideWorkspace,
- alwaysAllowWrite,
- alwaysAllowWriteOutsideWorkspace,
- alwaysAllowWriteProtected,
- alwaysAllowExecute,
- allowedCommands,
- deniedCommands,
- alwaysAllowBrowser,
- alwaysAllowMcp,
- alwaysAllowModeSwitch,
- alwaysAllowSubtasks,
- allowedMaxRequests,
- allowedMaxCost,
- autoCondenseContext,
- autoCondenseContextPercent,
- soundEnabled,
- ttsEnabled,
- ttsSpeed,
- diffEnabled,
- enableCheckpoints,
- checkpointTimeout,
- taskHistory,
- soundVolume,
- browserViewportSize,
- screenshotQuality,
- remoteBrowserHost,
- remoteBrowserEnabled,
- cachedChromeHostUrl,
- writeDelayMs,
- terminalOutputLineLimit,
- terminalOutputCharacterLimit,
- terminalShellIntegrationTimeout,
- terminalShellIntegrationDisabled,
- terminalCommandDelay,
- terminalPowershellCounter,
- terminalZshClearEolMark,
- terminalZshOhMy,
- terminalZshP10k,
- terminalZdotdir,
- fuzzyMatchThreshold,
- mcpEnabled,
- enableMcpServerCreation,
- currentApiConfigName,
- listApiConfigMeta,
- pinnedApiConfigs,
- mode,
- customModePrompts,
- customSupportPrompts,
- enhancementApiConfigId,
- autoApprovalEnabled,
- customModes,
- experiments,
- maxOpenTabsContext,
- maxWorkspaceFiles,
- browserToolEnabled,
- telemetrySetting,
- showRooIgnoredFiles,
- language,
- maxReadFileLine,
- maxImageFileSize,
- maxTotalImageSize,
- terminalCompressProgressBar,
- historyPreviewCollapsed,
- reasoningBlockCollapsed,
- enterBehavior,
- cloudUserInfo,
- cloudIsAuthenticated,
- sharingEnabled,
- organizationAllowList,
- organizationSettingsVersion,
- maxConcurrentFileReads,
- condensingApiConfigId,
- customCondensingPrompt,
- codebaseIndexConfig,
- codebaseIndexModels,
- profileThresholds,
- alwaysAllowFollowupQuestions,
- followupAutoApproveTimeoutMs,
- includeDiagnosticMessages,
- maxDiagnosticMessages,
- includeTaskHistoryInEnhance,
- includeCurrentTime,
- includeCurrentCost,
- maxGitStatusFiles,
- taskSyncEnabled,
- remoteControlEnabled,
- imageGenerationProvider,
- openRouterImageApiKey,
- openRouterImageGenerationSelectedModel,
- openRouterUseMiddleOutTransform,
- featureRoomoteControlEnabled,
- isBrowserSessionActive,
- } = await this.getState()
- let cloudOrganizations: CloudOrganizationMembership[] = []
- try {
- if (!CloudService.instance.isCloudAgent) {
- const now = Date.now()
- if (
- this.cloudOrganizationsCache !== null &&
- this.cloudOrganizationsCacheTimestamp !== null &&
- now - this.cloudOrganizationsCacheTimestamp < ClineProvider.CLOUD_ORGANIZATIONS_CACHE_DURATION_MS
- ) {
- cloudOrganizations = this.cloudOrganizationsCache!
- } else {
- cloudOrganizations = await CloudService.instance.getOrganizationMemberships()
- this.cloudOrganizationsCache = cloudOrganizations
- this.cloudOrganizationsCacheTimestamp = now
- }
- }
- } catch (error) {
- // Ignore this error.
- }
- const telemetryKey = process.env.POSTHOG_API_KEY
- const machineId = vscode.env.machineId
- const mergedAllowedCommands = this.mergeAllowedCommands(allowedCommands)
- const mergedDeniedCommands = this.mergeDeniedCommands(deniedCommands)
- const cwd = this.cwd
- // Check if there's a system prompt override for the current mode
- const currentMode = mode ?? defaultModeSlug
- const hasSystemPromptOverride = await this.hasFileBasedSystemPromptOverride(currentMode)
- return {
- version: this.context.extension?.packageJSON?.version ?? "",
- apiConfiguration,
- customInstructions,
- alwaysAllowReadOnly: alwaysAllowReadOnly ?? false,
- alwaysAllowReadOnlyOutsideWorkspace: alwaysAllowReadOnlyOutsideWorkspace ?? false,
- alwaysAllowWrite: alwaysAllowWrite ?? false,
- alwaysAllowWriteOutsideWorkspace: alwaysAllowWriteOutsideWorkspace ?? false,
- alwaysAllowWriteProtected: alwaysAllowWriteProtected ?? false,
- alwaysAllowExecute: alwaysAllowExecute ?? false,
- alwaysAllowBrowser: alwaysAllowBrowser ?? false,
- alwaysAllowMcp: alwaysAllowMcp ?? false,
- alwaysAllowModeSwitch: alwaysAllowModeSwitch ?? false,
- alwaysAllowSubtasks: alwaysAllowSubtasks ?? false,
- isBrowserSessionActive,
- allowedMaxRequests,
- allowedMaxCost,
- autoCondenseContext: autoCondenseContext ?? true,
- autoCondenseContextPercent: autoCondenseContextPercent ?? 100,
- uriScheme: vscode.env.uriScheme,
- currentTaskItem: this.getCurrentTask()?.taskId
- ? (taskHistory || []).find((item: HistoryItem) => item.id === this.getCurrentTask()?.taskId)
- : undefined,
- clineMessages: this.getCurrentTask()?.clineMessages || [],
- currentTaskTodos: this.getCurrentTask()?.todoList || [],
- messageQueue: this.getCurrentTask()?.messageQueueService?.messages,
- taskHistory: (taskHistory || [])
- .filter((item: HistoryItem) => item.ts && item.task)
- .sort((a: HistoryItem, b: HistoryItem) => b.ts - a.ts),
- soundEnabled: soundEnabled ?? false,
- ttsEnabled: ttsEnabled ?? false,
- ttsSpeed: ttsSpeed ?? 1.0,
- diffEnabled: diffEnabled ?? true,
- enableCheckpoints: enableCheckpoints ?? true,
- checkpointTimeout: checkpointTimeout ?? DEFAULT_CHECKPOINT_TIMEOUT_SECONDS,
- shouldShowAnnouncement:
- telemetrySetting !== "unset" && lastShownAnnouncementId !== this.latestAnnouncementId,
- allowedCommands: mergedAllowedCommands,
- deniedCommands: mergedDeniedCommands,
- soundVolume: soundVolume ?? 0.5,
- browserViewportSize: browserViewportSize ?? "900x600",
- screenshotQuality: screenshotQuality ?? 75,
- remoteBrowserHost,
- remoteBrowserEnabled: remoteBrowserEnabled ?? false,
- cachedChromeHostUrl: cachedChromeHostUrl,
- writeDelayMs: writeDelayMs ?? DEFAULT_WRITE_DELAY_MS,
- terminalOutputLineLimit: terminalOutputLineLimit ?? 500,
- terminalOutputCharacterLimit: terminalOutputCharacterLimit ?? DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT,
- terminalShellIntegrationTimeout: terminalShellIntegrationTimeout ?? Terminal.defaultShellIntegrationTimeout,
- terminalShellIntegrationDisabled: terminalShellIntegrationDisabled ?? true,
- terminalCommandDelay: terminalCommandDelay ?? 0,
- terminalPowershellCounter: terminalPowershellCounter ?? false,
- terminalZshClearEolMark: terminalZshClearEolMark ?? true,
- terminalZshOhMy: terminalZshOhMy ?? false,
- terminalZshP10k: terminalZshP10k ?? false,
- terminalZdotdir: terminalZdotdir ?? false,
- fuzzyMatchThreshold: fuzzyMatchThreshold ?? 1.0,
- mcpEnabled: mcpEnabled ?? true,
- enableMcpServerCreation: enableMcpServerCreation ?? true,
- currentApiConfigName: currentApiConfigName ?? "default",
- listApiConfigMeta: listApiConfigMeta ?? [],
- pinnedApiConfigs: pinnedApiConfigs ?? {},
- mode: mode ?? defaultModeSlug,
- customModePrompts: customModePrompts ?? {},
- customSupportPrompts: customSupportPrompts ?? {},
- enhancementApiConfigId,
- autoApprovalEnabled: autoApprovalEnabled ?? false,
- customModes,
- experiments: experiments ?? experimentDefault,
- mcpServers: this.mcpHub?.getAllServers() ?? [],
- maxOpenTabsContext: maxOpenTabsContext ?? 20,
- maxWorkspaceFiles: maxWorkspaceFiles ?? 200,
- cwd,
- browserToolEnabled: browserToolEnabled ?? true,
- telemetrySetting,
- telemetryKey,
- machineId,
- showRooIgnoredFiles: showRooIgnoredFiles ?? false,
- language: language ?? formatLanguage(vscode.env.language),
- renderContext: this.renderContext,
- maxReadFileLine: maxReadFileLine ?? -1,
- maxImageFileSize: maxImageFileSize ?? 5,
- maxTotalImageSize: maxTotalImageSize ?? 20,
- maxConcurrentFileReads: maxConcurrentFileReads ?? 5,
- settingsImportedAt: this.settingsImportedAt,
- terminalCompressProgressBar: terminalCompressProgressBar ?? true,
- hasSystemPromptOverride,
- historyPreviewCollapsed: historyPreviewCollapsed ?? false,
- reasoningBlockCollapsed: reasoningBlockCollapsed ?? true,
- enterBehavior: enterBehavior ?? "send",
- cloudUserInfo,
- cloudIsAuthenticated: cloudIsAuthenticated ?? false,
- cloudOrganizations,
- sharingEnabled: sharingEnabled ?? false,
- organizationAllowList,
- organizationSettingsVersion,
- condensingApiConfigId,
- customCondensingPrompt,
- codebaseIndexModels: codebaseIndexModels ?? EMBEDDING_MODEL_PROFILES,
- codebaseIndexConfig: {
- codebaseIndexEnabled: codebaseIndexConfig?.codebaseIndexEnabled ?? false,
- codebaseIndexQdrantUrl: codebaseIndexConfig?.codebaseIndexQdrantUrl ?? "http://localhost:6333",
- codebaseIndexEmbedderProvider: codebaseIndexConfig?.codebaseIndexEmbedderProvider ?? "openai",
- codebaseIndexEmbedderBaseUrl: codebaseIndexConfig?.codebaseIndexEmbedderBaseUrl ?? "",
- codebaseIndexEmbedderModelId: codebaseIndexConfig?.codebaseIndexEmbedderModelId ?? "",
- codebaseIndexEmbedderModelDimension: codebaseIndexConfig?.codebaseIndexEmbedderModelDimension ?? 1536,
- codebaseIndexOpenAiCompatibleBaseUrl: codebaseIndexConfig?.codebaseIndexOpenAiCompatibleBaseUrl,
- codebaseIndexSearchMaxResults: codebaseIndexConfig?.codebaseIndexSearchMaxResults,
- codebaseIndexSearchMinScore: codebaseIndexConfig?.codebaseIndexSearchMinScore,
- codebaseIndexBedrockRegion: codebaseIndexConfig?.codebaseIndexBedrockRegion,
- codebaseIndexBedrockProfile: codebaseIndexConfig?.codebaseIndexBedrockProfile,
- codebaseIndexOpenRouterSpecificProvider: codebaseIndexConfig?.codebaseIndexOpenRouterSpecificProvider,
- },
- // Only set mdmCompliant if there's an actual MDM policy
- // undefined means no MDM policy, true means compliant, false means non-compliant
- mdmCompliant: this.mdmService?.requiresCloudAuth() ? this.checkMdmCompliance() : undefined,
- profileThresholds: profileThresholds ?? {},
- cloudApiUrl: getRooCodeApiUrl(),
- hasOpenedModeSelector: this.getGlobalState("hasOpenedModeSelector") ?? false,
- alwaysAllowFollowupQuestions: alwaysAllowFollowupQuestions ?? false,
- followupAutoApproveTimeoutMs: followupAutoApproveTimeoutMs ?? 60000,
- includeDiagnosticMessages: includeDiagnosticMessages ?? true,
- maxDiagnosticMessages: maxDiagnosticMessages ?? 50,
- includeTaskHistoryInEnhance: includeTaskHistoryInEnhance ?? true,
- includeCurrentTime: includeCurrentTime ?? true,
- includeCurrentCost: includeCurrentCost ?? true,
- maxGitStatusFiles: maxGitStatusFiles ?? 0,
- taskSyncEnabled,
- remoteControlEnabled,
- imageGenerationProvider,
- openRouterImageApiKey,
- openRouterImageGenerationSelectedModel,
- openRouterUseMiddleOutTransform,
- featureRoomoteControlEnabled,
- debug: vscode.workspace.getConfiguration(Package.name).get<boolean>("debug", false),
- }
- }
- /**
- * Storage
- * https://dev.to/kompotkot/how-to-use-secretstorage-in-your-vscode-extensions-2hco
- * https://www.eliostruyf.com/devhack-code-extension-storage-options/
- */
- async getState(): Promise<
- Omit<
- ExtensionState,
- | "clineMessages"
- | "renderContext"
- | "hasOpenedModeSelector"
- | "version"
- | "shouldShowAnnouncement"
- | "hasSystemPromptOverride"
- >
- > {
- const stateValues = this.contextProxy.getValues()
- const customModes = await this.customModesManager.getCustomModes()
- // Determine apiProvider with the same logic as before.
- const apiProvider: ProviderName = stateValues.apiProvider ? stateValues.apiProvider : "anthropic"
- // Build the apiConfiguration object combining state values and secrets.
- const providerSettings = this.contextProxy.getProviderSettings()
- // Ensure apiProvider is set properly if not already in state
- if (!providerSettings.apiProvider) {
- providerSettings.apiProvider = apiProvider
- }
- let organizationAllowList = ORGANIZATION_ALLOW_ALL
- try {
- organizationAllowList = await CloudService.instance.getAllowList()
- } catch (error) {
- console.error(
- `[getState] failed to get organization allow list: ${error instanceof Error ? error.message : String(error)}`,
- )
- }
- let cloudUserInfo: CloudUserInfo | null = null
- try {
- cloudUserInfo = CloudService.instance.getUserInfo()
- } catch (error) {
- console.error(
- `[getState] failed to get cloud user info: ${error instanceof Error ? error.message : String(error)}`,
- )
- }
- let cloudIsAuthenticated: boolean = false
- try {
- cloudIsAuthenticated = CloudService.instance.isAuthenticated()
- } catch (error) {
- console.error(
- `[getState] failed to get cloud authentication state: ${error instanceof Error ? error.message : String(error)}`,
- )
- }
- let sharingEnabled: boolean = false
- try {
- sharingEnabled = await CloudService.instance.canShareTask()
- } catch (error) {
- console.error(
- `[getState] failed to get sharing enabled state: ${error instanceof Error ? error.message : String(error)}`,
- )
- }
- let organizationSettingsVersion: number = -1
- try {
- if (CloudService.hasInstance()) {
- const settings = CloudService.instance.getOrganizationSettings()
- organizationSettingsVersion = settings?.version ?? -1
- }
- } catch (error) {
- console.error(
- `[getState] failed to get organization settings version: ${error instanceof Error ? error.message : String(error)}`,
- )
- }
- let taskSyncEnabled: boolean = false
- try {
- taskSyncEnabled = CloudService.instance.isTaskSyncEnabled()
- } catch (error) {
- console.error(
- `[getState] failed to get task sync enabled state: ${error instanceof Error ? error.message : String(error)}`,
- )
- }
- // Get actual browser session state
- const isBrowserSessionActive = this.getCurrentTask()?.browserSession?.isSessionActive() ?? false
- // Return the same structure as before.
- return {
- apiConfiguration: providerSettings,
- lastShownAnnouncementId: stateValues.lastShownAnnouncementId,
- customInstructions: stateValues.customInstructions,
- apiModelId: stateValues.apiModelId,
- alwaysAllowReadOnly: stateValues.alwaysAllowReadOnly ?? false,
- alwaysAllowReadOnlyOutsideWorkspace: stateValues.alwaysAllowReadOnlyOutsideWorkspace ?? false,
- alwaysAllowWrite: stateValues.alwaysAllowWrite ?? false,
- alwaysAllowWriteOutsideWorkspace: stateValues.alwaysAllowWriteOutsideWorkspace ?? false,
- alwaysAllowWriteProtected: stateValues.alwaysAllowWriteProtected ?? false,
- alwaysAllowExecute: stateValues.alwaysAllowExecute ?? false,
- alwaysAllowBrowser: stateValues.alwaysAllowBrowser ?? false,
- alwaysAllowMcp: stateValues.alwaysAllowMcp ?? false,
- alwaysAllowModeSwitch: stateValues.alwaysAllowModeSwitch ?? false,
- alwaysAllowSubtasks: stateValues.alwaysAllowSubtasks ?? false,
- alwaysAllowFollowupQuestions: stateValues.alwaysAllowFollowupQuestions ?? false,
- isBrowserSessionActive,
- followupAutoApproveTimeoutMs: stateValues.followupAutoApproveTimeoutMs ?? 60000,
- diagnosticsEnabled: stateValues.diagnosticsEnabled ?? true,
- allowedMaxRequests: stateValues.allowedMaxRequests,
- allowedMaxCost: stateValues.allowedMaxCost,
- autoCondenseContext: stateValues.autoCondenseContext ?? true,
- autoCondenseContextPercent: stateValues.autoCondenseContextPercent ?? 100,
- taskHistory: stateValues.taskHistory ?? [],
- allowedCommands: stateValues.allowedCommands,
- deniedCommands: stateValues.deniedCommands,
- soundEnabled: stateValues.soundEnabled ?? false,
- ttsEnabled: stateValues.ttsEnabled ?? false,
- ttsSpeed: stateValues.ttsSpeed ?? 1.0,
- diffEnabled: stateValues.diffEnabled ?? true,
- enableCheckpoints: stateValues.enableCheckpoints ?? true,
- checkpointTimeout: stateValues.checkpointTimeout ?? DEFAULT_CHECKPOINT_TIMEOUT_SECONDS,
- soundVolume: stateValues.soundVolume,
- browserViewportSize: stateValues.browserViewportSize ?? "900x600",
- screenshotQuality: stateValues.screenshotQuality ?? 75,
- remoteBrowserHost: stateValues.remoteBrowserHost,
- remoteBrowserEnabled: stateValues.remoteBrowserEnabled ?? false,
- cachedChromeHostUrl: stateValues.cachedChromeHostUrl as string | undefined,
- fuzzyMatchThreshold: stateValues.fuzzyMatchThreshold ?? 1.0,
- writeDelayMs: stateValues.writeDelayMs ?? DEFAULT_WRITE_DELAY_MS,
- terminalOutputLineLimit: stateValues.terminalOutputLineLimit ?? 500,
- terminalOutputCharacterLimit:
- stateValues.terminalOutputCharacterLimit ?? DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT,
- terminalShellIntegrationTimeout:
- stateValues.terminalShellIntegrationTimeout ?? Terminal.defaultShellIntegrationTimeout,
- terminalShellIntegrationDisabled: stateValues.terminalShellIntegrationDisabled ?? true,
- terminalCommandDelay: stateValues.terminalCommandDelay ?? 0,
- terminalPowershellCounter: stateValues.terminalPowershellCounter ?? false,
- terminalZshClearEolMark: stateValues.terminalZshClearEolMark ?? true,
- terminalZshOhMy: stateValues.terminalZshOhMy ?? false,
- terminalZshP10k: stateValues.terminalZshP10k ?? false,
- terminalZdotdir: stateValues.terminalZdotdir ?? false,
- terminalCompressProgressBar: stateValues.terminalCompressProgressBar ?? true,
- mode: stateValues.mode ?? defaultModeSlug,
- language: stateValues.language ?? formatLanguage(vscode.env.language),
- mcpEnabled: stateValues.mcpEnabled ?? true,
- enableMcpServerCreation: stateValues.enableMcpServerCreation ?? true,
- mcpServers: this.mcpHub?.getAllServers() ?? [],
- currentApiConfigName: stateValues.currentApiConfigName ?? "default",
- listApiConfigMeta: stateValues.listApiConfigMeta ?? [],
- pinnedApiConfigs: stateValues.pinnedApiConfigs ?? {},
- modeApiConfigs: stateValues.modeApiConfigs ?? ({} as Record<Mode, string>),
- customModePrompts: stateValues.customModePrompts ?? {},
- customSupportPrompts: stateValues.customSupportPrompts ?? {},
- enhancementApiConfigId: stateValues.enhancementApiConfigId,
- experiments: stateValues.experiments ?? experimentDefault,
- autoApprovalEnabled: stateValues.autoApprovalEnabled ?? false,
- customModes,
- maxOpenTabsContext: stateValues.maxOpenTabsContext ?? 20,
- maxWorkspaceFiles: stateValues.maxWorkspaceFiles ?? 200,
- openRouterUseMiddleOutTransform: stateValues.openRouterUseMiddleOutTransform,
- browserToolEnabled: stateValues.browserToolEnabled ?? true,
- telemetrySetting: stateValues.telemetrySetting || "unset",
- showRooIgnoredFiles: stateValues.showRooIgnoredFiles ?? false,
- maxReadFileLine: stateValues.maxReadFileLine ?? -1,
- maxImageFileSize: stateValues.maxImageFileSize ?? 5,
- maxTotalImageSize: stateValues.maxTotalImageSize ?? 20,
- maxConcurrentFileReads: stateValues.maxConcurrentFileReads ?? 5,
- historyPreviewCollapsed: stateValues.historyPreviewCollapsed ?? false,
- reasoningBlockCollapsed: stateValues.reasoningBlockCollapsed ?? true,
- enterBehavior: stateValues.enterBehavior ?? "send",
- cloudUserInfo,
- cloudIsAuthenticated,
- sharingEnabled,
- organizationAllowList,
- organizationSettingsVersion,
- condensingApiConfigId: stateValues.condensingApiConfigId,
- customCondensingPrompt: stateValues.customCondensingPrompt,
- codebaseIndexModels: stateValues.codebaseIndexModels ?? EMBEDDING_MODEL_PROFILES,
- codebaseIndexConfig: {
- codebaseIndexEnabled: stateValues.codebaseIndexConfig?.codebaseIndexEnabled ?? false,
- codebaseIndexQdrantUrl:
- stateValues.codebaseIndexConfig?.codebaseIndexQdrantUrl ?? "http://localhost:6333",
- codebaseIndexEmbedderProvider:
- stateValues.codebaseIndexConfig?.codebaseIndexEmbedderProvider ?? "openai",
- codebaseIndexEmbedderBaseUrl: stateValues.codebaseIndexConfig?.codebaseIndexEmbedderBaseUrl ?? "",
- codebaseIndexEmbedderModelId: stateValues.codebaseIndexConfig?.codebaseIndexEmbedderModelId ?? "",
- codebaseIndexEmbedderModelDimension:
- stateValues.codebaseIndexConfig?.codebaseIndexEmbedderModelDimension,
- codebaseIndexOpenAiCompatibleBaseUrl:
- stateValues.codebaseIndexConfig?.codebaseIndexOpenAiCompatibleBaseUrl,
- codebaseIndexSearchMaxResults: stateValues.codebaseIndexConfig?.codebaseIndexSearchMaxResults,
- codebaseIndexSearchMinScore: stateValues.codebaseIndexConfig?.codebaseIndexSearchMinScore,
- codebaseIndexBedrockRegion: stateValues.codebaseIndexConfig?.codebaseIndexBedrockRegion,
- codebaseIndexBedrockProfile: stateValues.codebaseIndexConfig?.codebaseIndexBedrockProfile,
- codebaseIndexOpenRouterSpecificProvider:
- stateValues.codebaseIndexConfig?.codebaseIndexOpenRouterSpecificProvider,
- },
- profileThresholds: stateValues.profileThresholds ?? {},
- includeDiagnosticMessages: stateValues.includeDiagnosticMessages ?? true,
- maxDiagnosticMessages: stateValues.maxDiagnosticMessages ?? 50,
- includeTaskHistoryInEnhance: stateValues.includeTaskHistoryInEnhance ?? true,
- includeCurrentTime: stateValues.includeCurrentTime ?? true,
- includeCurrentCost: stateValues.includeCurrentCost ?? true,
- maxGitStatusFiles: stateValues.maxGitStatusFiles ?? 0,
- taskSyncEnabled,
- remoteControlEnabled: (() => {
- try {
- const cloudSettings = CloudService.instance.getUserSettings()
- return cloudSettings?.settings?.extensionBridgeEnabled ?? false
- } catch (error) {
- console.error(
- `[getState] failed to get remote control setting from cloud: ${error instanceof Error ? error.message : String(error)}`,
- )
- return false
- }
- })(),
- imageGenerationProvider: stateValues.imageGenerationProvider,
- openRouterImageApiKey: stateValues.openRouterImageApiKey,
- openRouterImageGenerationSelectedModel: stateValues.openRouterImageGenerationSelectedModel,
- featureRoomoteControlEnabled: (() => {
- try {
- const userSettings = CloudService.instance.getUserSettings()
- const hasOrganization = cloudUserInfo?.organizationId != null
- return hasOrganization || (userSettings?.features?.roomoteControlEnabled ?? false)
- } catch (error) {
- console.error(
- `[getState] failed to get featureRoomoteControlEnabled: ${error instanceof Error ? error.message : String(error)}`,
- )
- return false
- }
- })(),
- }
- }
- async updateTaskHistory(item: HistoryItem): Promise<HistoryItem[]> {
- const history = (this.getGlobalState("taskHistory") as HistoryItem[] | undefined) || []
- const existingItemIndex = history.findIndex((h) => h.id === item.id)
- if (existingItemIndex !== -1) {
- // Preserve existing metadata (e.g., delegation fields) unless explicitly overwritten.
- // This prevents loss of status/awaitingChildId/delegatedToId when tasks are reopened,
- // terminated, or when routine message persistence occurs.
- history[existingItemIndex] = {
- ...history[existingItemIndex],
- ...item,
- }
- } else {
- history.push(item)
- }
- await this.updateGlobalState("taskHistory", history)
- this.recentTasksCache = undefined
- return history
- }
- // ContextProxy
- // @deprecated - Use `ContextProxy#setValue` instead.
- private async updateGlobalState<K extends keyof GlobalState>(key: K, value: GlobalState[K]) {
- await this.contextProxy.setValue(key, value)
- }
- // @deprecated - Use `ContextProxy#getValue` instead.
- private getGlobalState<K extends keyof GlobalState>(key: K) {
- return this.contextProxy.getValue(key)
- }
- public async setValue<K extends keyof RooCodeSettings>(key: K, value: RooCodeSettings[K]) {
- await this.contextProxy.setValue(key, value)
- }
- public getValue<K extends keyof RooCodeSettings>(key: K) {
- return this.contextProxy.getValue(key)
- }
- public getValues() {
- return this.contextProxy.getValues()
- }
- public async setValues(values: RooCodeSettings) {
- await this.contextProxy.setValues(values)
- }
- // dev
- async resetState() {
- const answer = await vscode.window.showInformationMessage(
- t("common:confirmation.reset_state"),
- { modal: true },
- t("common:answers.yes"),
- )
- if (answer !== t("common:answers.yes")) {
- return
- }
- // Log out from cloud if authenticated
- if (CloudService.hasInstance()) {
- try {
- await CloudService.instance.logout()
- } catch (error) {
- this.log(
- `Failed to logout from cloud during reset: ${error instanceof Error ? error.message : String(error)}`,
- )
- // Continue with reset even if logout fails
- }
- }
- await this.contextProxy.resetAllState()
- await this.providerSettingsManager.resetAllConfigs()
- await this.customModesManager.resetCustomModes()
- await this.removeClineFromStack()
- await this.postStateToWebview()
- await this.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
- }
- // logging
- public log(message: string) {
- this.outputChannel.appendLine(message)
- console.log(message)
- }
- // getters
- public get workspaceTracker(): WorkspaceTracker | undefined {
- return this._workspaceTracker
- }
- get viewLaunched() {
- return this.isViewLaunched
- }
- get messages() {
- return this.getCurrentTask()?.clineMessages || []
- }
- public getMcpHub(): McpHub | undefined {
- return this.mcpHub
- }
- /**
- * Check if the current state is compliant with MDM policy
- * @returns true if compliant or no MDM policy exists, false if MDM policy exists and user is non-compliant
- */
- public checkMdmCompliance(): boolean {
- if (!this.mdmService) {
- return true // No MDM service, allow operation
- }
- const compliance = this.mdmService.isCompliant()
- if (!compliance.compliant) {
- return false
- }
- return true
- }
- public async remoteControlEnabled(enabled: boolean) {
- if (!enabled) {
- await BridgeOrchestrator.disconnect()
- return
- }
- const userInfo = CloudService.instance.getUserInfo()
- if (!userInfo) {
- this.log("[ClineProvider#remoteControlEnabled] Failed to get user info, disconnecting")
- await BridgeOrchestrator.disconnect()
- return
- }
- const config = await CloudService.instance.cloudAPI?.bridgeConfig().catch(() => undefined)
- if (!config) {
- this.log("[ClineProvider#remoteControlEnabled] Failed to get bridge config")
- return
- }
- await BridgeOrchestrator.connectOrDisconnect(userInfo, enabled, {
- ...config,
- provider: this,
- sessionId: vscode.env.sessionId,
- isCloudAgent: CloudService.instance.isCloudAgent,
- })
- const bridge = BridgeOrchestrator.getInstance()
- if (bridge) {
- const currentTask = this.getCurrentTask()
- if (currentTask && !currentTask.enableBridge) {
- try {
- currentTask.enableBridge = true
- await BridgeOrchestrator.subscribeToTask(currentTask)
- } catch (error) {
- const message = `[ClineProvider#remoteControlEnabled] BridgeOrchestrator.subscribeToTask() failed: ${error instanceof Error ? error.message : String(error)}`
- this.log(message)
- console.error(message)
- }
- }
- } else {
- for (const task of this.clineStack) {
- if (task.enableBridge) {
- try {
- await BridgeOrchestrator.getInstance()?.unsubscribeFromTask(task.taskId)
- } catch (error) {
- const message = `[ClineProvider#remoteControlEnabled] BridgeOrchestrator#unsubscribeFromTask() failed: ${error instanceof Error ? error.message : String(error)}`
- this.log(message)
- console.error(message)
- }
- }
- }
- }
- }
- /**
- * Gets the CodeIndexManager for the current active workspace
- * @returns CodeIndexManager instance for the current workspace or the default one
- */
- public getCurrentWorkspaceCodeIndexManager(): CodeIndexManager | undefined {
- return CodeIndexManager.getInstance(this.context)
- }
- /**
- * Updates the code index status subscription to listen to the current workspace manager
- */
- private updateCodeIndexStatusSubscription(): void {
- // Get the current workspace manager
- const currentManager = this.getCurrentWorkspaceCodeIndexManager()
- // If the manager hasn't changed, no need to update subscription
- if (currentManager === this.codeIndexManager) {
- return
- }
- // Dispose the old subscription if it exists
- if (this.codeIndexStatusSubscription) {
- this.codeIndexStatusSubscription.dispose()
- this.codeIndexStatusSubscription = undefined
- }
- // Update the current workspace manager reference
- this.codeIndexManager = currentManager
- // Subscribe to the new manager's progress updates if it exists
- if (currentManager) {
- this.codeIndexStatusSubscription = currentManager.onProgressUpdate((update: IndexProgressUpdate) => {
- // Only send updates if this manager is still the current one
- if (currentManager === this.getCurrentWorkspaceCodeIndexManager()) {
- // Get the full status from the manager to ensure we have all fields correctly formatted
- const fullStatus = currentManager.getCurrentStatus()
- this.postMessageToWebview({
- type: "indexingStatusUpdate",
- values: fullStatus,
- })
- }
- })
- if (this.view) {
- this.webviewDisposables.push(this.codeIndexStatusSubscription)
- }
- // Send initial status for the current workspace
- this.postMessageToWebview({
- type: "indexingStatusUpdate",
- values: currentManager.getCurrentStatus(),
- })
- }
- }
- /**
- * TaskProviderLike, TelemetryPropertiesProvider
- */
- public getCurrentTask(): Task | undefined {
- if (this.clineStack.length === 0) {
- return undefined
- }
- return this.clineStack[this.clineStack.length - 1]
- }
- public getRecentTasks(): string[] {
- if (this.recentTasksCache) {
- return this.recentTasksCache
- }
- const history = this.getGlobalState("taskHistory") ?? []
- const workspaceTasks: HistoryItem[] = []
- for (const item of history) {
- if (!item.ts || !item.task || item.workspace !== this.cwd) {
- continue
- }
- workspaceTasks.push(item)
- }
- if (workspaceTasks.length === 0) {
- this.recentTasksCache = []
- return this.recentTasksCache
- }
- workspaceTasks.sort((a, b) => b.ts - a.ts)
- let recentTaskIds: string[] = []
- if (workspaceTasks.length >= 100) {
- // If we have at least 100 tasks, return tasks from the last 7 days.
- const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000
- for (const item of workspaceTasks) {
- // Stop when we hit tasks older than 7 days.
- if (item.ts < sevenDaysAgo) {
- break
- }
- recentTaskIds.push(item.id)
- }
- } else {
- // Otherwise, return the most recent 100 tasks (or all if less than 100).
- recentTaskIds = workspaceTasks.slice(0, Math.min(100, workspaceTasks.length)).map((item) => item.id)
- }
- this.recentTasksCache = recentTaskIds
- return this.recentTasksCache
- }
- // When initializing a new task, (not from history but from a tool command
- // new_task) there is no need to remove the previous task since the new
- // task is a subtask of the previous one, and when it finishes it is removed
- // from the stack and the caller is resumed in this way we can have a chain
- // of tasks, each one being a sub task of the previous one until the main
- // task is finished.
- public async createTask(
- text?: string,
- images?: string[],
- parentTask?: Task,
- options: CreateTaskOptions = {},
- configuration: RooCodeSettings = {},
- ): Promise<Task> {
- if (configuration) {
- await this.setValues(configuration)
- if (configuration.allowedCommands) {
- await vscode.workspace
- .getConfiguration(Package.name)
- .update("allowedCommands", configuration.allowedCommands, vscode.ConfigurationTarget.Global)
- }
- if (configuration.deniedCommands) {
- await vscode.workspace
- .getConfiguration(Package.name)
- .update("deniedCommands", configuration.deniedCommands, vscode.ConfigurationTarget.Global)
- }
- if (configuration.commandExecutionTimeout !== undefined) {
- await vscode.workspace
- .getConfiguration(Package.name)
- .update(
- "commandExecutionTimeout",
- configuration.commandExecutionTimeout,
- vscode.ConfigurationTarget.Global,
- )
- }
- if (configuration.currentApiConfigName) {
- await this.setProviderProfile(configuration.currentApiConfigName)
- }
- }
- const {
- apiConfiguration,
- organizationAllowList,
- diffEnabled: enableDiff,
- enableCheckpoints,
- checkpointTimeout,
- fuzzyMatchThreshold,
- experiments,
- cloudUserInfo,
- remoteControlEnabled,
- } = await this.getState()
- // Single-open-task invariant: always enforce for user-initiated top-level tasks
- if (!parentTask) {
- try {
- await this.removeClineFromStack()
- } catch {
- // Non-fatal
- }
- }
- if (!ProfileValidator.isProfileAllowed(apiConfiguration, organizationAllowList)) {
- throw new OrganizationAllowListViolationError(t("common:errors.violated_organization_allowlist"))
- }
- const task = new Task({
- provider: this,
- apiConfiguration,
- enableDiff,
- enableCheckpoints,
- checkpointTimeout,
- fuzzyMatchThreshold,
- consecutiveMistakeLimit: apiConfiguration.consecutiveMistakeLimit,
- task: text,
- images,
- experiments,
- rootTask: this.clineStack.length > 0 ? this.clineStack[0] : undefined,
- parentTask,
- taskNumber: this.clineStack.length + 1,
- onCreated: this.taskCreationCallback,
- enableBridge: BridgeOrchestrator.isEnabled(cloudUserInfo, remoteControlEnabled),
- initialTodos: options.initialTodos,
- ...options,
- })
- await this.addClineToStack(task)
- this.log(
- `[createTask] ${task.parentTask ? "child" : "parent"} task ${task.taskId}.${task.instanceId} instantiated`,
- )
- return task
- }
- public async cancelTask(): Promise<void> {
- const task = this.getCurrentTask()
- if (!task) {
- return
- }
- console.log(`[cancelTask] cancelling task ${task.taskId}.${task.instanceId}`)
- const { historyItem, uiMessagesFilePath } = await this.getTaskWithId(task.taskId)
- // Preserve parent and root task information for history item.
- const rootTask = task.rootTask
- const parentTask = task.parentTask
- // Mark this as a user-initiated cancellation so provider-only rehydration can occur
- task.abortReason = "user_cancelled"
- // Capture the current instance to detect if rehydrate already occurred elsewhere
- const originalInstanceId = task.instanceId
- // Immediately cancel the underlying HTTP request if one is in progress
- // This ensures the stream fails quickly rather than waiting for network timeout
- task.cancelCurrentRequest()
- // Begin abort (non-blocking)
- task.abortTask()
- // Immediately mark the original instance as abandoned to prevent any residual activity
- task.abandoned = true
- await pWaitFor(
- () =>
- this.getCurrentTask()! === undefined ||
- this.getCurrentTask()!.isStreaming === false ||
- this.getCurrentTask()!.didFinishAbortingStream ||
- // If only the first chunk is processed, then there's no
- // need to wait for graceful abort (closes edits, browser,
- // etc).
- this.getCurrentTask()!.isWaitingForFirstChunk,
- {
- timeout: 3_000,
- },
- ).catch(() => {
- console.error("Failed to abort task")
- })
- // Defensive safeguard: if current instance already changed, skip rehydrate
- const current = this.getCurrentTask()
- if (current && current.instanceId !== originalInstanceId) {
- this.log(
- `[cancelTask] Skipping rehydrate: current instance ${current.instanceId} != original ${originalInstanceId}`,
- )
- return
- }
- // Final race check before rehydrate to avoid duplicate rehydration
- {
- const currentAfterCheck = this.getCurrentTask()
- if (currentAfterCheck && currentAfterCheck.instanceId !== originalInstanceId) {
- this.log(
- `[cancelTask] Skipping rehydrate after final check: current instance ${currentAfterCheck.instanceId} != original ${originalInstanceId}`,
- )
- return
- }
- }
- // Clears task again, so we need to abortTask manually above.
- await this.createTaskWithHistoryItem({ ...historyItem, rootTask, parentTask })
- }
- // Clear the current task without treating it as a subtask.
- // This is used when the user cancels a task that is not a subtask.
- public async clearTask(): Promise<void> {
- if (this.clineStack.length > 0) {
- const task = this.clineStack[this.clineStack.length - 1]
- console.log(`[clearTask] clearing task ${task.taskId}.${task.instanceId}`)
- await this.removeClineFromStack()
- }
- }
- public resumeTask(taskId: string): void {
- // Use the existing showTaskWithId method which handles both current and
- // historical tasks.
- this.showTaskWithId(taskId).catch((error) => {
- this.log(`Failed to resume task ${taskId}: ${error.message}`)
- })
- }
- // Modes
- public async getModes(): Promise<{ slug: string; name: string }[]> {
- try {
- const customModes = await this.customModesManager.getCustomModes()
- return [...DEFAULT_MODES, ...customModes].map(({ slug, name }) => ({ slug, name }))
- } catch (error) {
- return DEFAULT_MODES.map(({ slug, name }) => ({ slug, name }))
- }
- }
- public async getMode(): Promise<string> {
- const { mode } = await this.getState()
- return mode
- }
- public async setMode(mode: string): Promise<void> {
- await this.setValues({ mode })
- }
- // Provider Profiles
- public async getProviderProfiles(): Promise<{ name: string; provider?: string }[]> {
- const { listApiConfigMeta = [] } = await this.getState()
- return listApiConfigMeta.map((profile) => ({ name: profile.name, provider: profile.apiProvider }))
- }
- public async getProviderProfile(): Promise<string> {
- const { currentApiConfigName = "default" } = await this.getState()
- return currentApiConfigName
- }
- public async setProviderProfile(name: string): Promise<void> {
- await this.activateProviderProfile({ name })
- }
- // Telemetry
- private _appProperties?: StaticAppProperties
- private _gitProperties?: GitProperties
- private getAppProperties(): StaticAppProperties {
- if (!this._appProperties) {
- const packageJSON = this.context.extension?.packageJSON
- this._appProperties = {
- appName: packageJSON?.name ?? Package.name,
- appVersion: packageJSON?.version ?? Package.version,
- vscodeVersion: vscode.version,
- platform: process.platform,
- editorName: vscode.env.appName,
- }
- }
- return this._appProperties
- }
- public get appProperties(): StaticAppProperties {
- return this._appProperties ?? this.getAppProperties()
- }
- private getCloudProperties(): CloudAppProperties {
- let cloudIsAuthenticated: boolean | undefined
- try {
- if (CloudService.hasInstance()) {
- cloudIsAuthenticated = CloudService.instance.isAuthenticated()
- }
- } catch (error) {
- // Silently handle errors to avoid breaking telemetry collection.
- this.log(`[getTelemetryProperties] Failed to get cloud auth state: ${error}`)
- }
- return {
- cloudIsAuthenticated,
- }
- }
- private async getTaskProperties(): Promise<DynamicAppProperties & TaskProperties> {
- const { language = "en", mode, apiConfiguration } = await this.getState()
- const task = this.getCurrentTask()
- const todoList = task?.todoList
- let todos: { total: number; completed: number; inProgress: number; pending: number } | undefined
- if (todoList && todoList.length > 0) {
- todos = {
- total: todoList.length,
- completed: todoList.filter((todo) => todo.status === "completed").length,
- inProgress: todoList.filter((todo) => todo.status === "in_progress").length,
- pending: todoList.filter((todo) => todo.status === "pending").length,
- }
- }
- return {
- language,
- mode,
- taskId: task?.taskId,
- parentTaskId: task?.parentTaskId,
- apiProvider: apiConfiguration?.apiProvider,
- modelId: task?.api?.getModel().id,
- diffStrategy: task?.diffStrategy?.getName(),
- isSubtask: task ? !!task.parentTaskId : undefined,
- ...(todos && { todos }),
- }
- }
- private async getGitProperties(): Promise<GitProperties> {
- if (!this._gitProperties) {
- this._gitProperties = await getWorkspaceGitInfo()
- }
- return this._gitProperties
- }
- public get gitProperties(): GitProperties | undefined {
- return this._gitProperties
- }
- public async getTelemetryProperties(): Promise<TelemetryProperties> {
- return {
- ...this.getAppProperties(),
- ...this.getCloudProperties(),
- ...(await this.getTaskProperties()),
- ...(await this.getGitProperties()),
- }
- }
- public get cwd() {
- return this.currentWorkspacePath || getWorkspacePath()
- }
- /**
- * Delegate parent task and open child task.
- *
- * - Enforce single-open invariant
- * - Persist parent delegation metadata
- * - Emit TaskDelegated (task-level; API forwards to provider/bridge)
- * - Create child as sole active and switch mode to child's mode
- */
- public async delegateParentAndOpenChild(params: {
- parentTaskId: string
- message: string
- initialTodos: TodoItem[]
- mode: string
- }): Promise<Task> {
- const { parentTaskId, message, initialTodos, mode } = params
- // Metadata-driven delegation is always enabled
- // 1) Get parent (must be current task)
- const parent = this.getCurrentTask()
- if (!parent) {
- throw new Error("[delegateParentAndOpenChild] No current task")
- }
- if (parent.taskId !== parentTaskId) {
- throw new Error(
- `[delegateParentAndOpenChild] Parent mismatch: expected ${parentTaskId}, current ${parent.taskId}`,
- )
- }
- // 2) Flush pending tool results to API history BEFORE disposing the parent.
- // This is critical for native tool protocol: when tools are called before new_task,
- // their tool_result blocks are in userMessageContent but not yet saved to API history.
- // If we don't flush them, the parent's API conversation will be incomplete and
- // cause 400 errors when resumed (missing tool_result for tool_use blocks).
- //
- // NOTE: We do NOT pass the assistant message here because the assistant message
- // is already added to apiConversationHistory by the normal flow in
- // recursivelyMakeClineRequests BEFORE tools start executing. We only need to
- // flush the pending user message with tool_results.
- try {
- await parent.flushPendingToolResultsToHistory()
- } catch (error) {
- this.log(
- `[delegateParentAndOpenChild] Error flushing pending tool results (non-fatal): ${
- error instanceof Error ? error.message : String(error)
- }`,
- )
- }
- // 3) Enforce single-open invariant by closing/disposing the parent first
- // This ensures we never have >1 tasks open at any time during delegation.
- // Await abort completion to ensure clean disposal and prevent unhandled rejections.
- try {
- await this.removeClineFromStack()
- } catch (error) {
- this.log(
- `[delegateParentAndOpenChild] Error during parent disposal (non-fatal): ${
- error instanceof Error ? error.message : String(error)
- }`,
- )
- // Non-fatal: proceed with child creation even if parent cleanup had issues
- }
- // 3) Switch provider mode to child's requested mode BEFORE creating the child task
- // This ensures the child's system prompt and configuration are based on the correct mode.
- // The mode switch must happen before createTask() because the Task constructor
- // initializes its mode from provider.getState() during initializeTaskMode().
- try {
- await this.handleModeSwitch(mode as any)
- } catch (e) {
- this.log(
- `[delegateParentAndOpenChild] handleModeSwitch failed for mode '${mode}': ${
- (e as Error)?.message ?? String(e)
- }`,
- )
- }
- // 4) Create child as sole active (parent reference preserved for lineage)
- // Pass initialStatus: "active" to ensure the child task's historyItem is created
- // with status from the start, avoiding race conditions where the task might
- // call attempt_completion before status is persisted separately.
- const child = await this.createTask(message, undefined, parent as any, {
- initialTodos,
- initialStatus: "active",
- })
- // 5) Persist parent delegation metadata
- try {
- const { historyItem } = await this.getTaskWithId(parentTaskId)
- const childIds = Array.from(new Set([...(historyItem.childIds ?? []), child.taskId]))
- const updatedHistory: typeof historyItem = {
- ...historyItem,
- status: "delegated",
- delegatedToId: child.taskId,
- awaitingChildId: child.taskId,
- childIds,
- }
- await this.updateTaskHistory(updatedHistory)
- } catch (err) {
- this.log(
- `[delegateParentAndOpenChild] Failed to persist parent metadata for ${parentTaskId} -> ${child.taskId}: ${
- (err as Error)?.message ?? String(err)
- }`,
- )
- }
- // 6) Emit TaskDelegated (provider-level)
- try {
- this.emit(RooCodeEventName.TaskDelegated, parentTaskId, child.taskId)
- } catch {
- // non-fatal
- }
- return child
- }
- /**
- * Reopen parent task from delegation with write-back and events.
- */
- public async reopenParentFromDelegation(params: {
- parentTaskId: string
- childTaskId: string
- completionResultSummary: string
- }): Promise<void> {
- const { parentTaskId, childTaskId, completionResultSummary } = params
- const globalStoragePath = this.contextProxy.globalStorageUri.fsPath
- // 1) Load parent from history and current persisted messages
- const { historyItem } = await this.getTaskWithId(parentTaskId)
- let parentClineMessages: ClineMessage[] = []
- try {
- parentClineMessages = await readTaskMessages({
- taskId: parentTaskId,
- globalStoragePath,
- })
- } catch {
- parentClineMessages = []
- }
- let parentApiMessages: any[] = []
- try {
- parentApiMessages = (await readApiMessages({
- taskId: parentTaskId,
- globalStoragePath,
- })) as any[]
- } catch {
- parentApiMessages = []
- }
- // 2) Inject synthetic records: UI subtask_result and update API tool_result
- const ts = Date.now()
- // Defensive: ensure arrays
- if (!Array.isArray(parentClineMessages)) parentClineMessages = []
- if (!Array.isArray(parentApiMessages)) parentApiMessages = []
- const subtaskUiMessage: ClineMessage = {
- type: "say",
- say: "subtask_result",
- text: completionResultSummary,
- ts,
- }
- parentClineMessages.push(subtaskUiMessage)
- await saveTaskMessages({ messages: parentClineMessages, taskId: parentTaskId, globalStoragePath })
- // Find the tool_use_id from the last assistant message's new_task tool_use
- let toolUseId: string | undefined
- for (let i = parentApiMessages.length - 1; i >= 0; i--) {
- const msg = parentApiMessages[i]
- if (msg.role === "assistant" && Array.isArray(msg.content)) {
- for (const block of msg.content) {
- if (block.type === "tool_use" && block.name === "new_task") {
- toolUseId = block.id
- break
- }
- }
- if (toolUseId) break
- }
- }
- // The API expects: user → assistant (with tool_use) → user (with tool_result)
- // We need to add a NEW user message with the tool_result AFTER the assistant's tool_use
- // NOT add it to an existing user message
- if (toolUseId) {
- // Check if the last message is already a user message with a tool_result for this tool_use_id
- // (in case this is a retry or the history was already updated)
- const lastMsg = parentApiMessages[parentApiMessages.length - 1]
- let alreadyHasToolResult = false
- if (lastMsg?.role === "user" && Array.isArray(lastMsg.content)) {
- for (const block of lastMsg.content) {
- if (block.type === "tool_result" && block.tool_use_id === toolUseId) {
- // Update the existing tool_result content
- block.content = `Subtask ${childTaskId} completed.\n\nResult:\n${completionResultSummary}`
- alreadyHasToolResult = true
- break
- }
- }
- }
- // If no existing tool_result found, create a NEW user message with the tool_result
- if (!alreadyHasToolResult) {
- parentApiMessages.push({
- role: "user",
- content: [
- {
- type: "tool_result" as const,
- tool_use_id: toolUseId,
- content: `Subtask ${childTaskId} completed.\n\nResult:\n${completionResultSummary}`,
- },
- ],
- ts,
- })
- }
- } else {
- // Fallback for XML protocol or when toolUseId couldn't be found:
- // Add a text block (not ideal but maintains backward compatibility)
- parentApiMessages.push({
- role: "user",
- content: [
- {
- type: "text",
- text: `Subtask ${childTaskId} completed.\n\nResult:\n${completionResultSummary}`,
- },
- ],
- ts,
- })
- }
- await saveApiMessages({ messages: parentApiMessages as any, taskId: parentTaskId, globalStoragePath })
- // 3) Update child metadata to "completed" status
- try {
- const { historyItem: childHistory } = await this.getTaskWithId(childTaskId)
- await this.updateTaskHistory({
- ...childHistory,
- status: "completed",
- })
- } catch (err) {
- this.log(
- `[reopenParentFromDelegation] Failed to persist child completed status for ${childTaskId}: ${
- (err as Error)?.message ?? String(err)
- }`,
- )
- }
- // 4) Update parent metadata and persist BEFORE emitting completion event
- const childIds = Array.from(new Set([...(historyItem.childIds ?? []), childTaskId]))
- const updatedHistory: typeof historyItem = {
- ...historyItem,
- status: "active",
- completedByChildId: childTaskId,
- completionResultSummary,
- awaitingChildId: undefined,
- childIds,
- }
- await this.updateTaskHistory(updatedHistory)
- // 5) Emit TaskDelegationCompleted (provider-level)
- try {
- this.emit(RooCodeEventName.TaskDelegationCompleted, parentTaskId, childTaskId, completionResultSummary)
- } catch {
- // non-fatal
- }
- // 6) Close child instance if still open (single-open-task invariant)
- const current = this.getCurrentTask()
- if (current?.taskId === childTaskId) {
- await this.removeClineFromStack()
- }
- // 7) Reopen the parent from history as the sole active task (restores saved mode)
- // IMPORTANT: startTask=false to suppress resume-from-history ask scheduling
- const parentInstance = await this.createTaskWithHistoryItem(updatedHistory, { startTask: false })
- // 8) Inject restored histories into the in-memory instance before resuming
- if (parentInstance) {
- try {
- await parentInstance.overwriteClineMessages(parentClineMessages)
- } catch {
- // non-fatal
- }
- try {
- await parentInstance.overwriteApiConversationHistory(parentApiMessages as any)
- } catch {
- // non-fatal
- }
- // Auto-resume parent without ask("resume_task")
- await parentInstance.resumeAfterDelegation()
- }
- // 9) Emit TaskDelegationResumed (provider-level)
- try {
- this.emit(RooCodeEventName.TaskDelegationResumed, parentTaskId, childTaskId)
- } catch {
- // non-fatal
- }
- }
- /**
- * Convert a file path to a webview-accessible URI
- * This method safely converts file paths to URIs that can be loaded in the webview
- *
- * @param filePath - The absolute file path to convert
- * @returns The webview URI string, or the original file URI if conversion fails
- * @throws {Error} When webview is not available
- * @throws {TypeError} When file path is invalid
- */
- public convertToWebviewUri(filePath: string): string {
- try {
- const fileUri = vscode.Uri.file(filePath)
- // Check if we have a webview available
- if (this.view?.webview) {
- const webviewUri = this.view.webview.asWebviewUri(fileUri)
- return webviewUri.toString()
- }
- // Specific error for no webview available
- const error = new Error("No webview available for URI conversion")
- console.error(error.message)
- // Fallback to file URI if no webview available
- return fileUri.toString()
- } catch (error) {
- // More specific error handling
- if (error instanceof TypeError) {
- console.error("Invalid file path provided for URI conversion:", error)
- } else {
- console.error("Failed to convert to webview URI:", error)
- }
- // Return file URI as fallback
- return vscode.Uri.file(filePath).toString()
- }
- }
- }
|