import * as vscode from "vscode" import delay from "delay" import type { CommandId } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" import { Package } from "../shared/package" import { getCommand } from "../utils/commands" import { ClineProvider } from "../core/webview/ClineProvider" import { ContextProxy } from "../core/config/ContextProxy" import { focusPanel } from "../utils/focusPanel" import { handleNewTask } from "./handleTask" import { CodeIndexManager } from "../services/code-index/manager" import { importSettingsWithFeedback } from "../core/config/importExport" import { MdmService } from "../services/mdm/MdmService" import { t } from "../i18n" /** * Helper to get the visible ClineProvider instance or log if not found. */ export function getVisibleProviderOrLog(outputChannel: vscode.OutputChannel): ClineProvider | undefined { const visibleProvider = ClineProvider.getVisibleInstance() if (!visibleProvider) { outputChannel.appendLine("Cannot find any visible Roo Code instances.") return undefined } return visibleProvider } // Store panel references in both modes let sidebarPanel: vscode.WebviewView | undefined = undefined let tabPanel: vscode.WebviewPanel | undefined = undefined /** * Get the currently active panel * @returns WebviewPanelꈖWebviewView */ export function getPanel(): vscode.WebviewPanel | vscode.WebviewView | undefined { return tabPanel || sidebarPanel } /** * Set panel references */ export function setPanel( newPanel: vscode.WebviewPanel | vscode.WebviewView | undefined, type: "sidebar" | "tab", ): void { if (type === "sidebar") { sidebarPanel = newPanel as vscode.WebviewView tabPanel = undefined } else { tabPanel = newPanel as vscode.WebviewPanel sidebarPanel = undefined } } export type RegisterCommandOptions = { context: vscode.ExtensionContext outputChannel: vscode.OutputChannel provider: ClineProvider } export const registerCommands = (options: RegisterCommandOptions) => { const { context } = options for (const [id, callback] of Object.entries(getCommandsMap(options))) { const command = getCommand(id as CommandId) context.subscriptions.push(vscode.commands.registerCommand(command, callback)) } } const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOptions): Record => ({ activationCompleted: () => {}, cloudButtonClicked: () => { const visibleProvider = getVisibleProviderOrLog(outputChannel) if (!visibleProvider) { return } TelemetryService.instance.captureTitleButtonClicked("cloud") visibleProvider.postMessageToWebview({ type: "action", action: "cloudButtonClicked" }) }, plusButtonClicked: async () => { const visibleProvider = getVisibleProviderOrLog(outputChannel) if (!visibleProvider) { return } TelemetryService.instance.captureTitleButtonClicked("plus") await visibleProvider.removeClineFromStack() await visibleProvider.refreshWorkspace() await visibleProvider.postMessageToWebview({ type: "action", action: "chatButtonClicked" }) // Send focusInput action immediately after chatButtonClicked // This ensures the focus happens after the view has switched await visibleProvider.postMessageToWebview({ type: "action", action: "focusInput" }) }, popoutButtonClicked: () => { TelemetryService.instance.captureTitleButtonClicked("popout") return openClineInNewTab({ context, outputChannel }) }, openInNewTab: () => openClineInNewTab({ context, outputChannel }), openInCurrentTab: () => openClineInCurrentTab({ context, outputChannel }), settingsButtonClicked: () => { const visibleProvider = getVisibleProviderOrLog(outputChannel) if (!visibleProvider) { return } TelemetryService.instance.captureTitleButtonClicked("settings") visibleProvider.postMessageToWebview({ type: "action", action: "settingsButtonClicked" }) // Also explicitly post the visibility message to trigger scroll reliably visibleProvider.postMessageToWebview({ type: "action", action: "didBecomeVisible" }) }, historyButtonClicked: () => { const visibleProvider = getVisibleProviderOrLog(outputChannel) if (!visibleProvider) { return } TelemetryService.instance.captureTitleButtonClicked("history") visibleProvider.postMessageToWebview({ type: "action", action: "historyButtonClicked" }) }, marketplaceButtonClicked: () => { const visibleProvider = getVisibleProviderOrLog(outputChannel) if (!visibleProvider) return visibleProvider.postMessageToWebview({ type: "action", action: "marketplaceButtonClicked" }) }, newTask: handleNewTask, setCustomStoragePath: async () => { const { promptForCustomStoragePath } = await import("../utils/storage") await promptForCustomStoragePath() }, importSettings: async (filePath?: string) => { const visibleProvider = getVisibleProviderOrLog(outputChannel) if (!visibleProvider) { return } await importSettingsWithFeedback( { providerSettingsManager: visibleProvider.providerSettingsManager, contextProxy: visibleProvider.contextProxy, customModesManager: visibleProvider.customModesManager, provider: visibleProvider, }, filePath, ) }, focusInput: async () => { try { await focusPanel(tabPanel, sidebarPanel) // Send focus input message only for sidebar panels if (sidebarPanel && getPanel() === sidebarPanel) { provider.postMessageToWebview({ type: "action", action: "focusInput" }) } } catch (error) { outputChannel.appendLine(`Error focusing input: ${error}`) } }, focusPanel: async () => { try { await focusPanel(tabPanel, sidebarPanel) } catch (error) { outputChannel.appendLine(`Error focusing panel: ${error}`) } }, acceptInput: () => { const visibleProvider = getVisibleProviderOrLog(outputChannel) if (!visibleProvider) { return } visibleProvider.postMessageToWebview({ type: "acceptInput" }) }, toggleAutoApprove: async () => { const visibleProvider = getVisibleProviderOrLog(outputChannel) if (!visibleProvider) { return } visibleProvider.postMessageToWebview({ type: "action", action: "toggleAutoApprove", }) }, }) export const openClineInNewTab = async ({ context, outputChannel }: Omit) => { // (This example uses webviewProvider activation event which is necessary to // deserialize cached webview, but since we use retainContextWhenHidden, we // don't need to use that event). // https://github.com/microsoft/vscode-extension-samples/blob/main/webview-sample/src/extension.ts const contextProxy = await ContextProxy.getInstance(context) const codeIndexManager = CodeIndexManager.getInstance(context) // Get the existing MDM service instance to ensure consistent policy enforcement let mdmService: MdmService | undefined try { mdmService = MdmService.getInstance() } catch (error) { // MDM service not initialized, which is fine - extension can work without it mdmService = undefined } const tabProvider = new ClineProvider(context, outputChannel, "editor", contextProxy, mdmService) const lastCol = Math.max(...vscode.window.visibleTextEditors.map((editor) => editor.viewColumn || 0)) // Check if there are any visible text editors, otherwise open a new group // to the right. const hasVisibleEditors = vscode.window.visibleTextEditors.length > 0 if (!hasVisibleEditors) { await vscode.commands.executeCommand("workbench.action.newGroupRight") } const targetCol = hasVisibleEditors ? Math.max(lastCol + 1, 1) : vscode.ViewColumn.Two const newPanel = vscode.window.createWebviewPanel(ClineProvider.tabPanelId, "Roo Code", targetCol, { enableScripts: true, retainContextWhenHidden: true, localResourceRoots: [context.extensionUri], }) // Save as tab type panel. setPanel(newPanel, "tab") // TODO: Use better svg icon with light and dark variants (see // https://stackoverflow.com/questions/58365687/vscode-extension-iconpath). newPanel.iconPath = { light: vscode.Uri.joinPath(context.extensionUri, "assets", "icons", "panel_light.png"), dark: vscode.Uri.joinPath(context.extensionUri, "assets", "icons", "panel_dark.png"), } await tabProvider.resolveWebviewView(newPanel) // Add listener for visibility changes to notify webview newPanel.onDidChangeViewState( (e) => { const panel = e.webviewPanel if (panel.visible) { panel.webview.postMessage({ type: "action", action: "didBecomeVisible" }) // Use the same message type as in SettingsView.tsx } }, null, // First null is for `thisArgs` context.subscriptions, // Register listener for disposal ) // Handle panel closing events. newPanel.onDidDispose( () => { setPanel(undefined, "tab") }, null, context.subscriptions, // Also register dispose listener ) // Lock the editor group so clicking on files doesn't open them over the panel. await delay(100) await vscode.commands.executeCommand("workbench.action.lockEditorGroup") return tabProvider } export const openClineInCurrentTab = async ({ context, outputChannel }: Omit) => { const contextProxy = await ContextProxy.getInstance(context) const codeIndexManager = CodeIndexManager.getInstance(context) // Get the existing MDM service instance to ensure consistent policy enforcement let mdmService: MdmService | undefined try { mdmService = MdmService.getInstance() } catch (error) { // MDM service not initialized, which is fine - extension can work without it mdmService = undefined } const tabProvider = new ClineProvider(context, outputChannel, "editor", contextProxy, mdmService) // Use ViewColumn.Active to open in the current editor column instead of creating a new split const targetCol = vscode.ViewColumn.Active const newPanel = vscode.window.createWebviewPanel(ClineProvider.tabPanelId, "Roo Code", targetCol, { enableScripts: true, retainContextWhenHidden: true, localResourceRoots: [context.extensionUri], }) // Save as tab type panel. setPanel(newPanel, "tab") // TODO: Use better svg icon with light and dark variants (see // https://stackoverflow.com/questions/58365687/vscode-extension-iconpath). newPanel.iconPath = { light: vscode.Uri.joinPath(context.extensionUri, "assets", "icons", "panel_light.png"), dark: vscode.Uri.joinPath(context.extensionUri, "assets", "icons", "panel_dark.png"), } await tabProvider.resolveWebviewView(newPanel) // Add listener for visibility changes to notify webview newPanel.onDidChangeViewState( (e) => { const panel = e.webviewPanel if (panel.visible) { panel.webview.postMessage({ type: "action", action: "didBecomeVisible" }) } }, null, context.subscriptions, ) // Handle panel closing events. newPanel.onDidDispose( () => { setPanel(undefined, "tab") }, null, context.subscriptions, ) // Do NOT lock the editor group - allow files to open over the panel like normal tabs return tabProvider }