diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index c7bc472c68..87aec143b5 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -623,6 +623,7 @@ export interface WebviewMessage { | "hooksOpenConfigFolder" | "hooksDeleteHook" | "hooksOpenHookFile" + | "hooksCreateNew" text?: string editedMessageContent?: string tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud" diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 3aa92e84b7..06287acaf3 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -3601,6 +3601,62 @@ export const webviewMessageHandler = async ( break } + case "hooksCreateNew": { + // Check if hooks experiment is enabled + const hooksExperimentsState = getGlobalState("experiments") ?? experimentDefault + if (!experiments.isEnabled(hooksExperimentsState, EXPERIMENT_IDS.HOOKS)) { + break + } + + try { + const cwd = provider.cwd + const hooksPath = path.join(cwd, ".roo", "hooks") + + // Create directory if it doesn't exist + await fs.mkdir(hooksPath, { recursive: true }) + + const exampleFilePath = path.join(hooksPath, "example.yaml") + + // Check if file already exists + const exists = await fileExistsAtPath(exampleFilePath) + if (exists) { + // Open existing file instead of overwriting + const uri = vscode.Uri.file(exampleFilePath) + const doc = await vscode.workspace.openTextDocument(uri) + await vscode.window.showTextDocument(doc) + break + } + + // Create the example hook file + const exampleContent = `version: "1" +hooks: + PreToolUse: + - id: example-hook + matcher: "write_to_file|edit_file|apply_diff|apply_patch" + enabled: true + command: 'echo "Verification hook triggered"' + timeout: 5 +` + await safeWriteText(exampleFilePath, exampleContent) + + // Open the file in the editor + const uri = vscode.Uri.file(exampleFilePath) + const doc = await vscode.workspace.openTextDocument(uri) + await vscode.window.showTextDocument(doc) + + // Reload hooks config to pick up the new file + const hookManager = provider.getHookManager() + if (hookManager) { + await hookManager.reloadHooksConfig() + await provider.postStateToWebview() + } + } catch (error) { + provider.log(`Failed to create hook file: ${error instanceof Error ? error.message : String(error)}`) + vscode.window.showErrorMessage("Failed to create hook configuration file") + } + break + } + default: { // console.log(`Unhandled message type: ${message.type}`) // diff --git a/webview-ui/src/components/settings/HooksSettings.tsx b/webview-ui/src/components/settings/HooksSettings.tsx index 0856ec3c40..49ba78b798 100644 --- a/webview-ui/src/components/settings/HooksSettings.tsx +++ b/webview-ui/src/components/settings/HooksSettings.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useState } from "react" -import { RefreshCw, FolderOpen, AlertTriangle, Clock, Zap, X } from "lucide-react" +import { RefreshCw, FolderOpen, AlertTriangle, Clock, Zap, X, Plus } from "lucide-react" import { VSCodePanels, VSCodePanelTab, VSCodePanelView } from "@vscode/webview-ui-toolkit/react" import { useAppTranslation } from "@src/i18n/TranslationContext" import { useExtensionState } from "@src/context/ExtensionStateContext" @@ -73,6 +73,10 @@ export const HooksSettings: React.FC = () => { setTimeout(() => setIsUpdatingAllEnabled(false), 500) }, []) + const handleCreateNewHook = useCallback(() => { + vscode.postMessage({ type: "hooksCreateNew" }) + }, []) + const enabledHooks = hooks?.enabledHooks || [] const hasProjectHooks = hooks?.hasProjectHooks || false const snapshotTimestamp = hooks?.snapshotTimestamp @@ -179,7 +183,7 @@ export const HooksSettings: React.FC = () => { {/* Hook Activity Log */} - {/* Bottom Action Buttons - mirroring MCP settings order: global, project, refresh */} + {/* Bottom Action Buttons - mirroring MCP settings order: create, global, project, refresh */}
{ gridTemplateColumns: "repeat(auto-fit, minmax(200px, 1fr))", gap: "10px", }}> +