registerCommands.ts 8.5 KB

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