Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit bbced2b

Browse files
committed
feat: improve HookItem UI with copy duplication and file navigation
- Remove link icon from hook item header - Style source tag (project/global) as grey badge matching matchers - Change copy button to duplicate hook with unique ID suffix - Add open-in-editor functionality to navigate directly to hook in YAML - Change folder icon to file icon for the open button - Add HookItem component tests Changes: - packages/types: Add hooks/openFile message type - webviewMessageHandler: Handle hooks/openFile message - HooksService: Add openHookInEditor method - HooksSettings: Update copy and open handlers - HookItem: UI improvements and icon changes
1 parent a96c460 commit bbced2b

6 files changed

Lines changed: 606 additions & 47 deletions

File tree

packages/types/src/vscode-extension-host.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,7 @@ export interface WebviewMessage {
618618
| "hooks/reorder"
619619
| "hooks/move"
620620
| "hooks/openFolder"
621+
| "hooks/openFile"
621622
| "hooks/reload"
622623
text?: string
623624
editedMessageContent?: string

src/core/webview/webviewMessageHandler.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3480,6 +3480,24 @@ export const webviewMessageHandler = async (
34803480
break
34813481
}
34823482

3483+
case "hooks/openFile": {
3484+
try {
3485+
const hooksService = provider.getHooksService()
3486+
if (!hooksService) {
3487+
throw new Error("HooksService not available")
3488+
}
3489+
if (!message.hookId) {
3490+
throw new Error("Missing required field: hookId")
3491+
}
3492+
await hooksService.openHookInEditor(message.hookId)
3493+
} catch (error) {
3494+
const errorMessage = error instanceof Error ? error.message : String(error)
3495+
provider.log(`Error opening hook file: ${errorMessage}`)
3496+
await provider.postMessageToWebview({ type: "hooks/error", error: errorMessage })
3497+
}
3498+
break
3499+
}
3500+
34833501
case "hooks/reload": {
34843502
try {
34853503
const hooksService = provider.getHooksService()

src/services/hooks/HooksService.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,44 @@ export class HooksService {
335335
await vscode.commands.executeCommand("revealFileInOS", uri)
336336
}
337337

338+
/**
339+
* Open the hooks file in the editor and scroll to a specific hook
340+
*
341+
* @param hookId - The ID of the hook to scroll to
342+
*/
343+
public async openHookInEditor(hookId: string): Promise<void> {
344+
// Find the hook to determine its source file
345+
const hooks = await this.loadHooks()
346+
const hook = hooks.find((h) => h.id === hookId)
347+
348+
if (!hook) {
349+
throw new Error(`Hook not found: ${hookId}`)
350+
}
351+
352+
// Get the file path based on hook source
353+
const filePath = this.getHooksFilePath(hook.source)
354+
if (!filePath) {
355+
throw new Error(`Cannot find hooks file for source: ${hook.source}`)
356+
}
357+
358+
// Open the document
359+
const uri = vscode.Uri.file(filePath)
360+
const doc = await vscode.workspace.openTextDocument(uri)
361+
const editor = await vscode.window.showTextDocument(doc)
362+
363+
// Search for the hook ID in the file and scroll to it
364+
const text = doc.getText()
365+
const hookIdPattern = `id: ${hookId}`
366+
const position = text.indexOf(hookIdPattern)
367+
368+
if (position !== -1) {
369+
const line = doc.positionAt(position).line
370+
const range = new vscode.Range(line, 0, line, 0)
371+
editor.revealRange(range, vscode.TextEditorRevealType.InCenter)
372+
editor.selection = new vscode.Selection(line, 0, line, 0)
373+
}
374+
}
375+
338376
/**
339377
* Start watching hooks files for external changes
340378
*/

webview-ui/src/components/settings/HooksSettings.tsx

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { DndContext, DragOverlay, closestCenter } from "@dnd-kit/core"
44

55
import { hookEventTypes, type HookEventType, type HookWithMetadata } from "@roo-code/types"
66

7+
import { vscode } from "@/utils/vscode"
78
import { Button, Checkbox } from "@/components/ui"
89

910
import { SectionHeader } from "./SectionHeader"
@@ -133,36 +134,37 @@ export const HooksSettings: React.FC = () => {
133134
}
134135
}, [hookToDelete, deleteHook])
135136

136-
// Handler for copying hook configuration
137+
// Handler for duplicating a hook
137138
const handleCopyHook = useCallback(
138139
(hookId: string) => {
139140
const hook = hooks.find((h) => h.id === hookId)
140141
if (hook) {
141-
// Copy the hook configuration as JSON to clipboard
142-
const hookConfig = {
143-
id: hook.id,
142+
// Generate a 10-character random hash
143+
const randomHash = Math.random().toString(36).substring(2, 12)
144+
const newId = `${hook.id}-${randomHash}`
145+
146+
// Create duplicated hook (starts disabled for safety)
147+
const duplicatedHook: HookWithMetadata = {
148+
id: newId,
144149
name: hook.name,
145-
enabled: hook.enabled,
150+
enabled: false, // Duplicated hooks start disabled
146151
action: hook.action,
147152
matchers: hook.matchers,
153+
eventType: hook.eventType,
154+
source: hook.source,
148155
}
149-
navigator.clipboard
150-
.writeText(JSON.stringify(hookConfig, null, 2))
151-
.then(() => {
152-
console.log(`[HooksSettings] Copied hook ${hookId} to clipboard`)
153-
})
154-
.catch((err) => {
155-
console.error(`[HooksSettings] Failed to copy hook: ${err}`)
156-
})
156+
157+
// Add the duplicated hook under the same event type
158+
createHook(hook.eventType, duplicatedHook, hook.source)
159+
console.log(`[HooksSettings] Duplicated hook ${hookId} as ${newId}`)
157160
}
158161
},
159-
[hooks],
162+
[hooks, createHook],
160163
)
161164

162-
// Handler for opening hook folder (placeholder for Phase 5)
165+
// Handler for opening hook file and scrolling to the hook definition
163166
const handleOpenHookFolder = useCallback((hookId: string) => {
164-
// Placeholder - in real implementation, this would open the folder containing the hook
165-
console.log(`[HooksSettings] Open folder for hook: ${hookId}`)
167+
vscode.postMessage({ type: "hooks/openFile", hookId })
166168
}, [])
167169

168170
return (

webview-ui/src/components/settings/HooksSettings/HookItem.tsx

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import React, { useState } from "react"
2-
import { ChevronDown, ChevronRight, GripVertical, Link2, Copy, Trash2, FolderOpen } from "lucide-react"
2+
import { ChevronDown, ChevronRight, GripVertical, Copy, Trash2, FileText } from "lucide-react"
33
import { useSortable } from "@dnd-kit/sortable"
44
import { CSS } from "@dnd-kit/utilities"
55

66
import type { HookWithMetadata } from "@roo-code/types"
77

88
import { cn } from "@/lib/utils"
9-
import { Button, Checkbox } from "@/components/ui"
9+
import { Button, ToggleSwitch } from "@/components/ui"
1010

1111
import { HookConfigTab } from "./HookConfigTab"
1212
import { HookCommandTab } from "./HookCommandTab"
@@ -97,20 +97,14 @@ export const HookItem: React.FC<HookItemProps> = ({
9797
onClick={(e) => e.stopPropagation()}>
9898
<GripVertical className="w-4 h-4" />
9999
</div>
100-
101100
{/* Expand/collapse indicator */}
102101
{isExpanded ? (
103102
<ChevronDown className="w-4 h-4 text-vscode-foreground" />
104103
) : (
105104
<ChevronRight className="w-4 h-4 text-vscode-foreground" />
106105
)}
107-
108-
{/* Link icon */}
109-
<Link2 className="w-4 h-4 text-vscode-descriptionForeground" />
110-
111-
{/* Hook ID */}
112-
<span className="font-mono text-sm text-vscode-foreground">{hook.id}</span>
113-
106+
{/* Hook name */}
107+
<span className="font-mono text-sm text-vscode-foreground">{hook.name}</span>
114108
{/* Matcher badges */}
115109
{matcherBadges.length > 0 && (
116110
<div className="flex gap-1">
@@ -119,46 +113,58 @@ export const HookItem: React.FC<HookItemProps> = ({
119113
</span>
120114
</div>
121115
)}
122-
123116
{/* Source badge */}
124-
<span className="text-xs text-vscode-descriptionForeground ml-auto">{hook.source}</span>
125-
126-
{/* Action buttons */}
117+
<span className="text-xs bg-vscode-badge-background text-vscode-badge-foreground px-1.5 py-0.5 rounded ml-auto">
118+
{hook.source}
119+
</span>
127120
<div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
128121
<Button
129122
variant="ghost"
130123
size="icon"
131124
className="w-6 h-6 opacity-60 hover:opacity-100"
132-
onClick={onCopy}
133-
title="Copy hook">
134-
<Copy className="w-3.5 h-3.5" />
125+
onClick={onDelete}
126+
title="Delete hook">
127+
<Trash2 className="w-3.5 h-3.5" />
135128
</Button>
136129
<Button
137130
variant="ghost"
138131
size="icon"
139132
className="w-6 h-6 opacity-60 hover:opacity-100"
140-
onClick={onDelete}
141-
title="Delete hook">
142-
<Trash2 className="w-3.5 h-3.5" />
133+
onClick={onCopy}
134+
title="Copy hook">
135+
<Copy className="w-3.5 h-3.5" />
143136
</Button>
144137
<Button
145138
variant="ghost"
146139
size="icon"
147140
className="w-6 h-6 opacity-60 hover:opacity-100"
148141
onClick={onOpenFolder}
149-
title="Open folder">
150-
<FolderOpen className="w-3.5 h-3.5" />
142+
title="Open in editor">
143+
<FileText className="w-3.5 h-3.5" />
151144
</Button>
152145

146+
{/* Status indicator */}
147+
<div
148+
style={{
149+
width: "8px",
150+
height: "8px",
151+
borderRadius: "50%",
152+
background: hook.enabled
153+
? "var(--vscode-testing-iconPassed)"
154+
: "var(--vscode-descriptionForeground)",
155+
marginLeft: "4px",
156+
}}
157+
title={hook.enabled ? "Enabled" : "Disabled"}
158+
/>
159+
153160
{/* Enable toggle */}
154-
<div className="flex items-center ml-1">
155-
<Checkbox
156-
id={`hook-enabled-${hook.id}`}
157-
checked={hook.enabled}
158-
onCheckedChange={onToggleEnabled}
159-
disabled={disabled}
160-
/>
161-
</div>
161+
<ToggleSwitch
162+
checked={hook.enabled}
163+
onChange={onToggleEnabled}
164+
disabled={disabled}
165+
size="medium"
166+
aria-label={`Toggle ${hook.id} hook`}
167+
/>
162168
</div>
163169
</div>
164170

0 commit comments

Comments
 (0)