From e079e6c7aaeb1178be5ed29413387e19156c7aeb Mon Sep 17 00:00:00 2001 From: Toray Altas Date: Fri, 16 Jan 2026 18:27:13 -0500 Subject: [PATCH] feat: improve hooks settings UI Add open project/global folder buttons at bottom Move reload to bottom Add enable-all toggle (with new message type) Convert hooks list to accordion + per-hook logs --- packages/types/src/vscode-extension-host.ts | 2 + src/core/webview/ClineProvider.ts | 11 +- .../webviewMessageHandler.hooks.spec.ts | 86 ++++ src/core/webview/webviewMessageHandler.ts | 24 ++ .../src/components/settings/HooksSettings.tsx | 370 ++++++++++++++---- .../settings/__tests__/HooksSettings.spec.tsx | 285 +++++++++++++- webview-ui/src/i18n/locales/en/settings.json | 8 + 7 files changed, 693 insertions(+), 93 deletions(-) diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index c9f10c5e9f..d69f5c7141 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -617,6 +617,7 @@ export interface WebviewMessage { | "debugSetting" | "hooksReloadConfig" | "hooksSetEnabled" + | "hooksSetAllEnabled" | "hooksOpenConfigFolder" text?: string editedMessageContent?: string @@ -675,6 +676,7 @@ export interface WebviewMessage { useProviderSignup?: boolean // For rooCloudSignIn to use provider signup flow hookId?: string // For hooksSetEnabled hookEnabled?: boolean // For hooksSetEnabled + hooksEnabled?: boolean // For hooksSetAllEnabled hooksSource?: "global" | "project" // For hooksOpenConfigFolder codeIndexSettings?: { // Global state settings diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 8dfcf9f4f0..55aaa313b2 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -2240,16 +2240,21 @@ export class ClineProvider } const snapshot = this.hookManager.getConfigSnapshot() - const enabledHooks = this.hookManager.getEnabledHooks() const executionHistory = this.hookManager.getHookExecutionHistory() + // Build a list of *all* known hooks (including currently disabled ones) + // so the webview can show per-hook toggles and a global "Enable Hooks" toggle. + const allHooks = snapshot + ? Array.from(snapshot.hooksByEvent.values()).reduce((acc, hooks) => acc.concat(hooks), [] as any[]) + : [] + // Convert ResolvedHook[] to HookInfo[] - const hookInfos = enabledHooks.map((hook) => ({ + const hookInfos = allHooks.map((hook) => ({ id: hook.id, event: hook.event, matcher: hook.matcher, commandPreview: hook.command.length > 100 ? hook.command.substring(0, 97) + "..." : hook.command, - enabled: hook.enabled ?? true, + enabled: (hook.enabled ?? true) && !(snapshot?.disabledHookIds?.has(hook.id) ?? false), source: hook.source, timeout: hook.timeout ?? 60, shell: hook.shell, diff --git a/src/core/webview/__tests__/webviewMessageHandler.hooks.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.hooks.spec.ts index cd1a228ede..b6d6f693c4 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.hooks.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.hooks.spec.ts @@ -227,6 +227,92 @@ describe("webviewMessageHandler - hooks commands", () => { }) }) + describe("hooksSetAllEnabled", () => { + it("should call setHookEnabled for all hooks in snapshot and postStateToWebview", async () => { + const hooksById = new Map() + hooksById.set("hook-1", { + id: "hook-1", + event: "PreToolUse" as any, + matcher: ".*", + command: "echo 1", + enabled: true, + source: "global" as any, + timeout: 30, + includeConversationHistory: false, + } as any) + hooksById.set("hook-2", { + id: "hook-2", + event: "PostToolUse" as any, + matcher: ".*", + command: "echo 2", + enabled: true, + source: "project" as any, + timeout: 30, + includeConversationHistory: false, + } as any) + + vi.mocked(mockHookManager.getConfigSnapshot).mockReturnValue({ + hooksByEvent: new Map(), + hooksById, + loadedAt: new Date(), + disabledHookIds: new Set(), + hasProjectHooks: false, + } as HooksConfigSnapshot) + + await webviewMessageHandler(mockClineProvider, { + type: "hooksSetAllEnabled", + hooksEnabled: false, + }) + + expect(mockHookManager.setHookEnabled).toHaveBeenCalledTimes(2) + expect(mockHookManager.setHookEnabled).toHaveBeenCalledWith("hook-1", false) + expect(mockHookManager.setHookEnabled).toHaveBeenCalledWith("hook-2", false) + expect(mockClineProvider.postStateToWebview).toHaveBeenCalledTimes(1) + }) + + it("should not call setHookEnabled when hooksEnabled is not a boolean", async () => { + await webviewMessageHandler(mockClineProvider, { + type: "hooksSetAllEnabled", + hooksEnabled: "false", + } as any) + + expect(mockHookManager.setHookEnabled).not.toHaveBeenCalled() + expect(mockClineProvider.postStateToWebview).not.toHaveBeenCalled() + }) + + it("should show error message when bulk setHookEnabled fails", async () => { + const hooksById = new Map() + hooksById.set("hook-1", { + id: "hook-1", + event: "PreToolUse" as any, + matcher: ".*", + command: "echo 1", + enabled: true, + source: "global" as any, + timeout: 30, + includeConversationHistory: false, + } as any) + + vi.mocked(mockHookManager.getConfigSnapshot).mockReturnValue({ + hooksByEvent: new Map(), + hooksById, + loadedAt: new Date(), + disabledHookIds: new Set(), + hasProjectHooks: false, + } as HooksConfigSnapshot) + + vi.mocked(mockHookManager.setHookEnabled).mockRejectedValueOnce(new Error("boom")) + + await webviewMessageHandler(mockClineProvider, { + type: "hooksSetAllEnabled", + hooksEnabled: true, + }) + + expect(mockClineProvider.log).toHaveBeenCalledWith("Failed to set all hooks enabled: boom") + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Failed to enable all hooks") + }) + }) + describe("hooksOpenConfigFolder", () => { it("should open project hooks folder by default", async () => { await webviewMessageHandler(mockClineProvider, { diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index e4ffd2858b..e39d0e3245 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -3374,6 +3374,30 @@ export const webviewMessageHandler = async ( break } + case "hooksSetAllEnabled": { + // Enable or disable ALL currently known hooks. + // This mirrors MCP's "Enable MCP Servers" top-level toggle. + const hookManager = provider.getHookManager() + if (hookManager && typeof message.hooksEnabled === "boolean") { + try { + const snapshot = hookManager.getConfigSnapshot() + const allHookIds = snapshot ? Array.from(snapshot.hooksById.keys()) : [] + + for (const hookId of allHookIds) { + await hookManager.setHookEnabled(hookId, message.hooksEnabled) + } + + await provider.postStateToWebview() + } catch (error) { + provider.log( + `Failed to set all hooks enabled: ${error instanceof Error ? error.message : String(error)}`, + ) + vscode.window.showErrorMessage(`Failed to ${message.hooksEnabled ? "enable" : "disable"} all hooks`) + } + } + break + } + case "hooksOpenConfigFolder": { // Open the hooks configuration folder in VS Code const source = message.hooksSource ?? "project" diff --git a/webview-ui/src/components/settings/HooksSettings.tsx b/webview-ui/src/components/settings/HooksSettings.tsx index 63b229a45c..28c55a6fa2 100644 --- a/webview-ui/src/components/settings/HooksSettings.tsx +++ b/webview-ui/src/components/settings/HooksSettings.tsx @@ -12,6 +12,7 @@ export const HooksSettings: React.FC = () => { const { t } = useAppTranslation() const { hooks } = useExtensionState() const [executionHistory, setExecutionHistory] = useState(hooks?.executionHistory || []) + const [isUpdatingAllEnabled, setIsUpdatingAllEnabled] = useState(false) // Listen for realtime hookExecutionStatus messages useEffect(() => { @@ -63,54 +64,57 @@ export const HooksSettings: React.FC = () => { vscode.postMessage({ type: "hooksSetEnabled", hookId, hookEnabled: enabled }) }, []) + const handleToggleAllHooks = useCallback((enabled: boolean) => { + setIsUpdatingAllEnabled(true) + vscode.postMessage({ type: "hooksSetAllEnabled", hooksEnabled: enabled }) + // Optimistically clear the "updating" flag after a short delay. + // The extension will send updated state via postStateToWebview(). + setTimeout(() => setIsUpdatingAllEnabled(false), 500) + }, []) + const enabledHooks = hooks?.enabledHooks || [] const hasProjectHooks = hooks?.hasProjectHooks || false const snapshotTimestamp = hooks?.snapshotTimestamp + const allHooksEnabled = enabledHooks.length > 0 && enabledHooks.every((h) => h.enabled) return (
{t("settings:sections.hooks")}
- {/* Header with actions */} -
-
-

{t("settings:hooks.configuredHooks")}

- {snapshotTimestamp && ( - - - - )} + {/* Enable all hooks */} + {enabledHooks.length > 0 && ( +
+
+ {t("settings:hooks.enableHooks")} + + {t("settings:hooks.enableHooksDescription")} + +
+
-
- - - + )} + + {/* Header */} +
+

{t("settings:hooks.configuredHooks")}

+ {snapshotTimestamp && ( - + content={t("settings:hooks.lastLoadedTooltip", { + time: new Date(snapshotTimestamp).toLocaleString(), + })}> + -
+ )}
{/* Security warning for project hooks */} @@ -160,6 +164,37 @@ export const HooksSettings: React.FC = () => { {/* Hook Activity Log */} + + {/* Bottom Action Buttons - mirroring MCP settings order: global, project, refresh */} +
+ + + + + +
) @@ -172,61 +207,236 @@ interface HookItemProps { const HookItem: React.FC = ({ hook, onToggle }) => { const { t } = useAppTranslation() + const { hooks } = useExtensionState() + const [isExpanded, setIsExpanded] = useState(false) + const [hookLogs, setHookLogs] = useState([]) + + // Filter execution history for this specific hook + useEffect(() => { + const history = hooks?.executionHistory || [] + const filtered = history.filter((record) => record.hookId === hook.id) + setHookLogs(filtered) + }, [hooks?.executionHistory, hook.id]) + + // Listen for realtime hookExecutionStatus messages for this hook + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + const message = event.data + if (message.type === "hookExecutionStatus") { + const payload: HookExecutionStatusPayload = message.hookExecutionStatus + + // Only process messages for this specific hook + if (payload.hookId === hook.id) { + if (payload.status === "completed" || payload.status === "failed" || payload.status === "blocked") { + const record: HookExecutionRecord = { + timestamp: new Date().toISOString(), + hookId: payload.hookId || "unknown", + event: payload.event, + toolName: payload.toolName, + exitCode: payload.status === "completed" ? 0 : 1, + duration: payload.duration || 0, + timedOut: false, + blocked: payload.status === "blocked", + error: payload.error, + blockMessage: payload.blockMessage, + } + + setHookLogs((prev) => [record, ...prev].slice(0, 20)) // Keep last 20 per hook + } + } + } + } + + window.addEventListener("message", handleMessage) + return () => window.removeEventListener("message", handleMessage) + }, [hook.id]) + + const handleToggle = (e: React.ChangeEvent) => { + e.stopPropagation() + onToggle(hook.id, e.target.checked) + } return ( -
-
-
-
- - {hook.event} - {hook.matcher && ( - <> - - - {hook.matcher} - - - )} - - {hook.source} - -
- - {hook.description &&

{hook.description}

} - -
- - {hook.commandPreview} - - {hook.shell && ( - - {t("settings:hooks.shell")}: {hook.shell} - - )} - - {t("settings:hooks.timeout")}: {hook.timeout}s - -
+
+ {/* Collapsed Header */} +
setIsExpanded(!isExpanded)}> + + ▶ + +
+ + {hook.id} + + {hook.source} +
- -
+ + {/* Expanded Content */} + {isExpanded && ( +
+ {/* Hook Details */} +
+
+ + {t("settings:hooks.event")}: + + {hook.event} +
+ {hook.matcher && ( +
+ + {t("settings:hooks.matcher")}: + + + {hook.matcher} + +
+ )} + {hook.description && ( +
+ + {t("settings:hooks.description")}: + +

{hook.description}

+
+ )} +
+ + {t("settings:hooks.command")}: + + + {hook.commandPreview} + +
+
+ {hook.shell && ( + + {t("settings:hooks.shell")}: {hook.shell} + + )} + + {t("settings:hooks.timeout")}: {hook.timeout}s + +
+
+ + {/* Logs Section */} +
+
+ {t("settings:hooks.logs")} + {hookLogs.length > 0 && ( + ({hookLogs.length}) + )} +
+ {hookLogs.length === 0 ? ( +
+ {t("settings:hooks.noLogsForHook")} +
+ ) : ( +
+ {hookLogs.map((record, index) => ( + + ))} +
+ )} +
+
+ )} +
+ ) +} + +interface HookLogItemProps { + record: HookExecutionRecord +} + +const HookLogItem: React.FC = ({ record }) => { + const { t } = useAppTranslation() + + const getStatusDisplay = () => { + if (record.blocked) { + return { + label: t("settings:hooks.status.blocked"), + className: "bg-red-500/20 text-red-500", + icon: , + } + } + if (record.error || record.exitCode !== 0) { + return { + label: t("settings:hooks.status.failed"), + className: "bg-red-500/20 text-red-500", + icon: , + } + } + if (record.timedOut) { + return { + label: t("settings:hooks.status.timeout"), + className: "bg-yellow-500/20 text-yellow-500", + icon: , + } + } + return { + label: t("settings:hooks.status.completed"), + className: "bg-green-500/20 text-green-500", + icon: , + } + } + + const status = getStatusDisplay() + const timestamp = new Date(record.timestamp) + const timeAgo = getTimeAgo(timestamp) + + return ( +
+
+
+ + {status.icon} + {status.label} + + {record.toolName && ( + + {record.toolName} + + )} +
+
+ {record.duration}ms + + {timeAgo} + +
+
+ + {(record.error || record.blockMessage) && ( +
+ {record.blockMessage || record.error} +
+ )}
) } diff --git a/webview-ui/src/components/settings/__tests__/HooksSettings.spec.tsx b/webview-ui/src/components/settings/__tests__/HooksSettings.spec.tsx index 6f9de43259..b87f6b8bba 100644 --- a/webview-ui/src/components/settings/__tests__/HooksSettings.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/HooksSettings.spec.tsx @@ -104,9 +104,173 @@ describe("HooksSettings", () => { render() + // Hook ID should be visible in collapsed state + expect(screen.getByText(mockHook.id)).toBeInTheDocument() + expect(screen.getByText(mockHook.source)).toBeInTheDocument() + }) + + it("expands and collapses hook accordion on click", () => { + const mockHook: HookInfo = { + id: "hook-1", + event: "before_execute_command", + matcher: "git*", + commandPreview: "echo 'Before git command'", + enabled: true, + source: "project", + timeout: 30, + description: "Test hook", + } + + currentHooksState = { + enabledHooks: [mockHook], + executionHistory: [], + hasProjectHooks: false, + } + + render() + + // Hook details should not be visible initially + expect(screen.queryByText(mockHook.event)).not.toBeInTheDocument() + expect(screen.queryByText(mockHook.matcher!)).not.toBeInTheDocument() + + // Click to expand + const hookHeader = screen.getByText(mockHook.id).closest("div") + fireEvent.click(hookHeader!) + + // Hook details should now be visible expect(screen.getByText(mockHook.event)).toBeInTheDocument() expect(screen.getByText(mockHook.matcher!)).toBeInTheDocument() expect(screen.getByText(mockHook.commandPreview)).toBeInTheDocument() + + // Click to collapse + fireEvent.click(hookHeader!) + + // Hook details should be hidden again + expect(screen.queryByText(mockHook.event)).not.toBeInTheDocument() + }) + + it("shows per-hook logs in expanded view", () => { + const mockHook: HookInfo = { + id: "hook-1", + event: "before_execute_command", + commandPreview: "echo test", + enabled: true, + source: "global", + timeout: 30, + } + + const mockRecord: HookExecutionRecord = { + timestamp: new Date().toISOString(), + hookId: "hook-1", + event: "before_execute_command", + toolName: "write_to_file", + exitCode: 0, + duration: 150, + timedOut: false, + blocked: false, + } + + currentHooksState = { + enabledHooks: [mockHook], + executionHistory: [mockRecord], + hasProjectHooks: false, + } + + render() + + // Expand hook + const hookHeader = screen.getByText(mockHook.id).closest("div") + fireEvent.click(hookHeader!) + + // Logs section should be visible + expect(screen.getByText("settings:hooks.logs")).toBeInTheDocument() + expect(screen.getByText(mockRecord.toolName!)).toBeInTheDocument() + expect(screen.getByText("settings:hooks.status.completed")).toBeInTheDocument() + }) + + it("filters logs per hook correctly", () => { + const mockHook1: HookInfo = { + id: "hook-1", + event: "before_execute_command", + commandPreview: "echo test", + enabled: true, + source: "global", + timeout: 30, + } + + const mockHook2: HookInfo = { + id: "hook-2", + event: "after_execute_command", + commandPreview: "echo after", + enabled: true, + source: "global", + timeout: 30, + } + + const record1: HookExecutionRecord = { + timestamp: new Date().toISOString(), + hookId: "hook-1", + event: "before_execute_command", + toolName: "write_to_file", + exitCode: 0, + duration: 100, + timedOut: false, + blocked: false, + } + + const record2: HookExecutionRecord = { + timestamp: new Date().toISOString(), + hookId: "hook-2", + event: "after_execute_command", + toolName: "read_file", + exitCode: 0, + duration: 50, + timedOut: false, + blocked: false, + } + + currentHooksState = { + enabledHooks: [mockHook1, mockHook2], + executionHistory: [record1, record2], + hasProjectHooks: false, + } + + render() + + // Expand first hook + const hook1Headers = screen.getAllByText("hook-1") + const hook1Header = hook1Headers[0].closest("div") + fireEvent.click(hook1Header!) + + // Should show only hook-1's log + expect(screen.getByText("write_to_file")).toBeInTheDocument() + expect(screen.queryByText("read_file")).not.toBeInTheDocument() + }) + + it("shows 'no logs' message when hook has no execution history", () => { + const mockHook: HookInfo = { + id: "hook-1", + event: "before_execute_command", + commandPreview: "echo test", + enabled: true, + source: "global", + timeout: 30, + } + + currentHooksState = { + enabledHooks: [mockHook], + executionHistory: [], + hasProjectHooks: false, + } + + render() + + // Expand hook + const hookHeader = screen.getByText(mockHook.id).closest("div") + fireEvent.click(hookHeader!) + + // Should show no logs message + expect(screen.getByText("settings:hooks.noLogsForHook")).toBeInTheDocument() }) it("shows project hooks warning when hasProjectHooks is true", () => { @@ -121,27 +285,40 @@ describe("HooksSettings", () => { expect(screen.getByText("settings:hooks.projectHooksWarningMessage")).toBeInTheDocument() }) - it("sends hooksReloadConfig message when Reload button is clicked", async () => { + it("sends hooksReloadConfig message when Reload button is clicked (bottom action)", async () => { const { vscode } = await import("@src/utils/vscode") render() + // Reload button is now in bottom action area (mirroring MCP settings) const reloadButton = screen.getByText("settings:hooks.reload") fireEvent.click(reloadButton) expect(vscode.postMessage).toHaveBeenCalledWith({ type: "hooksReloadConfig" }) }) - it("sends hooksOpenConfigFolder message when Open Folder button is clicked", async () => { + it("sends hooksOpenConfigFolder message with 'global' when Global Folder button is clicked (bottom action)", async () => { const { vscode } = await import("@src/utils/vscode") - currentHooksState = { - ...mockHooksState, - hasProjectHooks: true, - } render() - const openFolderButton = screen.getByText("settings:hooks.openProjectFolder") - fireEvent.click(openFolderButton) + // Button is now in bottom action area (like MCP settings) + const globalFolderButton = screen.getByText("settings:hooks.openGlobalFolder") + fireEvent.click(globalFolderButton) + + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: "hooksOpenConfigFolder", + hooksSource: "global", + }) + }) + + it("sends hooksOpenConfigFolder message with 'project' when Project Folder button is clicked (bottom action)", async () => { + const { vscode } = await import("@src/utils/vscode") + + render() + + // Button is now in bottom action area (like MCP settings) + const projectFolderButton = screen.getByText("settings:hooks.openProjectFolder") + fireEvent.click(projectFolderButton) expect(vscode.postMessage).toHaveBeenCalledWith({ type: "hooksOpenConfigFolder", @@ -149,6 +326,19 @@ describe("HooksSettings", () => { }) }) + it("renders both Global and Project folder buttons in bottom action area regardless of hasProjectHooks state", () => { + currentHooksState = { + ...mockHooksState, + hasProjectHooks: false, + } + + render() + + // Both buttons should be present + expect(screen.getByText("settings:hooks.openGlobalFolder")).toBeInTheDocument() + expect(screen.getByText("settings:hooks.openProjectFolder")).toBeInTheDocument() + }) + it("sends hooksSetEnabled message when hook toggle is changed", async () => { const { vscode } = await import("@src/utils/vscode") const mockHook: HookInfo = { @@ -168,8 +358,9 @@ describe("HooksSettings", () => { render() - const checkbox = screen.getByRole("checkbox") - fireEvent.click(checkbox) + // First checkbox is the top-level "Enable Hooks" toggle; the second is the per-hook toggle in collapsed header + const checkboxes = screen.getAllByRole("checkbox") + fireEvent.click(checkboxes[1]) expect(vscode.postMessage).toHaveBeenCalledWith({ type: "hooksSetEnabled", @@ -178,6 +369,80 @@ describe("HooksSettings", () => { }) }) + it("toggles hook enabled state in collapsed view without expanding accordion", async () => { + const { vscode } = await import("@src/utils/vscode") + const mockHook: HookInfo = { + id: "hook-1", + event: "before_execute_command", + commandPreview: "echo test", + enabled: true, + source: "global", + timeout: 30, + } + + currentHooksState = { + enabledHooks: [mockHook], + executionHistory: [], + hasProjectHooks: false, + } + + render() + + // Hook should be collapsed initially + expect(screen.queryByText(mockHook.event)).not.toBeInTheDocument() + + // Click the checkbox (second checkbox, first is "Enable Hooks") + const checkboxes = screen.getAllByRole("checkbox") + fireEvent.click(checkboxes[1]) + + // Hook should still be collapsed after toggling + expect(screen.queryByText(mockHook.event)).not.toBeInTheDocument() + + // Toggle message should have been sent + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: "hooksSetEnabled", + hookId: "hook-1", + hookEnabled: false, + }) + }) + + it("sends hooksSetAllEnabled message when top-level Enable Hooks toggle is changed", async () => { + const { vscode } = await import("@src/utils/vscode") + + currentHooksState = { + enabledHooks: [ + { + id: "hook-1", + event: "event1", + commandPreview: "cmd1", + enabled: true, + source: "global", + timeout: 30, + }, + { + id: "hook-2", + event: "event2", + commandPreview: "cmd2", + enabled: true, + source: "project", + timeout: 30, + }, + ], + executionHistory: [], + hasProjectHooks: false, + } + + render() + + const checkboxes = screen.getAllByRole("checkbox") + fireEvent.click(checkboxes[0]) + + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: "hooksSetAllEnabled", + hooksEnabled: false, + }) + }) + it("renders execution history when available", () => { const mockRecord: HookExecutionRecord = { timestamp: new Date().toISOString(), diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 514b7b5f74..2e8189fe9e 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -63,9 +63,17 @@ }, "noHooksConfigured": "No hooks configured", "noHooksHint": "Create hook configuration files to automate actions on tool execution events.", + "enableHooks": "Enable Hooks", + "enableHooksDescription": "Toggle all hooks on or off at once", "enabled": "Enabled", + "event": "Event", + "matcher": "Matcher", + "description": "Description", + "command": "Command", "shell": "Shell", "timeout": "Timeout", + "logs": "Logs", + "noLogsForHook": "No execution logs for this hook yet", "activityLog": "Hook Activity", "status": { "running": "Running",