diff --git a/.husky/pre-push b/.husky/pre-push deleted file mode 100644 index 3c206835b..000000000 --- a/.husky/pre-push +++ /dev/null @@ -1,29 +0,0 @@ -branch="$(git rev-parse --abbrev-ref HEAD)" - -if [ "$branch" = "main" ]; then - echo "You can't push directly to main - please check out a branch." - exit 1 -fi - -# Detect if running on Windows and use pnpm.cmd, otherwise use pnpm. -if [ "$OS" = "Windows_NT" ]; then - pnpm_cmd="pnpm.cmd" -else - if command -v pnpm >/dev/null 2>&1; then - pnpm_cmd="pnpm" - else - pnpm_cmd="npx pnpm" - fi -fi - -$pnpm_cmd run check-types - -# Check for new changesets. -NEW_CHANGESETS=$(find .changeset -name "*.md" ! -name "README.md" | wc -l | tr -d ' ') -echo "Changeset files: $NEW_CHANGESETS" - -if [ "$NEW_CHANGESETS" = "0" ]; then - echo "-------------------------------------------------------------------------------------" - echo "Changes detected. Please run 'pnpm changeset' to create a changeset if applicable." - echo "-------------------------------------------------------------------------------------" -fi diff --git a/packages/types/src/tool.ts b/packages/types/src/tool.ts index c921e58a3..8e9c0d454 100644 --- a/packages/types/src/tool.ts +++ b/packages/types/src/tool.ts @@ -4,7 +4,7 @@ import { z } from "zod" * ToolGroup */ -export const toolGroups = ["read", "edit", "browser", "command", "mcp", "modes"] as const +export const toolGroups = ["read", "edit", "browser", "command", "vsclmt", "mcp", "modes"] as const export const toolGroupsSchema = z.enum(toolGroups) @@ -25,6 +25,7 @@ export const toolNames = [ "list_files", "list_code_definition_names", "browser_action", + "use_vsclmt", "use_mcp_tool", "access_mcp_resource", "ask_followup_question", diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index 44394045b..3f821144a 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -17,6 +17,7 @@ import { listCodeDefinitionNamesTool } from "../tools/listCodeDefinitionNamesToo import { searchFilesTool } from "../tools/searchFilesTool" import { browserActionTool } from "../tools/browserActionTool" import { executeCommandTool } from "../tools/executeCommandTool" +import { useVSCLMT } from "../tools/vsclmt" import { useMcpToolTool } from "../tools/useMcpToolTool" import { accessMcpResourceTool } from "../tools/accessMcpResourceTool" import { askFollowupQuestionTool } from "../tools/askFollowupQuestionTool" @@ -195,6 +196,8 @@ export async function presentAssistantMessage(cline: Task) { return `[${block.name} for '${block.params.path}']` case "browser_action": return `[${block.name} for '${block.params.action}']` + case "use_vsclmt": + return `[${block.name} for '${block.params.tool_name}']` case "use_mcp_tool": return `[${block.name} for '${block.params.server_name}']` case "access_mcp_resource": @@ -481,6 +484,9 @@ export async function presentAssistantMessage(cline: Task) { case "execute_command": await executeCommandTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag) break + case "use_vsclmt": + await useVSCLMT(cline, block, askApproval, handleError, pushToolResult, removeClosingTag) + break case "use_mcp_tool": await useMcpToolTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag) break diff --git a/src/core/prompts/sections/index.ts b/src/core/prompts/sections/index.ts index d06dbbfde..52411bffe 100644 --- a/src/core/prompts/sections/index.ts +++ b/src/core/prompts/sections/index.ts @@ -3,6 +3,7 @@ export { getSystemInfoSection } from "./system-info" export { getObjectiveSection } from "./objective" export { addCustomInstructions } from "./custom-instructions" export { getSharedToolUseSection } from "./tool-use" +export { getVSCLMTSection } from "./vsclmt" export { getMcpServersSection } from "./mcp-servers" export { getToolUseGuidelinesSection } from "./tool-use-guidelines" export { getCapabilitiesSection } from "./capabilities" diff --git a/src/core/prompts/sections/vsclmt.ts b/src/core/prompts/sections/vsclmt.ts new file mode 100644 index 000000000..66fca75fc --- /dev/null +++ b/src/core/prompts/sections/vsclmt.ts @@ -0,0 +1,51 @@ +import type { ToolInfo } from "../../../services/vsclm/VSCLMToolsService" + +export function getVSCLMTSection(selectedVSCLMT: ToolInfo[]): string { + if (!selectedVSCLMT || selectedVSCLMT.length === 0) { + return "" + } + + const toolDescriptions = selectedVSCLMT + .map((tool) => { + const displayName = tool.displayName || tool.name + const description = tool.description || tool.userDescription || "No description available" + + let toolSection = `### ${displayName} +**Provider Extension:** ${tool.providerExtensionDisplayName} (${tool.providerExtensionId}) +**Description:** ${description} + +**Tool Name:** ${tool.name}` + + // Add input schema information if available + if (tool.inputSchema && typeof tool.inputSchema === "object") { + try { + const schemaStr = JSON.stringify(tool.inputSchema, null, 2) + toolSection += ` +**Input Schema:** +\`\`\`json +${schemaStr} +\`\`\`` + } catch (error) { + // If schema can't be serialized, skip it + console.log(`Error serializing input schema for tool ${tool.name}:`, error) + } + } + + // Add tags if available + if (tool.tags && tool.tags.length > 0) { + toolSection += ` +**Tags:** ${tool.tags.join(", ")}` + } + + return toolSection + }) + .join("\n\n") + + return `## VS Code Language Model Tools + +The following VS Code Language Model tools are available for use. You can invoke them using the \`use_vsclmt\` tool with the appropriate tool name and arguments. + +${toolDescriptions} + +**Usage:** To use any of these tools, use the \`use_vsclmt\` tool with the \`tool_name\` parameter set to the exact tool name shown above, and provide any required arguments as a JSON string in the \`arguments\` parameter.` +} diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index a9a2b686a..b5cb9f408 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -7,6 +7,7 @@ import { Mode, modes, defaultModeSlug, getModeBySlug, getGroupName, getModeSelec import { DiffStrategy } from "../../shared/tools" import { formatLanguage } from "../../shared/language" +import { VSCLMToolsService } from "../../services/vsclm/VSCLMToolsService" import { McpHub } from "../../services/mcp/McpHub" import { CodeIndexManager } from "../../services/code-index/manager" @@ -18,6 +19,7 @@ import { getSystemInfoSection, getObjectiveSection, getSharedToolUseSection, + getVSCLMTSection, getMcpServersSection, getToolUseGuidelinesSection, getCapabilitiesSection, @@ -31,6 +33,7 @@ async function generatePrompt( cwd: string, supportsComputerUse: boolean, mode: Mode, + vsclmtService?: VSCLMToolsService, mcpHub?: McpHub, diffStrategy?: DiffStrategy, browserViewportSize?: string, @@ -56,8 +59,9 @@ async function generatePrompt( const modeConfig = getModeBySlug(mode, customModeConfigs) || modes.find((m) => m.slug === mode) || modes[0] const { roleDefinition, baseInstructions } = getModeSelection(mode, promptComponent, customModeConfigs) - const [modesSection, mcpServersSection] = await Promise.all([ + const [modesSection, vsclmtSection, mcpServersSection] = await Promise.all([ getModesSection(context), + vsclmtService ? getVSCLMTSection(vsclmtService.getSelectedTools()) : Promise.resolve(""), modeConfig.groups.some((groupEntry) => getGroupName(groupEntry) === "mcp") ? getMcpServersSection(mcpHub, effectiveDiffStrategy, enableMcpServerCreation) : Promise.resolve(""), @@ -87,6 +91,8 @@ ${getToolDescriptionsForMode( ${getToolUseGuidelinesSection(codeIndexManager)} +${vsclmtSection} + ${mcpServersSection} ${getCapabilitiesSection(cwd, supportsComputerUse, mcpHub, effectiveDiffStrategy, codeIndexManager)} @@ -113,6 +119,7 @@ export const SYSTEM_PROMPT = async ( context: vscode.ExtensionContext, cwd: string, supportsComputerUse: boolean, + vsclmtService?: VSCLMToolsService, mcpHub?: McpHub, diffStrategy?: DiffStrategy, browserViewportSize?: string, @@ -187,6 +194,7 @@ ${customInstructions}` cwd, supportsComputerUse, currentMode.slug, + vsclmtService, mcpHub, effectiveDiffStrategy, browserViewportSize, diff --git a/src/core/prompts/tools/index.ts b/src/core/prompts/tools/index.ts index 736c716a2..ea28149bb 100644 --- a/src/core/prompts/tools/index.ts +++ b/src/core/prompts/tools/index.ts @@ -17,6 +17,7 @@ import { getListCodeDefinitionNamesDescription } from "./list-code-definition-na import { getBrowserActionDescription } from "./browser-action" import { getAskFollowupQuestionDescription } from "./ask-followup-question" import { getAttemptCompletionDescription } from "./attempt-completion" +import { getVSCLMTDescription } from "./vsclmt" import { getUseMcpToolDescription } from "./use-mcp-tool" import { getAccessMcpResourceDescription } from "./access-mcp-resource" import { getSwitchModeDescription } from "./switch-mode" @@ -26,6 +27,7 @@ import { CodeIndexManager } from "../../../services/code-index/manager" // Map of tool names to their description functions const toolDescriptionMap: Record string | undefined> = { + use_vsclmt: (args) => getVSCLMTDescription(args), execute_command: (args) => getExecuteCommandDescription(args), read_file: (args) => getReadFileDescription(args), fetch_instructions: () => getFetchInstructionsDescription(), @@ -135,6 +137,7 @@ export { getBrowserActionDescription, getAskFollowupQuestionDescription, getAttemptCompletionDescription, + getVSCLMTDescription, getUseMcpToolDescription, getAccessMcpResourceDescription, getSwitchModeDescription, diff --git a/src/core/prompts/tools/vsclmt.ts b/src/core/prompts/tools/vsclmt.ts new file mode 100644 index 000000000..e6ddd03c7 --- /dev/null +++ b/src/core/prompts/tools/vsclmt.ts @@ -0,0 +1,27 @@ +import { ToolArgs } from "./types" + +export function getVSCLMTDescription(args: ToolArgs): string { + return `## use_vsclmt + +Access and invoke VS Code Language Model tools that are selected and available in the current workspace. + +Required parameters: +- tool_name: The name of the VS Code LM tool to invoke + +Optional parameters: +- arguments: JSON string containing the arguments for the tool + +The tool will: +1. Validate that the specified VS Code LM tool is available and selected +2. Parse and validate the provided arguments +3. Invoke the tool using VS Code's native language model tool system +4. Return the tool's result or any error messages + +Use this tool to leverage VS Code's ecosystem of language model tools for enhanced functionality. + +Example: + +example-tool +{"param1": "value1", "param2": "value2"} +` +} diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 35bcb4b01..3d9cea46c 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -1659,6 +1659,7 @@ export class Task extends EventEmitter { // kilocode_change end /*private kilocode_change*/ async getSystemPrompt(): Promise { + const vsclmtService = this.providerRef.deref()?.getVSCLMToolService() const { mcpEnabled } = (await this.providerRef.deref()?.getState()) ?? {} let mcpHub: McpHub | undefined if (mcpEnabled ?? true) { @@ -1710,6 +1711,7 @@ export class Task extends EventEmitter { provider.context, this.cwd, (this.api.getModel().info.supportsComputerUse ?? false) && (browserToolEnabled ?? true), + vsclmtService, mcpHub, this.diffStrategy, browserViewportSize, diff --git a/src/core/tools/vsclmt.ts b/src/core/tools/vsclmt.ts new file mode 100644 index 000000000..43184c880 --- /dev/null +++ b/src/core/tools/vsclmt.ts @@ -0,0 +1,143 @@ +import * as vscode from "vscode" +import { formatResponse } from "../prompts/responses" +import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" +import { Task } from "../task/Task" + +export async function useVSCLMT( + cline: Task, + toolUse: ToolUse, + askApproval: AskApproval, + handleError: HandleError, + pushToolResult: PushToolResult, + removeClosingTag: RemoveClosingTag, +): Promise { + const { tool_name, arguments: toolArgs } = toolUse.params + + // Handle partial tool invocation (missing parameters) + if (toolUse.partial) { + const partialMessage = JSON.stringify({ + type: "vsclmt_tool", + toolName: removeClosingTag("tool_name", tool_name), + arguments: removeClosingTag("arguments", toolArgs), + }) + + await cline.ask("tool", partialMessage, toolUse.partial).catch(() => {}) + return + } + + // Non-partial: require tool_name + if (!tool_name) { + cline.recordToolError("use_vsclmt") + pushToolResult(await cline.sayAndCreateMissingParamError("use_vsclmt", "tool_name")) + return + } + + // Get the vsclmt service from ClineProvider + const provider = cline.providerRef.deref() + const vsclmtService = provider?.getVSCLMToolService() + + if (!vsclmtService) { + pushToolResult(formatResponse.toolError("VS Code LM tool system not available")) + return + } + + // Parse arguments if provided + let parsedArgs: any = {} + if (toolArgs) { + try { + parsedArgs = JSON.parse(toolArgs) + } catch (error) { + pushToolResult(formatResponse.toolError(`Invalid JSON arguments: ${error.stack || error.message}`)) + return + } + } + + try { + // Check if tool is selected + if (!vsclmtService.isToolSelected(tool_name)) { + pushToolResult( + formatResponse.toolError( + `Tool '${tool_name}' is not selected for use. Please select it in the Tool Selection panel.`, + ), + ) + return + } + + // Prepare tool invocation to get any user confirmation message + const prepared = await vsclmtService.prepareToolInvocation(tool_name, parsedArgs) + if (prepared?.confirmationMessages) { + // Ask for approval with the tool's custom confirmation message + const message = + prepared.confirmationMessages.message instanceof vscode.MarkdownString + ? prepared.confirmationMessages.message.value + : prepared.confirmationMessages.message + + const confirmText = `${prepared.confirmationMessages.title}\n${message}` + const approved = await askApproval("tool", confirmText) + + if (!approved) { + pushToolResult(formatResponse.toolDenied()) + return + } + } + + // Invoke the tool with any progress message from preparation + const result = await vsclmtService.invokeTool(tool_name, parsedArgs) + + // Format the result for display + const extractText = (obj: unknown): string => { + if (typeof obj === "string") return obj + if (!obj || typeof obj !== "object") return "" + const asRecord = obj as Record + if ("text" in asRecord && typeof asRecord.text === "string") return asRecord.text + if ("value" in asRecord && typeof asRecord.value === "string") return asRecord.value + // Special handling for PromptTsx node structure + if ("node" in asRecord && asRecord.node && typeof asRecord.node === "object") { + const node = asRecord.node as Record + if ("children" in node && Array.isArray(node.children)) { + return node.children.map((child: unknown) => extractText(child)).join("\n") + } + } + if (Array.isArray(obj)) return obj.map((item) => extractText(item)).join("") + return Object.values(asRecord) + .map((v) => extractText(v)) + .join(" ") + } + + const resultText = result.content + .map((part): string => { + if (typeof part === "object" && part !== null) { + // Handle MarkdownString specifically + if (part instanceof vscode.MarkdownString) { + return part.value + } + // Handle objects with value property + if ("value" in part) { + const value = part.value + if (value instanceof vscode.MarkdownString) { + return value.value + } + if (typeof value === "string") { + return value + } + // Handle LanguageModelPromptTsxPart or complex nested structures + if (value && typeof value === "object") { + return extractText(value) + } + } + return JSON.stringify(part, null, 2) + } + return String(part) + }) + .join("\n ---\n") + + pushToolResult(resultText) + } catch (error) { + await handleError("VS Code LM tool invocation", error) + pushToolResult( + formatResponse.toolError( + `Failed to invoke VS Code LM tool '${tool_name}': ${error.stack || error.message}`, + ), + ) + } +} diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index d252cbbcd..c535cc294 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -45,6 +45,7 @@ import { Terminal } from "../../integrations/terminal/Terminal" import { downloadTask } from "../../integrations/misc/export-markdown" import { getTheme } from "../../integrations/theme/getTheme" import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker" +import { VSCLMToolsService } from "../../services/vsclm/VSCLMToolsService" import { McpHub } from "../../services/mcp/McpHub" import { McpServerManager } from "../../services/mcp/McpServerManager" import { MarketplaceManager } from "../../services/marketplace" @@ -103,6 +104,7 @@ export class ClineProvider extends EventEmitter implements public get workspaceTracker(): WorkspaceTracker | undefined { return this._workspaceTracker } + private vsclmtService: VSCLMToolsService protected mcpHub?: McpHub // Change from private to protected private marketplaceManager: MarketplaceManager private mdmService?: MdmService @@ -141,6 +143,8 @@ export class ClineProvider extends EventEmitter implements await this.postStateToWebview() }) + this.vsclmtService = new VSCLMToolsService(context) + // Initialize MCP Hub through the singleton manager McpServerManager.getInstance(this.context, this) .then((hub) => { @@ -209,6 +213,9 @@ export class ClineProvider extends EventEmitter implements return this.clineStack[this.clineStack.length - 1] } + public getVSCLMToolService(): VSCLMToolsService { + return this.vsclmtService + } // returns the current clineStack length (how many cline objects are in the stack) getClineStackSize(): number { return this.clineStack.length diff --git a/src/core/webview/generateSystemPrompt.ts b/src/core/webview/generateSystemPrompt.ts index 2c88b98d2..ce3fcb6a1 100644 --- a/src/core/webview/generateSystemPrompt.ts +++ b/src/core/webview/generateSystemPrompt.ts @@ -67,6 +67,7 @@ export const generateSystemPrompt = async (provider: ClineProvider, message: Web provider.context, cwd, canUseBrowserTool, + provider.getVSCLMToolService(), mcpEnabled ? provider.getMcpHub() : undefined, diffStrategy, browserViewportSize ?? "900x600", diff --git a/src/package.json b/src/package.json index b23dc4c3f..997ff9f7c 100644 --- a/src/package.json +++ b/src/package.json @@ -120,6 +120,13 @@ "type": "webview", "id": "kilo-code.SidebarProvider", "name": "%views.sidebar.name%" + }, + { + "type": "tree", + "id": "kilo-tool-selection", + "name": "Tool Selection", + "icon": "$(tools)", + "visibility": "visible" } ] }, @@ -238,6 +245,21 @@ "dark": "assets/icons/kilo-dark.svg" }, "category": "%configuration.title%" + }, + { + "command": "kilo-code.setManualApproval", + "title": "Switch to Manual Approval", + "category": "%configuration.title%" + }, + { + "command": "kilo-code.setAutoApproval", + "title": "Switch to Auto Approval", + "category": "%configuration.title%" + }, + { + "command": "kilo-code.addMoreTools", + "title": "+ Add More", + "category": "%configuration.title%" } ], "keybindings": [ @@ -300,6 +322,21 @@ } ], "view/title": [ + { + "command": "kilo-code.setManualApproval", + "group": "navigation@1", + "when": "view == kilo-tool-selection && config.chat.tools.autoApprove == true" + }, + { + "command": "kilo-code.setAutoApproval", + "group": "navigation@1", + "when": "view == kilo-tool-selection && config.chat.tools.autoApprove != true" + }, + { + "command": "kilo-code.addMoreTools", + "group": "navigation@2", + "when": "view == kilo-tool-selection" + }, { "command": "kilo-code.plusButtonClicked", "group": "navigation@1", @@ -395,6 +432,11 @@ "configuration": { "title": "%configuration.title%", "properties": { + "chat.tools.autoApprove": { + "type": "boolean", + "default": false, + "description": "Automatically approve tool invocations in chat without manual confirmation" + }, "kilo-code.allowedCommands": { "type": "array", "items": { diff --git a/src/services/vsclm/VSCLMToolsService.ts b/src/services/vsclm/VSCLMToolsService.ts new file mode 100644 index 000000000..e3dd99f8e --- /dev/null +++ b/src/services/vsclm/VSCLMToolsService.ts @@ -0,0 +1,386 @@ +import * as vscode from "vscode" +import * as fs from "fs" +import * as path from "path" +import * as yaml from "yaml" + +export interface ToolInfo { + name: string + description: string + inputSchema?: object + tags?: readonly string[] + displayName?: string + userDescription?: string + icon?: string + providerExtensionId: string + providerExtensionDisplayName: string + toolReferenceName?: string + canBeReferencedInPrompt?: boolean +} + +export class ToolTreeItem extends vscode.TreeItem { + constructor( + label: string, + collapsibleState: vscode.TreeItemCollapsibleState, + public readonly extensionId?: string, + public readonly toolName?: string, + description?: string, + isSelected: boolean = false, + ) { + super(label, collapsibleState) + + this.description = description + + // Set properties based on whether this is a group or tool item + if (extensionId && !toolName) { + // This is a group item + this.contextValue = "toolGroup" + this.iconPath = new vscode.ThemeIcon("extensions") + this.checkboxState = isSelected + ? vscode.TreeItemCheckboxState.Checked + : vscode.TreeItemCheckboxState.Unchecked + this.tooltip = `${extensionId}\n${description || ""}` + } else if (toolName) { + // This is a tool item + this.contextValue = "tool" + this.iconPath = new vscode.ThemeIcon("tools") + this.description = description + this.checkboxState = isSelected + ? vscode.TreeItemCheckboxState.Checked + : vscode.TreeItemCheckboxState.Unchecked + this.tooltip = description + } + } +} + +export class VSCLMToolsService implements vscode.TreeDataProvider { + private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter< + ToolTreeItem | undefined | null | void + >() + readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event + + // Internal cache for grouped tool info + private groupedTools: Record = {} + private initialized = false + private toolSelectionState: Record = {} + private saveSelectionsTimeout: NodeJS.Timeout | null = null + private treeView: vscode.TreeView + + constructor(private readonly context: vscode.ExtensionContext) { + // Initialize tool discovery + this.ensureInitialized() + + // Load initial selections + this.loadSelections().then(() => this.refresh()) + + // Register to save selections on deactivation + context.subscriptions.push({ + dispose: () => { + if (this.saveSelectionsTimeout) { + clearTimeout(this.saveSelectionsTimeout) + this.saveSelectionsNow() + } + }, + }) + + const treeView = vscode.window.createTreeView("kilo-tool-selection", { + treeDataProvider: this, + canSelectMany: true, + }) + this.treeView = treeView + context.subscriptions.push(treeView) + treeView.onDidChangeCheckboxState((e) => { + // Persist the changes with some debouncing + for (const [item, state] of e.items) { + if (item.toolName) { + const isChecked = state === vscode.TreeItemCheckboxState.Checked + this.toolSelectionState[item.toolName] = isChecked + } + } + this.debouncedSaveSelections() + this.updateTreeViewTitle() + }) + + // Register the approval mode commands + const setManualApprovalCommand = vscode.commands.registerCommand("kilo-code.setManualApproval", () => + this.setApprovalMode(false), + ) + const setAutoApprovalCommand = vscode.commands.registerCommand("kilo-code.setAutoApproval", () => + this.setApprovalMode(true), + ) + const addMoreToolsCommand = vscode.commands.registerCommand("kilo-code.addMoreTools", () => + this.openExtensionsWithToolsFilter(), + ) + context.subscriptions.push(setManualApprovalCommand, setAutoApprovalCommand, addMoreToolsCommand) + } + + private scanExtensionsForTools(): void { + const result: Record = {} + for (const ext of vscode.extensions.all) { + const pkg = ext.packageJSON + const lmTools = pkg?.contributes?.languageModelTools + if (Array.isArray(lmTools)) { + result[ext.id] = { + extensionDisplayName: pkg.displayName || ext.id, + tools: lmTools.map((tool: any) => ({ + name: tool.name, + description: tool.modelDescription || tool.description || "", + inputSchema: tool.inputSchema, + tags: tool.tags, + displayName: tool.displayName, + userDescription: tool.userDescription, + icon: tool.icon, + providerExtensionId: ext.id, + providerExtensionDisplayName: pkg.displayName || ext.id, + toolReferenceName: tool.toolReferenceName, + canBeReferencedInPrompt: tool.canBeReferencedInPrompt, + })), + } + } + } + this.groupedTools = result + } + + private ensureInitialized(): void { + if (!this.initialized) { + this.scanExtensionsForTools() + // Listen for extension changes to keep tool info up to date + vscode.extensions.onDidChange(() => { + this.scanExtensionsForTools() + this.refresh() + }) + this.initialized = true + } + } + + // Public methods for tool management + + getAllToolsGroupedByExtension(): Record { + this.ensureInitialized() + return this.groupedTools + } + + getSelectedTools(): ToolInfo[] { + this.ensureInitialized() + const selectedTools: ToolInfo[] = [] + + for (const [extensionId, { tools }] of Object.entries(this.groupedTools)) { + for (const tool of tools) { + if (this.toolSelectionState[tool.name]) { + selectedTools.push(tool) + } + } + } + + return selectedTools + } + + isToolSelected(toolName: string): boolean { + return this.toolSelectionState[toolName] || false + } + + private getSelectionFilePath(): string { + const workspaceFolders = vscode.workspace.workspaceFolders + if (!workspaceFolders || workspaceFolders.length === 0) { + return "" + } + return path.join(workspaceFolders[0].uri.fsPath, ".roo", "tools", "selection.yaml") + } + + private async ensureDirectoryExists(filePath: string): Promise { + const dir = path.dirname(filePath) + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }) + } + } + + private async loadSelections(): Promise { + const filePath = this.getSelectionFilePath() + if (!filePath) return + + try { + await this.ensureDirectoryExists(filePath) + if (fs.existsSync(filePath)) { + const content = fs.readFileSync(filePath, "utf-8") + const state = yaml.parse(content) || {} + + if (typeof state === "object" && !Array.isArray(state)) { + this.toolSelectionState = state + } else { + // default to none selected + this.toolSelectionState = {} + } + } + } catch (error) { + console.error("Error reading tool selections:", error) + } + } + + private debouncedSaveSelections(): void { + if (this.saveSelectionsTimeout) { + clearTimeout(this.saveSelectionsTimeout) + } + + this.saveSelectionsTimeout = setTimeout(() => { + this.saveSelectionsNow() + }, 5000) + } + + private async saveSelectionsNow(): Promise { + const filePath = this.getSelectionFilePath() + if (!filePath) return + + try { + await this.ensureDirectoryExists(filePath) + fs.writeFileSync(filePath, yaml.stringify(this.toolSelectionState)) + } catch (error) { + console.error("Error writing tool selections:", error) + } finally { + this.saveSelectionsTimeout = null + } + } + + invokeTool(toolName: string, input: object, token?: vscode.CancellationToken): Thenable { + const tool = this.getToolInfoByName(toolName) + if (!tool) { + throw new Error(`Tool '${toolName}' not found`) + } + + const invocationToken = undefined // We're not in a chat context, so no token + const options = { + input, + toolInvocationToken: invocationToken, + } + + // Execute the tool + return vscode.lm.invokeTool(toolName, options, token) + } + + async prepareToolInvocation( + toolName: string, + input: object, + token?: vscode.CancellationToken, + ): Promise { + return undefined + + // const tool = this.getToolInfoByName(toolName) + // if (!tool || typeof tool.prepareInvocation !== 'function') { + // return undefined + // } + + // const options = { + // input, + // toolInvocationToken: undefined // No chat context token + // } + + // // Let the tool prepare its invocation + // const prepared = await tool.prepareInvocation(options, token) + // return prepared + } + + private getToolInfoByName(toolName: string): vscode.LanguageModelToolInformation | undefined { + return vscode.lm.tools.find((t) => t.name === toolName) + } + + // TreeDataProvider implementation + + refresh(): void { + this._onDidChangeTreeData.fire() + this.updateTreeViewTitle() + } + + getTreeItem(element: ToolTreeItem): vscode.TreeItem { + if (element.toolName) { + element.checkboxState = this.toolSelectionState[element.toolName] + ? vscode.TreeItemCheckboxState.Checked + : vscode.TreeItemCheckboxState.Unchecked + } else if (element.extensionId) { + const { extensionDisplayName } = this.groupedTools[element.extensionId] + element.label = extensionDisplayName + } + return element + } + + getChildren(element?: ToolTreeItem): ToolTreeItem[] { + if (!element) { + // Root level - return extension groups + const groupedTools = this.getAllToolsGroupedByExtension() + const items: ToolTreeItem[] = [] + + for (const [extensionId, { extensionDisplayName, tools }] of Object.entries(groupedTools)) { + if (tools.length === 0) continue + + const groupSelectedCount = tools.filter((t) => this.toolSelectionState[t.name]).length + const allSelected = groupSelectedCount === tools.length + + const groupItem = new ToolTreeItem( + `${extensionDisplayName} (${groupSelectedCount}/${tools.length})`, + vscode.TreeItemCollapsibleState.Expanded, + extensionId, + undefined, + undefined, + allSelected, + ) + items.push(groupItem) + } + return items + } else if (element.extensionId) { + // Extension group level - return tools + const { tools } = this.groupedTools[element.extensionId] + + return tools.map( + (tool) => + new ToolTreeItem( + tool.displayName || tool.name, + vscode.TreeItemCollapsibleState.None, + element.extensionId, + tool.name, + tool.description, + this.toolSelectionState[tool.name], + ), + ) + } + return [] + } + + async setApprovalMode(autoApprove: boolean): Promise { + try { + // Get current setting value + const config = vscode.workspace.getConfiguration() + const currentValue = config.get("chat.tools.autoApprove", false) + + // Only update if value is different + if (autoApprove !== currentValue) { + // Update the configuration + await config.update("chat.tools.autoApprove", autoApprove, vscode.ConfigurationTarget.Workspace) + + const mode = autoApprove ? "Auto" : "Manual" + vscode.window.showInformationMessage(`Tool approval mode: ${mode}`) + + // Update tree view title to reflect new state + this.updateTreeViewTitle() + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : "Unknown error" + vscode.window.showErrorMessage(`Failed to update approval mode: ${errorMessage}`) + } + } + + private updateTreeViewTitle(): void { + const config = vscode.workspace.getConfiguration() + const autoApprove = config.get("chat.tools.autoApprove", false) + const approvalMode = autoApprove ? "Auto" : "Manual" + const selectedCount = this.getSelectedTools().length + const totalCount = Object.values(this.groupedTools).reduce((sum, group) => sum + group.tools.length, 0) + + this.treeView.title = `Tool Selection (${selectedCount}/${totalCount}) - ${approvalMode} Approval` + } + + private async openExtensionsWithToolsFilter(): Promise { + try { + await vscode.commands.executeCommand("workbench.extensions.search", "@tag:language-model-tools") + } catch (error) { + const errorMessage = error instanceof Error ? error.message : "Unknown error" + vscode.window.showErrorMessage(`Failed to open extensions view: ${errorMessage}`) + } + } +} diff --git a/src/services/vsclm/design.md b/src/services/vsclm/design.md new file mode 100644 index 000000000..94ce109ce --- /dev/null +++ b/src/services/vsclm/design.md @@ -0,0 +1,58 @@ +# Roo Tool Selection Feature Design + +## Goal + +Provide a dedicated tool selection interface in the Roo sidebar that allows users to: + +- View and manage all available tools from any extension (discovered via `vscode.extensions.all` and `vscode.lm.tools`) +- Select/deselect tools for AI agent use through a persistent TreeView interface +- Group tools by extension with hierarchical selection controls +- Present the selected tools to the AI agent for awareness (so the agent knows which tools are available) +- Per agent request, invoke selected tools via the native `vscode.lm` API + +## Implementation Plan + +### 1. Tool Discovery + +- Enumerate all available tools by scanning each installed extension's `packageJSON` at runtime and reading its `contributes.languageModelTools` section. This provides tool metadata (name, description, inputSchema, tags, icon, displayName, etc.) for UI and selection. +- When invoking a tool, use the runtime tool object from `vscode.lm.tools` (type: `LanguageModelToolInformation[]`). Only tools present in both the metadata and `vscode.lm.tools` are considered available for invocation and agent awareness. + +### 2. UI Integration + +- Add a persistent TreeView to the Roo sidebar panel using VS Code's native TreeView API. +- Register the view in package.json under the `roo-cline-ActivityBar` container. +- Position the TreeView at the bottom of the Roo panel for easy access. +- Implement hierarchical tool organization with extension-level groups. +- Use checkbox states to show and control tool selection. +- Show selection counts in group headers (e.g., "Extension Name (3/5)"). + +### 3. Tool Selection Handling + +- Persist selected tool names in a YAML file at `.roo/tools/selection.yaml` +- Bidirectional selection logic is automatically handled by VSCode's TreeView implementation +- Only expose selected tools to the chat agent +- Update selection state immediately when checkboxes are toggled + +### 4. Tool Invocation + +- When a tool is invoked via chat, use the native `vscode.lm.invokeTool(toolName, { input }, token)` API. +- Present tool results in chat. + +## Implementation Details + +### TreeView Implementation + +- Use VS Code's native TreeView component for tool selection interface +- Implement VSCLMToolsService class to manage tool data and state +- Use proper VS Code icons: `$(extensions)` for groups, `$(tools)` for individual tools +- Show selection count in TreeView title (e.g., "Tool Selection (3/10)") +- Store selections in YAML file at `.roo/tools/selection.yaml` +- Bidirectional selection is automatically handled by TreeView + +## Notes + +- **File-based persistence** - YAML storage provides better version control and visibility +- **VSCode TreeView benefits** - handles bidirectional selection automatically +- **Visual hierarchy** - extension groups provide clear organization +- **Approval mode** - simple toggle implementation provides effective control +- **Minimal implementation** - focuses on core functionality while maintaining extensibility diff --git a/src/shared/tools.ts b/src/shared/tools.ts index bbca3524a..e074d85ff 100644 --- a/src/shared/tools.ts +++ b/src/shared/tools.ts @@ -170,6 +170,12 @@ export interface SearchAndReplaceToolUse extends ToolUse { Partial, "use_regex" | "ignore_case" | "start_line" | "end_line">> } +export interface VSCLMTUse extends ToolUse { + name: "use_vsclmt" + tool_name: string + arguments?: string +} + // Define tool group configuration export type ToolGroupConfig = { tools: readonly string[] @@ -186,6 +192,7 @@ export const TOOL_DISPLAY_NAMES: Record = { list_files: "list files", list_code_definition_names: "list definitions", browser_action: "use a browser", + use_vsclmt: "use VS Code LM tools", use_mcp_tool: "use mcp tools", access_mcp_resource: "access mcp resources", ask_followup_question: "ask questions", @@ -221,6 +228,10 @@ export const TOOL_GROUPS: Record = { command: { tools: ["execute_command"], }, + vsclmt: { + tools: ["use_vsclmt"], + alwaysAvailable: true, + }, mcp: { tools: ["use_mcp_tool", "access_mcp_resource"], }, @@ -232,6 +243,7 @@ export const TOOL_GROUPS: Record = { // Tools that are always available to all modes. export const ALWAYS_AVAILABLE_TOOLS: ToolName[] = [ + "use_vsclmt", "ask_followup_question", "attempt_completion", "switch_mode",