mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
fix: improve hooks settings layout and UI
This commit is contained in:
parent
a13fba4c88
commit
8bb5b61555
5 changed files with 207 additions and 73 deletions
|
|
@ -2254,7 +2254,7 @@ export class ClineProvider
|
|||
filePath: hook.filePath,
|
||||
event: hook.event,
|
||||
matcher: hook.matcher,
|
||||
commandPreview: hook.command.length > 100 ? hook.command.substring(0, 97) + "..." : hook.command,
|
||||
commandPreview: hook.command,
|
||||
enabled: (hook.enabled ?? true) && !(snapshot?.disabledHookIds?.has(hook.id) ?? false),
|
||||
source: hook.source,
|
||||
timeout: hook.timeout ?? 60,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useCallback, useEffect, useState } from "react"
|
||||
import { RefreshCw, FolderOpen, AlertTriangle, Clock, Zap, X } 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"
|
||||
import { vscode } from "@src/utils/vscode"
|
||||
|
|
@ -361,73 +362,103 @@ const HookItem: React.FC<HookItemProps> = ({ hook, onToggle }) => {
|
|||
|
||||
{/* Expanded Content */}
|
||||
{isExpanded && (
|
||||
<div className="px-3 pb-3 pt-0 space-y-3 border-t border-vscode-input-border">
|
||||
{/* Hook Details */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-vscode-descriptionForeground">
|
||||
{t("settings:hooks.event")}:
|
||||
</span>
|
||||
<code className="text-sm font-mono text-vscode-textLink-foreground">{hook.event}</code>
|
||||
</div>
|
||||
{hook.matcher && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-vscode-descriptionForeground">
|
||||
{t("settings:hooks.matcher")}:
|
||||
</span>
|
||||
<code className="text-xs font-mono text-vscode-descriptionForeground">
|
||||
{hook.matcher}
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
{hook.description && (
|
||||
<div>
|
||||
<span className="text-xs text-vscode-descriptionForeground">
|
||||
{t("settings:hooks.description")}:
|
||||
</span>
|
||||
<p className="text-sm text-vscode-foreground mt-1">{hook.description}</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-vscode-descriptionForeground">
|
||||
{t("settings:hooks.command")}:
|
||||
</span>
|
||||
<code className="text-xs font-mono bg-vscode-editor-background px-2 py-1 rounded flex-1">
|
||||
{hook.commandPreview}
|
||||
</code>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-xs text-vscode-descriptionForeground">
|
||||
{hook.shell && (
|
||||
<span>
|
||||
{t("settings:hooks.shell")}: <code className="font-mono">{hook.shell}</code>
|
||||
</span>
|
||||
)}
|
||||
<span>
|
||||
{t("settings:hooks.timeout")}: {hook.timeout}s
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t border-vscode-input-border p-3">
|
||||
<VSCodePanels>
|
||||
<VSCodePanelTab id="config">{t("settings:hooks.tabs.config")}</VSCodePanelTab>
|
||||
<VSCodePanelTab id="command">{t("settings:hooks.tabs.command")}</VSCodePanelTab>
|
||||
<VSCodePanelTab id="logs">
|
||||
{t("settings:hooks.tabs.logs")}
|
||||
{hookLogs.length > 0 && <span className="ml-1 opacity-60">({hookLogs.length})</span>}
|
||||
</VSCodePanelTab>
|
||||
|
||||
{/* Logs Section */}
|
||||
<div className="border-t border-vscode-input-border pt-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-sm font-medium">{t("settings:hooks.logs")}</span>
|
||||
{hookLogs.length > 0 && (
|
||||
<span className="text-xs text-vscode-descriptionForeground">({hookLogs.length})</span>
|
||||
)}
|
||||
</div>
|
||||
{hookLogs.length === 0 ? (
|
||||
<div className="text-xs text-vscode-descriptionForeground py-2">
|
||||
{t("settings:hooks.noLogsForHook")}
|
||||
<VSCodePanelView id="view-config">
|
||||
<div className="flex flex-col gap-3 pt-3 w-full">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<span className="text-xs font-medium text-vscode-descriptionForeground block mb-1">
|
||||
{t("settings:hooks.event")}
|
||||
</span>
|
||||
<code className="text-xs font-mono text-vscode-textLink-foreground bg-vscode-textCodeBlock-background px-1.5 py-0.5 rounded">
|
||||
{hook.event}
|
||||
</code>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-xs font-medium text-vscode-descriptionForeground block mb-1">
|
||||
{t("settings:hooks.timeout")}
|
||||
</span>
|
||||
<span className="text-xs text-vscode-foreground">{hook.timeout}s</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hook.matcher && (
|
||||
<div>
|
||||
<span className="text-xs font-medium text-vscode-descriptionForeground block mb-1">
|
||||
{t("settings:hooks.matcher")}
|
||||
</span>
|
||||
<div className="bg-vscode-textCodeBlock-background p-2 rounded border border-vscode-widget-border">
|
||||
<ul className="list-disc list-inside text-xs font-mono text-vscode-foreground">
|
||||
{hook.matcher
|
||||
.split("|")
|
||||
.map((m) => m.trim())
|
||||
.filter(Boolean)
|
||||
.map((m, i) => (
|
||||
<li key={i}>{m}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hook.shell && (
|
||||
<div>
|
||||
<span className="text-xs font-medium text-vscode-descriptionForeground block mb-1">
|
||||
{t("settings:hooks.shell")}
|
||||
</span>
|
||||
<code className="text-xs font-mono text-vscode-foreground bg-vscode-textCodeBlock-background px-1.5 py-0.5 rounded">
|
||||
{hook.shell}
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hook.description && (
|
||||
<div>
|
||||
<span className="text-xs font-medium text-vscode-descriptionForeground block mb-1">
|
||||
{t("settings:hooks.description")}
|
||||
</span>
|
||||
<p className="text-xs text-vscode-foreground">{hook.description}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2 max-h-48 overflow-y-auto">
|
||||
{hookLogs.map((record, index) => (
|
||||
<HookLogItem key={`${record.timestamp}-${index}`} record={record} />
|
||||
))}
|
||||
</VSCodePanelView>
|
||||
|
||||
<VSCodePanelView id="view-command">
|
||||
<div className="pt-3 w-full">
|
||||
<div className="bg-vscode-textCodeBlock-background p-3 rounded border border-vscode-widget-border overflow-x-auto max-h-48 overflow-y-auto">
|
||||
<code
|
||||
data-testid={`command-preview-${hook.id}`}
|
||||
className="text-xs font-mono text-vscode-foreground whitespace-pre-wrap break-words">
|
||||
{hook.commandPreview}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</VSCodePanelView>
|
||||
|
||||
<VSCodePanelView id="view-logs">
|
||||
<div className="pt-3 w-full">
|
||||
{hookLogs.length === 0 ? (
|
||||
<div className="text-xs text-vscode-descriptionForeground">
|
||||
{t("settings:hooks.noLogsForHook")}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2 max-h-48 overflow-y-auto">
|
||||
{hookLogs.map((record, index) => (
|
||||
<HookLogItem key={`${record.timestamp}-${index}`} record={record} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</VSCodePanelView>
|
||||
</VSCodePanels>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -484,7 +515,9 @@ const HookLogItem: React.FC<HookLogItemProps> = ({ record }) => {
|
|||
{status.label}
|
||||
</span>
|
||||
{record.toolName && (
|
||||
<code className="text-xs font-mono text-vscode-descriptionForeground truncate">
|
||||
<code
|
||||
data-testid="log-tool-name"
|
||||
className="text-xs font-mono text-vscode-descriptionForeground break-words">
|
||||
{record.toolName}
|
||||
</code>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ export const sectionNames = [
|
|||
"providers",
|
||||
"autoApprove",
|
||||
"slashCommands",
|
||||
"hooks",
|
||||
"browser",
|
||||
"checkpoints",
|
||||
"notifications",
|
||||
|
|
@ -106,7 +107,6 @@ export const sectionNames = [
|
|||
"terminal",
|
||||
"modes",
|
||||
"mcp",
|
||||
"hooks",
|
||||
"prompts",
|
||||
"ui",
|
||||
"experimental",
|
||||
|
|
@ -528,12 +528,12 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
{ id: "mcp", icon: Server },
|
||||
{ id: "autoApprove", icon: CheckCheck },
|
||||
{ id: "slashCommands", icon: SquareSlash },
|
||||
{ id: "hooks", icon: Zap },
|
||||
{ id: "browser", icon: SquareMousePointer },
|
||||
{ id: "checkpoints", icon: GitBranch },
|
||||
{ id: "notifications", icon: Bell },
|
||||
{ id: "contextManagement", icon: Database },
|
||||
{ id: "terminal", icon: SquareTerminal },
|
||||
{ id: "hooks", icon: Zap },
|
||||
{ id: "prompts", icon: MessageSquare },
|
||||
{ id: "ui", icon: Glasses },
|
||||
{ id: "experimental", icon: FlaskConical },
|
||||
|
|
|
|||
|
|
@ -5,6 +5,25 @@ import { vi, describe, it, expect, beforeEach } from "vitest"
|
|||
import { HooksSettings } from "../HooksSettings"
|
||||
import type { HookInfo, HookExecutionRecord, HooksState } from "@roo-code/types"
|
||||
|
||||
// Mock webview-ui-toolkit components
|
||||
vi.mock("@vscode/webview-ui-toolkit/react", () => ({
|
||||
VSCodePanels: ({ children, ...props }: any) => (
|
||||
<div data-testid="vscode-panels" {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
VSCodePanelTab: ({ children, id, ...props }: any) => (
|
||||
<div data-testid={`tab-${id}`} data-tab-id={id} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
VSCodePanelView: ({ children, id, ...props }: any) => (
|
||||
<div data-testid={`panel-${id}`} data-panel-id={id} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock vscode utilities
|
||||
vi.mock("@src/utils/vscode", () => ({
|
||||
vscode: {
|
||||
|
|
@ -147,9 +166,16 @@ describe("HooksSettings", () => {
|
|||
const hookHeader = screen.getByText(mockHook.id).closest("div")
|
||||
fireEvent.click(hookHeader!)
|
||||
|
||||
// Hook details should now be visible
|
||||
// Should show tabs
|
||||
expect(screen.getByTestId("tab-config")).toBeInTheDocument()
|
||||
expect(screen.getByTestId("tab-command")).toBeInTheDocument()
|
||||
expect(screen.getByTestId("tab-logs")).toBeInTheDocument()
|
||||
|
||||
// Check Config tab content (visible by default usually or we can check panels exist)
|
||||
expect(screen.getByText(mockHook.event)).toBeInTheDocument()
|
||||
expect(screen.getByText(mockHook.matcher!)).toBeInTheDocument()
|
||||
|
||||
// Check Command tab content
|
||||
expect(screen.getByText(mockHook.commandPreview)).toBeInTheDocument()
|
||||
|
||||
// Click to collapse
|
||||
|
|
@ -159,7 +185,7 @@ describe("HooksSettings", () => {
|
|||
expect(screen.queryByText(mockHook.event)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("shows per-hook logs in expanded view", () => {
|
||||
it("shows per-hook logs in Logs tab", () => {
|
||||
const mockHook: HookInfo = {
|
||||
id: "hook-1",
|
||||
event: "before_execute_command",
|
||||
|
|
@ -192,12 +218,82 @@ describe("HooksSettings", () => {
|
|||
const hookHeader = screen.getByText(mockHook.id).closest("div")
|
||||
fireEvent.click(hookHeader!)
|
||||
|
||||
// Logs section should be visible
|
||||
expect(screen.getByText("settings:hooks.logs")).toBeInTheDocument()
|
||||
// Find Logs tab panel content
|
||||
expect(screen.getByTestId("panel-view-logs")).toBeInTheDocument()
|
||||
expect(screen.getByText(mockRecord.toolName!)).toBeInTheDocument()
|
||||
expect(screen.getByText("settings:hooks.status.completed")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders command preview with wrapping enabled (no truncation)", () => {
|
||||
const longCommand = "long_command_".repeat(20)
|
||||
const mockHook: HookInfo = {
|
||||
id: "hook-1",
|
||||
event: "before_execute_command",
|
||||
matcher: "git*",
|
||||
commandPreview: longCommand,
|
||||
enabled: true,
|
||||
source: "project",
|
||||
timeout: 30,
|
||||
}
|
||||
|
||||
currentHooksState = {
|
||||
enabledHooks: [mockHook],
|
||||
executionHistory: [],
|
||||
hasProjectHooks: false,
|
||||
}
|
||||
|
||||
render(<HooksSettings />)
|
||||
|
||||
// Expand hook
|
||||
const hookHeader = screen.getByText(mockHook.id).closest("div")
|
||||
fireEvent.click(hookHeader!)
|
||||
|
||||
const commandCode = screen.getByTestId(`command-preview-${mockHook.id}`)
|
||||
expect(commandCode).toHaveClass("whitespace-pre-wrap")
|
||||
expect(commandCode).toHaveClass("break-words")
|
||||
expect(commandCode).not.toHaveClass("truncate")
|
||||
expect(commandCode).toHaveTextContent(longCommand)
|
||||
expect(commandCode.textContent).not.toContain("...")
|
||||
})
|
||||
|
||||
it("renders log items with wrapping enabled (no truncation)", () => {
|
||||
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: "very_long_tool_name_that_should_not_be_truncated_" + "x".repeat(20),
|
||||
exitCode: 0,
|
||||
duration: 150,
|
||||
timedOut: false,
|
||||
blocked: false,
|
||||
}
|
||||
|
||||
currentHooksState = {
|
||||
enabledHooks: [mockHook],
|
||||
executionHistory: [mockRecord],
|
||||
hasProjectHooks: false,
|
||||
}
|
||||
|
||||
render(<HooksSettings />)
|
||||
|
||||
// Expand hook
|
||||
const hookHeader = screen.getByText(mockHook.id).closest("div")
|
||||
fireEvent.click(hookHeader!)
|
||||
|
||||
const toolName = screen.getByTestId("log-tool-name")
|
||||
expect(toolName).toHaveClass("break-words")
|
||||
expect(toolName).not.toHaveClass("truncate")
|
||||
})
|
||||
|
||||
it("filters logs per hook correctly", () => {
|
||||
const mockHook1: HookInfo = {
|
||||
id: "hook-1",
|
||||
|
|
@ -280,7 +376,7 @@ describe("HooksSettings", () => {
|
|||
fireEvent.click(hookHeader!)
|
||||
|
||||
// Should show no logs message
|
||||
expect(screen.getByText("settings:hooks.noLogsForHook")).toBeInTheDocument()
|
||||
expect(screen.getAllByText("settings:hooks.noLogsForHook")[0]).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("shows project hooks warning when hasProjectHooks is true", () => {
|
||||
|
|
|
|||
|
|
@ -83,6 +83,11 @@
|
|||
"failed": "Failed",
|
||||
"blocked": "Blocked",
|
||||
"timeout": "Timeout"
|
||||
},
|
||||
"tabs": {
|
||||
"config": "Config",
|
||||
"command": "Command",
|
||||
"logs": "Logs"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue