mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-05 08:10:14 +00:00
356 lines
11 KiB
TypeScript
356 lines
11 KiB
TypeScript
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"
|
|
import {
|
|
getGitDiff,
|
|
generateCommitMessageFromDiff,
|
|
getWorkspaceRoot,
|
|
setScmInputBoxMessage,
|
|
} from "../utils/commit-message-generator"
|
|
|
|
/**
|
|
* 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<CommandId, any> => ({
|
|
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 }),
|
|
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",
|
|
})
|
|
},
|
|
generateCommitMessage: async () => {
|
|
const workspaceRoot = getWorkspaceRoot()
|
|
|
|
if (!workspaceRoot) {
|
|
vscode.window.showErrorMessage(t("common:commit.no_workspace"))
|
|
return
|
|
}
|
|
|
|
const visibleProvider = getVisibleProviderOrLog(outputChannel)
|
|
|
|
if (!visibleProvider) {
|
|
return
|
|
}
|
|
|
|
// Let the user optionally pick a different API profile
|
|
const listApiConfigMeta = await visibleProvider.providerSettingsManager.listConfig()
|
|
let apiConfiguration = visibleProvider.contextProxy.getProviderSettings()
|
|
|
|
if (listApiConfigMeta.length > 1) {
|
|
const items = [
|
|
{ label: t("common:commit.use_current_profile"), id: undefined },
|
|
...listApiConfigMeta.map((config) => ({
|
|
label: config.name ?? config.id,
|
|
id: config.id,
|
|
})),
|
|
]
|
|
|
|
const selected = await vscode.window.showQuickPick(items, {
|
|
placeHolder: t("common:commit.select_profile"),
|
|
})
|
|
|
|
if (!selected) {
|
|
return // User cancelled
|
|
}
|
|
|
|
if (selected.id) {
|
|
const { name: _, ...providerSettings } = await visibleProvider.providerSettingsManager.getProfile({
|
|
id: selected.id,
|
|
})
|
|
|
|
if (providerSettings.apiProvider) {
|
|
apiConfiguration = providerSettings
|
|
}
|
|
}
|
|
}
|
|
|
|
await vscode.window.withProgress(
|
|
{
|
|
location: vscode.ProgressLocation.Notification,
|
|
title: t("common:commit.generating"),
|
|
cancellable: false,
|
|
},
|
|
async () => {
|
|
try {
|
|
const diff = await getGitDiff(workspaceRoot)
|
|
|
|
if (!diff.trim()) {
|
|
vscode.window.showInformationMessage(t("common:commit.no_changes"))
|
|
return
|
|
}
|
|
|
|
const commitMessage = await generateCommitMessageFromDiff(apiConfiguration, diff)
|
|
const success = await setScmInputBoxMessage(commitMessage)
|
|
|
|
if (success) {
|
|
// Focus the SCM view to show the generated message
|
|
await vscode.commands.executeCommand("workbench.view.scm")
|
|
}
|
|
} catch (error) {
|
|
vscode.window.showErrorMessage(
|
|
`${t("common:commit.generation_failed")}: ${error instanceof Error ? error.message : String(error)}`,
|
|
)
|
|
}
|
|
},
|
|
)
|
|
},
|
|
})
|
|
|
|
export const openClineInNewTab = async ({ context, outputChannel }: Omit<RegisterCommandOptions, "provider">) => {
|
|
// (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
|
|
}
|