registerCommands.ts 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. import * as vscode from "vscode"
  2. import delay from "delay"
  3. import type { CommandId } from "@roo-code/types"
  4. import { TelemetryService } from "@roo-code/telemetry"
  5. import { Package } from "../shared/package"
  6. import { getCommand } from "../utils/commands"
  7. import { ClineProvider } from "../core/webview/ClineProvider"
  8. import { ContextProxy } from "../core/config/ContextProxy"
  9. import { focusPanel } from "../utils/focusPanel"
  10. import { registerHumanRelayCallback, unregisterHumanRelayCallback, handleHumanRelayResponse } from "./humanRelay"
  11. import { handleNewTask } from "./handleTask"
  12. import { CodeIndexManager } from "../services/code-index/manager"
  13. import { importSettingsWithFeedback } from "../core/config/importExport"
  14. import { MdmService } from "../services/mdm/MdmService"
  15. import { t } from "../i18n"
  16. /**
  17. * Helper to get the visible ClineProvider instance or log if not found.
  18. */
  19. export function getVisibleProviderOrLog(outputChannel: vscode.OutputChannel): ClineProvider | undefined {
  20. const visibleProvider = ClineProvider.getVisibleInstance()
  21. if (!visibleProvider) {
  22. outputChannel.appendLine("Cannot find any visible Roo Code instances.")
  23. return undefined
  24. }
  25. return visibleProvider
  26. }
  27. // Store panel references in both modes
  28. let sidebarPanel: vscode.WebviewView | undefined = undefined
  29. let tabPanel: vscode.WebviewPanel | undefined = undefined
  30. /**
  31. * Get the currently active panel
  32. * @returns WebviewPanel或WebviewView
  33. */
  34. export function getPanel(): vscode.WebviewPanel | vscode.WebviewView | undefined {
  35. return tabPanel || sidebarPanel
  36. }
  37. /**
  38. * Set panel references
  39. */
  40. export function setPanel(
  41. newPanel: vscode.WebviewPanel | vscode.WebviewView | undefined,
  42. type: "sidebar" | "tab",
  43. ): void {
  44. if (type === "sidebar") {
  45. sidebarPanel = newPanel as vscode.WebviewView
  46. tabPanel = undefined
  47. } else {
  48. tabPanel = newPanel as vscode.WebviewPanel
  49. sidebarPanel = undefined
  50. }
  51. }
  52. export type RegisterCommandOptions = {
  53. context: vscode.ExtensionContext
  54. outputChannel: vscode.OutputChannel
  55. provider: ClineProvider
  56. }
  57. export const registerCommands = (options: RegisterCommandOptions) => {
  58. const { context } = options
  59. for (const [id, callback] of Object.entries(getCommandsMap(options))) {
  60. const command = getCommand(id as CommandId)
  61. context.subscriptions.push(vscode.commands.registerCommand(command, callback))
  62. }
  63. }
  64. const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOptions): Record<CommandId, any> => ({
  65. activationCompleted: () => {},
  66. accountButtonClicked: () => {
  67. const visibleProvider = getVisibleProviderOrLog(outputChannel)
  68. if (!visibleProvider) {
  69. return
  70. }
  71. TelemetryService.instance.captureTitleButtonClicked("account")
  72. visibleProvider.postMessageToWebview({ type: "action", action: "accountButtonClicked" })
  73. },
  74. plusButtonClicked: async () => {
  75. const visibleProvider = getVisibleProviderOrLog(outputChannel)
  76. if (!visibleProvider) {
  77. return
  78. }
  79. TelemetryService.instance.captureTitleButtonClicked("plus")
  80. await visibleProvider.removeClineFromStack()
  81. await visibleProvider.postStateToWebview()
  82. await visibleProvider.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
  83. },
  84. mcpButtonClicked: () => {
  85. const visibleProvider = getVisibleProviderOrLog(outputChannel)
  86. if (!visibleProvider) {
  87. return
  88. }
  89. TelemetryService.instance.captureTitleButtonClicked("mcp")
  90. visibleProvider.postMessageToWebview({ type: "action", action: "mcpButtonClicked" })
  91. },
  92. promptsButtonClicked: () => {
  93. const visibleProvider = getVisibleProviderOrLog(outputChannel)
  94. if (!visibleProvider) {
  95. return
  96. }
  97. TelemetryService.instance.captureTitleButtonClicked("prompts")
  98. visibleProvider.postMessageToWebview({ type: "action", action: "promptsButtonClicked" })
  99. },
  100. popoutButtonClicked: () => {
  101. TelemetryService.instance.captureTitleButtonClicked("popout")
  102. return openClineInNewTab({ context, outputChannel })
  103. },
  104. openInNewTab: () => openClineInNewTab({ context, outputChannel }),
  105. settingsButtonClicked: () => {
  106. const visibleProvider = getVisibleProviderOrLog(outputChannel)
  107. if (!visibleProvider) {
  108. return
  109. }
  110. TelemetryService.instance.captureTitleButtonClicked("settings")
  111. visibleProvider.postMessageToWebview({ type: "action", action: "settingsButtonClicked" })
  112. // Also explicitly post the visibility message to trigger scroll reliably
  113. visibleProvider.postMessageToWebview({ type: "action", action: "didBecomeVisible" })
  114. },
  115. historyButtonClicked: () => {
  116. const visibleProvider = getVisibleProviderOrLog(outputChannel)
  117. if (!visibleProvider) {
  118. return
  119. }
  120. TelemetryService.instance.captureTitleButtonClicked("history")
  121. visibleProvider.postMessageToWebview({ type: "action", action: "historyButtonClicked" })
  122. },
  123. marketplaceButtonClicked: () => {
  124. const visibleProvider = getVisibleProviderOrLog(outputChannel)
  125. if (!visibleProvider) return
  126. visibleProvider.postMessageToWebview({ type: "action", action: "marketplaceButtonClicked" })
  127. },
  128. showHumanRelayDialog: (params: { requestId: string; promptText: string }) => {
  129. const panel = getPanel()
  130. if (panel) {
  131. panel?.webview.postMessage({
  132. type: "showHumanRelayDialog",
  133. requestId: params.requestId,
  134. promptText: params.promptText,
  135. })
  136. }
  137. },
  138. registerHumanRelayCallback: registerHumanRelayCallback,
  139. unregisterHumanRelayCallback: unregisterHumanRelayCallback,
  140. handleHumanRelayResponse: handleHumanRelayResponse,
  141. newTask: handleNewTask,
  142. setCustomStoragePath: async () => {
  143. const { promptForCustomStoragePath } = await import("../utils/storage")
  144. await promptForCustomStoragePath()
  145. },
  146. importSettings: async (filePath?: string) => {
  147. const visibleProvider = getVisibleProviderOrLog(outputChannel)
  148. if (!visibleProvider) {
  149. return
  150. }
  151. await importSettingsWithFeedback(
  152. {
  153. providerSettingsManager: visibleProvider.providerSettingsManager,
  154. contextProxy: visibleProvider.contextProxy,
  155. customModesManager: visibleProvider.customModesManager,
  156. provider: visibleProvider,
  157. },
  158. filePath,
  159. )
  160. },
  161. focusInput: async () => {
  162. try {
  163. await focusPanel(tabPanel, sidebarPanel)
  164. // Send focus input message only for sidebar panels
  165. if (sidebarPanel && getPanel() === sidebarPanel) {
  166. provider.postMessageToWebview({ type: "action", action: "focusInput" })
  167. }
  168. } catch (error) {
  169. outputChannel.appendLine(`Error focusing input: ${error}`)
  170. }
  171. },
  172. focusPanel: async () => {
  173. try {
  174. await focusPanel(tabPanel, sidebarPanel)
  175. } catch (error) {
  176. outputChannel.appendLine(`Error focusing panel: ${error}`)
  177. }
  178. },
  179. acceptInput: () => {
  180. const visibleProvider = getVisibleProviderOrLog(outputChannel)
  181. if (!visibleProvider) {
  182. return
  183. }
  184. visibleProvider.postMessageToWebview({ type: "acceptInput" })
  185. },
  186. })
  187. export const openClineInNewTab = async ({ context, outputChannel }: Omit<RegisterCommandOptions, "provider">) => {
  188. // (This example uses webviewProvider activation event which is necessary to
  189. // deserialize cached webview, but since we use retainContextWhenHidden, we
  190. // don't need to use that event).
  191. // https://github.com/microsoft/vscode-extension-samples/blob/main/webview-sample/src/extension.ts
  192. const contextProxy = await ContextProxy.getInstance(context)
  193. const codeIndexManager = CodeIndexManager.getInstance(context)
  194. // Get the existing MDM service instance to ensure consistent policy enforcement
  195. let mdmService: MdmService | undefined
  196. try {
  197. mdmService = MdmService.getInstance()
  198. } catch (error) {
  199. // MDM service not initialized, which is fine - extension can work without it
  200. mdmService = undefined
  201. }
  202. const tabProvider = new ClineProvider(context, outputChannel, "editor", contextProxy, codeIndexManager, mdmService)
  203. const lastCol = Math.max(...vscode.window.visibleTextEditors.map((editor) => editor.viewColumn || 0))
  204. // Check if there are any visible text editors, otherwise open a new group
  205. // to the right.
  206. const hasVisibleEditors = vscode.window.visibleTextEditors.length > 0
  207. if (!hasVisibleEditors) {
  208. await vscode.commands.executeCommand("workbench.action.newGroupRight")
  209. }
  210. const targetCol = hasVisibleEditors ? Math.max(lastCol + 1, 1) : vscode.ViewColumn.Two
  211. const newPanel = vscode.window.createWebviewPanel(ClineProvider.tabPanelId, "Roo Code", targetCol, {
  212. enableScripts: true,
  213. retainContextWhenHidden: true,
  214. localResourceRoots: [context.extensionUri],
  215. })
  216. // Save as tab type panel.
  217. setPanel(newPanel, "tab")
  218. // TODO: Use better svg icon with light and dark variants (see
  219. // https://stackoverflow.com/questions/58365687/vscode-extension-iconpath).
  220. newPanel.iconPath = {
  221. light: vscode.Uri.joinPath(context.extensionUri, "assets", "icons", "panel_light.png"),
  222. dark: vscode.Uri.joinPath(context.extensionUri, "assets", "icons", "panel_dark.png"),
  223. }
  224. await tabProvider.resolveWebviewView(newPanel)
  225. // Add listener for visibility changes to notify webview
  226. newPanel.onDidChangeViewState(
  227. (e) => {
  228. const panel = e.webviewPanel
  229. if (panel.visible) {
  230. panel.webview.postMessage({ type: "action", action: "didBecomeVisible" }) // Use the same message type as in SettingsView.tsx
  231. }
  232. },
  233. null, // First null is for `thisArgs`
  234. context.subscriptions, // Register listener for disposal
  235. )
  236. // Handle panel closing events.
  237. newPanel.onDidDispose(
  238. () => {
  239. setPanel(undefined, "tab")
  240. },
  241. null,
  242. context.subscriptions, // Also register dispose listener
  243. )
  244. // Lock the editor group so clicking on files doesn't open them over the panel.
  245. await delay(100)
  246. await vscode.commands.executeCommand("workbench.action.lockEditorGroup")
  247. return tabProvider
  248. }