extension.ts 15 KB

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