mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-05 08:10:14 +00:00
feat: open hook config file from UI
This commit is contained in:
parent
ff4d057f1e
commit
a13fba4c88
6 changed files with 181 additions and 2 deletions
|
|
@ -622,6 +622,7 @@ export interface WebviewMessage {
|
|||
| "hooksSetAllEnabled"
|
||||
| "hooksOpenConfigFolder"
|
||||
| "hooksDeleteHook"
|
||||
| "hooksOpenHookFile"
|
||||
text?: string
|
||||
editedMessageContent?: string
|
||||
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud"
|
||||
|
|
@ -681,6 +682,7 @@ export interface WebviewMessage {
|
|||
hookEnabled?: boolean // For hooksSetEnabled
|
||||
hooksEnabled?: boolean // For hooksSetAllEnabled
|
||||
hooksSource?: "global" | "project" | "mode" // For hooksOpenConfigFolder, hooksDeleteHook
|
||||
filePath?: string // For hooksOpenHookFile
|
||||
codeIndexSettings?: {
|
||||
// Global state settings
|
||||
codebaseIndexEnabled: boolean
|
||||
|
|
|
|||
|
|
@ -7,14 +7,18 @@ vi.mock("vscode", () => {
|
|||
const executeCommand = vi.fn().mockResolvedValue(undefined)
|
||||
const showInformationMessage = vi.fn()
|
||||
const showErrorMessage = vi.fn()
|
||||
const showTextDocument = vi.fn().mockResolvedValue(undefined)
|
||||
const openTextDocument = vi.fn().mockResolvedValue({ uri: { fsPath: "/mock/file" } })
|
||||
|
||||
return {
|
||||
window: {
|
||||
showInformationMessage,
|
||||
showErrorMessage,
|
||||
showTextDocument,
|
||||
},
|
||||
workspace: {
|
||||
workspaceFolders: [{ uri: { fsPath: "/mock/workspace" } }],
|
||||
openTextDocument,
|
||||
},
|
||||
commands: {
|
||||
executeCommand,
|
||||
|
|
@ -505,6 +509,55 @@ describe("webviewMessageHandler - hooks commands", () => {
|
|||
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Failed to delete hook")
|
||||
})
|
||||
})
|
||||
|
||||
describe("hooksOpenHookFile", () => {
|
||||
it("should open hook file in editor when filePath is provided and file exists", async () => {
|
||||
const hookFilePath = "/mock/workspace/.roo/hooks/hooks.json"
|
||||
|
||||
await webviewMessageHandler(mockClineProvider, {
|
||||
type: "hooksOpenHookFile",
|
||||
filePath: hookFilePath,
|
||||
} as any)
|
||||
|
||||
expect(vscode.Uri.file).toHaveBeenCalledWith(hookFilePath)
|
||||
expect(vscode.workspace.openTextDocument).toHaveBeenCalled()
|
||||
expect(vscode.window.showTextDocument).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should show error message when file does not exist", async () => {
|
||||
const hookFilePath = "/mock/workspace/.roo/hooks/missing.json"
|
||||
vi.mocked(fsUtils.fileExistsAtPath).mockResolvedValueOnce(false)
|
||||
|
||||
await webviewMessageHandler(mockClineProvider, {
|
||||
type: "hooksOpenHookFile",
|
||||
filePath: hookFilePath,
|
||||
} as any)
|
||||
|
||||
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(`Hook file not found: ${hookFilePath}`)
|
||||
})
|
||||
|
||||
it("should not attempt to open when filePath is missing", async () => {
|
||||
await webviewMessageHandler(mockClineProvider, {
|
||||
type: "hooksOpenHookFile",
|
||||
} as any)
|
||||
|
||||
expect(vscode.workspace.openTextDocument).not.toHaveBeenCalled()
|
||||
expect(vscode.window.showTextDocument).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should show error message when open fails", async () => {
|
||||
const hookFilePath = "/mock/workspace/.roo/hooks/hooks.json"
|
||||
vi.mocked(vscode.workspace.openTextDocument).mockRejectedValueOnce(new Error("Cannot open file"))
|
||||
|
||||
await webviewMessageHandler(mockClineProvider, {
|
||||
type: "hooksOpenHookFile",
|
||||
filePath: hookFilePath,
|
||||
} as any)
|
||||
|
||||
expect(mockClineProvider.log).toHaveBeenCalledWith(expect.stringContaining("Failed to open hook file"))
|
||||
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Failed to open hook configuration file")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("webviewMessageHandler - hooks state integration", () => {
|
||||
|
|
|
|||
|
|
@ -3535,6 +3535,29 @@ export const webviewMessageHandler = async (
|
|||
break
|
||||
}
|
||||
|
||||
case "hooksOpenHookFile": {
|
||||
const { filePath: hookFilePath } = message
|
||||
if (!hookFilePath) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const exists = await fileExistsAtPath(hookFilePath)
|
||||
if (exists) {
|
||||
// Open the file in the editor
|
||||
const uri = vscode.Uri.file(hookFilePath)
|
||||
const doc = await vscode.workspace.openTextDocument(uri)
|
||||
await vscode.window.showTextDocument(doc)
|
||||
} else {
|
||||
vscode.window.showErrorMessage(`Hook file not found: ${hookFilePath}`)
|
||||
}
|
||||
} catch (error) {
|
||||
provider.log(`Failed to open hook file: ${error}`)
|
||||
vscode.window.showErrorMessage("Failed to open hook configuration file")
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
// console.log(`Unhandled message type: ${message.type}`)
|
||||
//
|
||||
|
|
|
|||
|
|
@ -276,6 +276,14 @@ const HookItem: React.FC<HookItemProps> = ({ hook, onToggle }) => {
|
|||
})
|
||||
}
|
||||
|
||||
const handleOpenHookFile = () => {
|
||||
if (!hook.filePath) return
|
||||
vscode.postMessage({
|
||||
type: "hooksOpenHookFile",
|
||||
filePath: hook.filePath,
|
||||
})
|
||||
}
|
||||
|
||||
const getEnabledDotColor = () => {
|
||||
return hook.enabled ? "var(--vscode-testing-iconPassed)" : "var(--vscode-descriptionForeground)"
|
||||
}
|
||||
|
|
@ -311,10 +319,26 @@ const HookItem: React.FC<HookItemProps> = ({ hook, onToggle }) => {
|
|||
size="icon"
|
||||
onClick={handleDeleteHook}
|
||||
data-testid={`hook-delete-${hook.id}`}
|
||||
aria-label={t("settings:hooks.deleteHook")}
|
||||
style={{ marginRight: "6px" }}>
|
||||
aria-label={t("settings:hooks.deleteHook")}>
|
||||
<span className="codicon codicon-trash" style={{ fontSize: "14px" }}></span>
|
||||
</Button>
|
||||
<StandardTooltip
|
||||
content={
|
||||
hook.filePath
|
||||
? t("settings:hooks.openHookFileTooltip")
|
||||
: t("settings:hooks.openHookFileUnavailableTooltip")
|
||||
}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleOpenHookFile}
|
||||
disabled={!hook.filePath}
|
||||
data-testid={`hook-open-file-${hook.id}`}
|
||||
aria-label={t("settings:hooks.openHookFile")}
|
||||
style={{ marginRight: "6px" }}>
|
||||
<span className="codicon codicon-link-external" style={{ fontSize: "14px" }}></span>
|
||||
</Button>
|
||||
</StandardTooltip>
|
||||
<div
|
||||
data-testid={`hook-status-dot-${hook.id}`}
|
||||
style={{
|
||||
|
|
|
|||
|
|
@ -463,6 +463,81 @@ describe("HooksSettings", () => {
|
|||
})
|
||||
})
|
||||
|
||||
it("sends hooksOpenHookFile message when open file button is clicked", async () => {
|
||||
const { vscode } = await import("@src/utils/vscode")
|
||||
const mockHook: HookInfo = {
|
||||
id: "hook-1",
|
||||
event: "before_execute_command",
|
||||
commandPreview: "echo test",
|
||||
enabled: true,
|
||||
source: "project",
|
||||
timeout: 30,
|
||||
filePath: "/path/to/hooks.json",
|
||||
}
|
||||
|
||||
currentHooksState = {
|
||||
enabledHooks: [mockHook],
|
||||
executionHistory: [],
|
||||
hasProjectHooks: false,
|
||||
}
|
||||
|
||||
render(<HooksSettings />)
|
||||
|
||||
fireEvent.click(screen.getByTestId("hook-open-file-hook-1"))
|
||||
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: "hooksOpenHookFile",
|
||||
filePath: "/path/to/hooks.json",
|
||||
})
|
||||
})
|
||||
|
||||
it("shows correct tooltip for open file button when file path is available", () => {
|
||||
const mockHook: HookInfo = {
|
||||
id: "hook-1",
|
||||
event: "before_execute_command",
|
||||
commandPreview: "echo test",
|
||||
enabled: true,
|
||||
source: "project",
|
||||
timeout: 30,
|
||||
filePath: "/path/to/hooks.json",
|
||||
}
|
||||
|
||||
currentHooksState = {
|
||||
enabledHooks: [mockHook],
|
||||
executionHistory: [],
|
||||
hasProjectHooks: false,
|
||||
}
|
||||
|
||||
render(<HooksSettings />)
|
||||
|
||||
const button = screen.getByTestId("hook-open-file-hook-1")
|
||||
// In the mock, StandardTooltip wraps the button with a div having the title
|
||||
expect(button.parentElement).toHaveAttribute("title", "settings:hooks.openHookFileTooltip")
|
||||
})
|
||||
|
||||
it("shows correct tooltip for open file button when file path is unavailable", () => {
|
||||
const mockHook: HookInfo = {
|
||||
id: "hook-1",
|
||||
event: "before_execute_command",
|
||||
commandPreview: "echo test",
|
||||
enabled: true,
|
||||
source: "project",
|
||||
timeout: 30,
|
||||
// No filePath
|
||||
}
|
||||
|
||||
currentHooksState = {
|
||||
enabledHooks: [mockHook],
|
||||
executionHistory: [],
|
||||
hasProjectHooks: false,
|
||||
}
|
||||
|
||||
render(<HooksSettings />)
|
||||
|
||||
const button = screen.getByTestId("hook-open-file-hook-1")
|
||||
expect(button.parentElement).toHaveAttribute("title", "settings:hooks.openHookFileUnavailableTooltip")
|
||||
})
|
||||
|
||||
it("sends hooksSetAllEnabled message when top-level Enable Hooks toggle is changed", async () => {
|
||||
const { vscode } = await import("@src/utils/vscode")
|
||||
|
||||
|
|
|
|||
|
|
@ -56,6 +56,8 @@
|
|||
"projectHooksWarningTitle": "Project-level hooks detected",
|
||||
"projectHooksWarningMessage": "This project includes hook configurations that will execute shell commands. Only enable hooks from sources you trust.",
|
||||
"reloadNote": "Changes to hook configuration files require clicking Reload to take effect.",
|
||||
"openHookFileTooltip": "Open hook file in editor",
|
||||
"openHookFileUnavailableTooltip": "Hook file location unavailable",
|
||||
"matcherNote": "Matchers are evaluated against Roo Code's internal tool IDs (e.g. write_to_file, edit_file, apply_diff, apply_patch), not UI labels like Write/Edit.",
|
||||
"matcherExamplesLabel": "Examples:",
|
||||
"matcherExamples": {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue