extension.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. import * as vscode from "vscode"
  2. import * as dotenvx from "@dotenvx/dotenvx"
  3. import * as path from "path"
  4. // Load environment variables from .env file
  5. try {
  6. // Specify path to .env file in the project root directory
  7. const envPath = path.join(__dirname, "..", ".env")
  8. dotenvx.config({ path: envPath })
  9. } catch (e) {
  10. // Silently handle environment loading errors
  11. console.warn("Failed to load environment variables:", e)
  12. }
  13. import type { CloudUserInfo, AuthState } from "@roo-code/types"
  14. import { CloudService, BridgeOrchestrator } from "@roo-code/cloud"
  15. import { TelemetryService, PostHogTelemetryClient } from "@roo-code/telemetry"
  16. import { customToolRegistry } from "@roo-code/core"
  17. import "./utils/path" // Necessary to have access to String.prototype.toPosix.
  18. import { createOutputChannelLogger, createDualLogger } from "./utils/outputChannelLogger"
  19. import { initializeNetworkProxy } from "./utils/networkProxy"
  20. import { Package } from "./shared/package"
  21. import { formatLanguage } from "./shared/language"
  22. import { ContextProxy } from "./core/config/ContextProxy"
  23. import { ClineProvider } from "./core/webview/ClineProvider"
  24. import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider"
  25. import { TerminalRegistry } from "./integrations/terminal/TerminalRegistry"
  26. import { claudeCodeOAuthManager } from "./integrations/claude-code/oauth"
  27. import { McpServerManager } from "./services/mcp/McpServerManager"
  28. import { CodeIndexManager } from "./services/code-index/manager"
  29. import { MdmService } from "./services/mdm/MdmService"
  30. import { migrateSettings } from "./utils/migrateSettings"
  31. import { autoImportSettings } from "./utils/autoImportSettings"
  32. import { API } from "./extension/api"
  33. import {
  34. handleUri,
  35. registerCommands,
  36. registerCodeActions,
  37. registerTerminalActions,
  38. CodeActionProvider,
  39. } from "./activate"
  40. import { initializeI18n } from "./i18n"
  41. import { flushModels, initializeModelCacheRefresh, refreshModels } from "./api/providers/fetchers/modelCache"
  42. /**
  43. * Built using https://github.com/microsoft/vscode-webview-ui-toolkit
  44. *
  45. * Inspired by:
  46. * - https://github.com/microsoft/vscode-webview-ui-toolkit-samples/tree/main/default/weather-webview
  47. * - https://github.com/microsoft/vscode-webview-ui-toolkit-samples/tree/main/frameworks/hello-world-react-cra
  48. */
  49. let outputChannel: vscode.OutputChannel
  50. let extensionContext: vscode.ExtensionContext
  51. let cloudService: CloudService | undefined
  52. let authStateChangedHandler: ((data: { state: AuthState; previousState: AuthState }) => Promise<void>) | undefined
  53. let settingsUpdatedHandler: (() => void) | undefined
  54. let userInfoHandler: ((data: { userInfo: CloudUserInfo }) => Promise<void>) | undefined
  55. // This method is called when your extension is activated.
  56. // Your extension is activated the very first time the command is executed.
  57. export async function activate(context: vscode.ExtensionContext) {
  58. extensionContext = context
  59. outputChannel = vscode.window.createOutputChannel(Package.outputChannel)
  60. context.subscriptions.push(outputChannel)
  61. outputChannel.appendLine(`${Package.name} extension activated - ${JSON.stringify(Package)}`)
  62. // Initialize network proxy configuration early, before any network requests.
  63. // When proxyUrl is configured, all HTTP/HTTPS traffic will be routed through it.
  64. // Only applied in debug mode (F5).
  65. await initializeNetworkProxy(context, outputChannel)
  66. // Set extension path for custom tool registry to find bundled esbuild
  67. customToolRegistry.setExtensionPath(context.extensionPath)
  68. // Migrate old settings to new
  69. await migrateSettings(context, outputChannel)
  70. // Initialize telemetry service.
  71. const telemetryService = TelemetryService.createInstance()
  72. try {
  73. telemetryService.register(new PostHogTelemetryClient())
  74. } catch (error) {
  75. console.warn("Failed to register PostHogTelemetryClient:", error)
  76. }
  77. // Create logger for cloud services.
  78. const cloudLogger = createDualLogger(createOutputChannelLogger(outputChannel))
  79. // Initialize MDM service
  80. const mdmService = await MdmService.createInstance(cloudLogger)
  81. // Initialize i18n for internationalization support.
  82. initializeI18n(context.globalState.get("language") ?? formatLanguage(vscode.env.language))
  83. // Initialize terminal shell execution handlers.
  84. TerminalRegistry.initialize()
  85. // Initialize Claude Code OAuth manager for direct API access.
  86. claudeCodeOAuthManager.initialize(context, (message) => outputChannel.appendLine(message))
  87. // Get default commands from configuration.
  88. const defaultCommands = vscode.workspace.getConfiguration(Package.name).get<string[]>("allowedCommands") || []
  89. // Initialize global state if not already set.
  90. if (!context.globalState.get("allowedCommands")) {
  91. context.globalState.update("allowedCommands", defaultCommands)
  92. }
  93. const contextProxy = await ContextProxy.getInstance(context)
  94. // Initialize code index managers for all workspace folders.
  95. const codeIndexManagers: CodeIndexManager[] = []
  96. if (vscode.workspace.workspaceFolders) {
  97. for (const folder of vscode.workspace.workspaceFolders) {
  98. const manager = CodeIndexManager.getInstance(context, folder.uri.fsPath)
  99. if (manager) {
  100. codeIndexManagers.push(manager)
  101. // Initialize in background; do not block extension activation
  102. void manager.initialize(contextProxy).catch((error) => {
  103. const message = error instanceof Error ? error.message : String(error)
  104. outputChannel.appendLine(
  105. `[CodeIndexManager] Error during background CodeIndexManager configuration/indexing for ${folder.uri.fsPath}: ${message}`,
  106. )
  107. })
  108. context.subscriptions.push(manager)
  109. }
  110. }
  111. }
  112. // Initialize the provider *before* the Roo Code Cloud service.
  113. const provider = new ClineProvider(context, outputChannel, "sidebar", contextProxy, mdmService)
  114. // Initialize Roo Code Cloud service.
  115. const postStateListener = () => ClineProvider.getVisibleInstance()?.postStateToWebview()
  116. authStateChangedHandler = async (data: { state: AuthState; previousState: AuthState }) => {
  117. postStateListener()
  118. if (data.state === "logged-out") {
  119. try {
  120. await provider.remoteControlEnabled(false)
  121. } catch (error) {
  122. cloudLogger(
  123. `[authStateChangedHandler] remoteControlEnabled(false) failed: ${error instanceof Error ? error.message : String(error)}`,
  124. )
  125. }
  126. }
  127. // Handle Roo models cache based on auth state (ROO-202)
  128. const handleRooModelsCache = async () => {
  129. try {
  130. if (data.state === "active-session") {
  131. // Refresh with auth token to get authenticated models
  132. const sessionToken = CloudService.hasInstance()
  133. ? CloudService.instance.authService?.getSessionToken()
  134. : undefined
  135. await refreshModels({
  136. provider: "roo",
  137. baseUrl: process.env.ROO_CODE_PROVIDER_URL ?? "https://api.roocode.com/proxy",
  138. apiKey: sessionToken,
  139. })
  140. } else {
  141. // Flush without refresh on logout
  142. await flushModels({ provider: "roo" }, false)
  143. }
  144. } catch (error) {
  145. cloudLogger(
  146. `[authStateChangedHandler] Failed to handle Roo models cache: ${error instanceof Error ? error.message : String(error)}`,
  147. )
  148. }
  149. }
  150. if (data.state === "active-session" || data.state === "logged-out") {
  151. await handleRooModelsCache()
  152. // Apply stored provider model to API configuration if present
  153. if (data.state === "active-session") {
  154. try {
  155. const storedModel = context.globalState.get<string>("roo-provider-model")
  156. if (storedModel) {
  157. cloudLogger(`[authStateChangedHandler] Applying stored provider model: ${storedModel}`)
  158. // Get the current API configuration name
  159. const currentConfigName =
  160. provider.contextProxy.getGlobalState("currentApiConfigName") || "default"
  161. // Update it with the stored model using upsertProviderProfile
  162. await provider.upsertProviderProfile(currentConfigName, {
  163. apiProvider: "roo",
  164. apiModelId: storedModel,
  165. })
  166. // Clear the stored model after applying
  167. await context.globalState.update("roo-provider-model", undefined)
  168. cloudLogger(`[authStateChangedHandler] Applied and cleared stored provider model`)
  169. }
  170. } catch (error) {
  171. cloudLogger(
  172. `[authStateChangedHandler] Failed to apply stored provider model: ${error instanceof Error ? error.message : String(error)}`,
  173. )
  174. }
  175. }
  176. }
  177. }
  178. settingsUpdatedHandler = async () => {
  179. const userInfo = CloudService.instance.getUserInfo()
  180. if (userInfo && CloudService.instance.cloudAPI) {
  181. try {
  182. provider.remoteControlEnabled(CloudService.instance.isTaskSyncEnabled())
  183. } catch (error) {
  184. cloudLogger(
  185. `[settingsUpdatedHandler] remoteControlEnabled failed: ${error instanceof Error ? error.message : String(error)}`,
  186. )
  187. }
  188. }
  189. postStateListener()
  190. }
  191. userInfoHandler = async ({ userInfo }: { userInfo: CloudUserInfo }) => {
  192. postStateListener()
  193. if (!CloudService.instance.cloudAPI) {
  194. cloudLogger("[userInfoHandler] CloudAPI is not initialized")
  195. return
  196. }
  197. try {
  198. provider.remoteControlEnabled(CloudService.instance.isTaskSyncEnabled())
  199. } catch (error) {
  200. cloudLogger(
  201. `[userInfoHandler] remoteControlEnabled failed: ${error instanceof Error ? error.message : String(error)}`,
  202. )
  203. }
  204. }
  205. cloudService = await CloudService.createInstance(context, cloudLogger, {
  206. "auth-state-changed": authStateChangedHandler,
  207. "settings-updated": settingsUpdatedHandler,
  208. "user-info": userInfoHandler,
  209. })
  210. try {
  211. if (cloudService.telemetryClient) {
  212. TelemetryService.instance.register(cloudService.telemetryClient)
  213. }
  214. } catch (error) {
  215. outputChannel.appendLine(
  216. `[CloudService] Failed to register TelemetryClient: ${error instanceof Error ? error.message : String(error)}`,
  217. )
  218. }
  219. // Add to subscriptions for proper cleanup on deactivate.
  220. context.subscriptions.push(cloudService)
  221. // Trigger initial cloud profile sync now that CloudService is ready.
  222. try {
  223. await provider.initializeCloudProfileSyncWhenReady()
  224. } catch (error) {
  225. outputChannel.appendLine(
  226. `[CloudService] Failed to initialize cloud profile sync: ${error instanceof Error ? error.message : String(error)}`,
  227. )
  228. }
  229. // Finish initializing the provider.
  230. TelemetryService.instance.setProvider(provider)
  231. context.subscriptions.push(
  232. vscode.window.registerWebviewViewProvider(ClineProvider.sideBarId, provider, {
  233. webviewOptions: { retainContextWhenHidden: true },
  234. }),
  235. )
  236. // Auto-import configuration if specified in settings.
  237. try {
  238. await autoImportSettings(outputChannel, {
  239. providerSettingsManager: provider.providerSettingsManager,
  240. contextProxy: provider.contextProxy,
  241. customModesManager: provider.customModesManager,
  242. })
  243. } catch (error) {
  244. outputChannel.appendLine(
  245. `[AutoImport] Error during auto-import: ${error instanceof Error ? error.message : String(error)}`,
  246. )
  247. }
  248. registerCommands({ context, outputChannel, provider })
  249. /**
  250. * We use the text document content provider API to show the left side for diff
  251. * view by creating a virtual document for the original content. This makes it
  252. * readonly so users know to edit the right side if they want to keep their changes.
  253. *
  254. * This API allows you to create readonly documents in VSCode from arbitrary
  255. * sources, and works by claiming an uri-scheme for which your provider then
  256. * returns text contents. The scheme must be provided when registering a
  257. * provider and cannot change afterwards.
  258. *
  259. * Note how the provider doesn't create uris for virtual documents - its role
  260. * is to provide contents given such an uri. In return, content providers are
  261. * wired into the open document logic so that providers are always considered.
  262. *
  263. * https://code.visualstudio.com/api/extension-guides/virtual-documents
  264. */
  265. const diffContentProvider = new (class implements vscode.TextDocumentContentProvider {
  266. provideTextDocumentContent(uri: vscode.Uri): string {
  267. return Buffer.from(uri.query, "base64").toString("utf-8")
  268. }
  269. })()
  270. context.subscriptions.push(
  271. vscode.workspace.registerTextDocumentContentProvider(DIFF_VIEW_URI_SCHEME, diffContentProvider),
  272. )
  273. context.subscriptions.push(vscode.window.registerUriHandler({ handleUri }))
  274. // Register code actions provider.
  275. context.subscriptions.push(
  276. vscode.languages.registerCodeActionsProvider({ pattern: "**/*" }, new CodeActionProvider(), {
  277. providedCodeActionKinds: CodeActionProvider.providedCodeActionKinds,
  278. }),
  279. )
  280. registerCodeActions(context)
  281. registerTerminalActions(context)
  282. // Allows other extensions to activate once Roo is ready.
  283. vscode.commands.executeCommand(`${Package.name}.activationCompleted`)
  284. // Implements the `RooCodeAPI` interface.
  285. const socketPath = process.env.ROO_CODE_IPC_SOCKET_PATH
  286. const enableLogging = typeof socketPath === "string"
  287. // Watch the core files and automatically reload the extension host.
  288. if (process.env.NODE_ENV === "development") {
  289. const watchPaths = [
  290. { path: context.extensionPath, pattern: "**/*.ts" },
  291. { path: path.join(context.extensionPath, "../packages/types"), pattern: "**/*.ts" },
  292. { path: path.join(context.extensionPath, "../packages/telemetry"), pattern: "**/*.ts" },
  293. { path: path.join(context.extensionPath, "node_modules/@roo-code/cloud"), pattern: "**/*" },
  294. ]
  295. console.log(
  296. `♻️♻️♻️ Core auto-reloading: Watching for changes in ${watchPaths.map(({ path }) => path).join(", ")}`,
  297. )
  298. // Create a debounced reload function to prevent excessive reloads
  299. let reloadTimeout: NodeJS.Timeout | undefined
  300. const DEBOUNCE_DELAY = 1_000
  301. const debouncedReload = (uri: vscode.Uri) => {
  302. if (reloadTimeout) {
  303. clearTimeout(reloadTimeout)
  304. }
  305. console.log(`♻️ ${uri.fsPath} changed; scheduling reload...`)
  306. reloadTimeout = setTimeout(() => {
  307. console.log(`♻️ Reloading host after debounce delay...`)
  308. vscode.commands.executeCommand("workbench.action.reloadWindow")
  309. }, DEBOUNCE_DELAY)
  310. }
  311. watchPaths.forEach(({ path: watchPath, pattern }) => {
  312. const relPattern = new vscode.RelativePattern(vscode.Uri.file(watchPath), pattern)
  313. const watcher = vscode.workspace.createFileSystemWatcher(relPattern, false, false, false)
  314. // Listen to all change types to ensure symlinked file updates trigger reloads.
  315. watcher.onDidChange(debouncedReload)
  316. watcher.onDidCreate(debouncedReload)
  317. watcher.onDidDelete(debouncedReload)
  318. context.subscriptions.push(watcher)
  319. })
  320. // Clean up the timeout on deactivation
  321. context.subscriptions.push({
  322. dispose: () => {
  323. if (reloadTimeout) {
  324. clearTimeout(reloadTimeout)
  325. }
  326. },
  327. })
  328. }
  329. // Initialize background model cache refresh
  330. initializeModelCacheRefresh()
  331. return new API(outputChannel, provider, socketPath, enableLogging)
  332. }
  333. // This method is called when your extension is deactivated.
  334. export async function deactivate() {
  335. outputChannel.appendLine(`${Package.name} extension deactivated`)
  336. if (cloudService && CloudService.hasInstance()) {
  337. try {
  338. if (authStateChangedHandler) {
  339. CloudService.instance.off("auth-state-changed", authStateChangedHandler)
  340. }
  341. if (settingsUpdatedHandler) {
  342. CloudService.instance.off("settings-updated", settingsUpdatedHandler)
  343. }
  344. if (userInfoHandler) {
  345. CloudService.instance.off("user-info", userInfoHandler as any)
  346. }
  347. outputChannel.appendLine("CloudService event handlers cleaned up")
  348. } catch (error) {
  349. outputChannel.appendLine(
  350. `Failed to clean up CloudService event handlers: ${error instanceof Error ? error.message : String(error)}`,
  351. )
  352. }
  353. }
  354. const bridge = BridgeOrchestrator.getInstance()
  355. if (bridge) {
  356. await bridge.disconnect()
  357. }
  358. await McpServerManager.cleanup(extensionContext)
  359. TelemetryService.instance.shutdown()
  360. TerminalRegistry.cleanup()
  361. }