feat: 增加DeepWiki页面和侧边栏导航支持,更新Hook管理功能以支持作用域和索引

This commit is contained in:
catlog22
2026-03-05 18:53:24 +08:00
parent fb4f6e718e
commit bc7a556985
9 changed files with 78 additions and 22 deletions

View File

@@ -3,12 +3,11 @@
// ======================================== // ========================================
// Displays DeepWiki documentation content with table of contents // Displays DeepWiki documentation content with table of contents
import { useMemo, useState } from 'react'; import { useMemo, useState, useCallback } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { FileText, Hash, Clock, Sparkles, AlertCircle, Link2, Check } from 'lucide-react'; import { FileText, Hash, Clock, Sparkles, AlertCircle, Link2, Check } from 'lucide-react';
import { Card } from '@/components/ui/Card'; import { Card } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge'; import { Badge } from '@/components/ui/Badge';
import { Button } from '@/components/ui/Button';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import type { DeepWikiSymbol, DeepWikiDoc } from '@/hooks/useDeepWiki'; import type { DeepWikiSymbol, DeepWikiDoc } from '@/hooks/useDeepWiki';
@@ -276,11 +275,14 @@ export function DocumentViewer({
</h4> </h4>
<nav className="space-y-1"> <nav className="space-y-1">
{symbols.map(symbol => ( {symbols.map(symbol => (
<a <div
key={symbol.name} key={symbol.name}
className="group flex items-center gap-1"
>
<a
href={`#${symbol.anchor.replace('#', '')}`} href={`#${symbol.anchor.replace('#', '')}`}
className={cn( className={cn(
'block text-xs py-1.5 px-2 rounded transition-colors', 'flex-1 text-xs py-1.5 px-2 rounded transition-colors',
'text-muted-foreground hover:text-foreground hover:bg-muted/50', 'text-muted-foreground hover:text-foreground hover:bg-muted/50',
'font-mono' 'font-mono'
)} )}
@@ -290,6 +292,24 @@ export function DocumentViewer({
</span> </span>
{symbol.name} {symbol.name}
</a> </a>
<button
onClick={() => copyDeepLink(symbol.name, symbol.anchor)}
className={cn(
'opacity-0 group-hover:opacity-100 p-1 rounded transition-all',
'hover:bg-muted/50',
copiedSymbol === symbol.name
? 'text-green-500'
: 'text-muted-foreground hover:text-foreground'
)}
title={copiedSymbol === symbol.name ? 'Copied!' : 'Copy deep link'}
>
{copiedSymbol === symbol.name ? (
<Check className="w-3 h-3" />
) : (
<Link2 className="w-3 h-3" />
)}
</button>
</div>
))} ))}
</nav> </nav>
</div> </div>

View File

@@ -30,6 +30,8 @@ export interface HookCardData {
matcher?: string; matcher?: string;
command?: string; command?: string;
script?: string; script?: string;
scope?: 'global' | 'project';
index?: number;
} }
export interface HookCardProps { export interface HookCardProps {

View File

@@ -28,6 +28,7 @@ import {
FileSearch, FileSearch,
ScrollText, ScrollText,
Clock, Clock,
BookOpen,
} from 'lucide-react'; } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
@@ -92,6 +93,7 @@ const navGroupDefinitions: NavGroupDef[] = [
icon: Brain, icon: Brain,
items: [ items: [
{ path: '/memory', labelKey: 'navigation.main.memory', icon: Brain }, { path: '/memory', labelKey: 'navigation.main.memory', icon: Brain },
{ path: '/deepwiki', labelKey: 'navigation.main.deepwiki', icon: BookOpen },
{ path: '/skills', labelKey: 'navigation.main.skills', icon: Sparkles }, { path: '/skills', labelKey: 'navigation.main.skills', icon: Sparkles },
{ path: '/commands', labelKey: 'navigation.main.commands', icon: Terminal }, { path: '/commands', labelKey: 'navigation.main.commands', icon: Terminal },
{ path: '/settings/rules', labelKey: 'navigation.main.rules', icon: Shield }, { path: '/settings/rules', labelKey: 'navigation.main.rules', icon: Shield },

View File

@@ -514,23 +514,33 @@ export function useToggleHook() {
export function useDeleteHook() { export function useDeleteHook() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const projectPath = useWorkflowStore(selectProjectPath);
const mutation = useMutation({ const mutation = useMutation({
mutationFn: (hookName: string) => deleteHook(hookName), mutationFn: (hook: { name: string; scope?: 'global' | 'project'; trigger: string; index?: number }) => {
onMutate: async (hookName) => { const scope = hook.scope || 'project';
const hookIndex = hook.index ?? 0;
return deleteHook({
projectPath: projectPath || undefined,
scope,
event: hook.trigger,
hookIndex,
});
},
onMutate: async (hook) => {
await queryClient.cancelQueries({ queryKey: hooksKeys.all }); await queryClient.cancelQueries({ queryKey: hooksKeys.all });
const previousHooks = queryClient.getQueryData<HooksResponse>(hooksKeys.lists()); const previousHooks = queryClient.getQueryData<HooksResponse>(hooksKeys.lists());
queryClient.setQueryData<HooksResponse>(hooksKeys.lists(), (old) => { queryClient.setQueryData<HooksResponse>(hooksKeys.lists(), (old) => {
if (!old) return old; if (!old) return old;
return { return {
hooks: old.hooks.filter((h) => h.name !== hookName), hooks: old.hooks.filter((h) => h.name !== hook.name),
}; };
}); });
return { previousHooks }; return { previousHooks };
}, },
onError: (_error, _hookName, context) => { onError: (_error, _hook, context) => {
if (context?.previousHooks) { if (context?.previousHooks) {
queryClient.setQueryData(hooksKeys.lists(), context.previousHooks); queryClient.setQueryData(hooksKeys.lists(), context.previousHooks);
} }

View File

@@ -4407,9 +4407,15 @@ export async function updateHookConfig(
/** /**
* Delete a hook * Delete a hook
*/ */
export async function deleteHook(hookName: string): Promise<void> { export async function deleteHook(params: {
return fetchApi<void>(`/api/hooks/delete/${encodeURIComponent(hookName)}`, { projectPath?: string;
scope: 'global' | 'project';
event: string;
hookIndex: number;
}): Promise<{ success: boolean }> {
return fetchApi<{ success: boolean }>('/api/hooks', {
method: 'DELETE', method: 'DELETE',
body: JSON.stringify(params),
}); });
} }

View File

@@ -23,6 +23,7 @@
"skills": "Skills", "skills": "Skills",
"commands": "Commands", "commands": "Commands",
"memory": "Memory", "memory": "Memory",
"deepwiki": "DeepWiki",
"prompts": "Prompt History", "prompts": "Prompt History",
"settings": "Settings", "settings": "Settings",
"mcp": "MCP Servers", "mcp": "MCP Servers",

View File

@@ -23,6 +23,7 @@
"skills": "技能", "skills": "技能",
"commands": "命令", "commands": "命令",
"memory": "记忆", "memory": "记忆",
"deepwiki": "DeepWiki",
"prompts": "提示历史", "prompts": "提示历史",
"settings": "设置", "settings": "设置",
"mcp": "MCP 服务器", "mcp": "MCP 服务器",

View File

@@ -177,6 +177,7 @@ export function DeepWikiPage() {
symbols={symbols} symbols={symbols}
isLoading={docLoading} isLoading={docLoading}
error={docError} error={docError}
filePath={selectedFile ?? undefined}
/> />
</div> </div>
)} )}

View File

@@ -53,7 +53,7 @@ function isHookTriggerType(value: string): value is HookTriggerType {
return ['SessionStart', 'UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SubagentStart', 'SubagentStop', 'PreCompact', 'SessionEnd', 'PostToolUseFailure', 'PermissionRequest'].includes(value); return ['SessionStart', 'UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SubagentStart', 'SubagentStop', 'PreCompact', 'SessionEnd', 'PostToolUseFailure', 'PermissionRequest'].includes(value);
} }
function toHookCardData(hook: { name: string; description?: string; enabled: boolean; trigger: string; matcher?: string; command?: string; script?: string }): HookCardData | null { function toHookCardData(hook: { name: string; description?: string; enabled: boolean; trigger: string; matcher?: string; command?: string; script?: string; scope?: 'global' | 'project'; index?: number }): HookCardData | null {
if (!isHookTriggerType(hook.trigger)) { if (!isHookTriggerType(hook.trigger)) {
return null; return null;
} }
@@ -64,6 +64,8 @@ function toHookCardData(hook: { name: string; description?: string; enabled: boo
trigger: hook.trigger, trigger: hook.trigger,
matcher: hook.matcher, matcher: hook.matcher,
command: hook.command || hook.script, command: hook.command || hook.script,
scope: hook.scope,
index: hook.index,
}; };
} }
@@ -200,8 +202,19 @@ export function HookManagerPage() {
}; };
const handleDeleteClick = async (hookName: string) => { const handleDeleteClick = async (hookName: string) => {
// Find the hook in filteredHooks to get scope and index
const hook = filteredHooks.find(h => h.name === hookName);
if (!hook) {
console.error('Hook not found:', hookName);
return;
}
try { try {
await deleteHook(hookName); await deleteHook({
name: hook.name,
scope: hook.scope,
trigger: hook.trigger,
index: hook.index,
});
} catch (error) { } catch (error) {
console.error('Failed to delete hook:', error); console.error('Failed to delete hook:', error);
} }