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
import { useMemo, useState } from 'react';
import { useMemo, useState, useCallback } from 'react';
import { useIntl } from 'react-intl';
import { FileText, Hash, Clock, Sparkles, AlertCircle, Link2, Check } from 'lucide-react';
import { Card } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge';
import { Button } from '@/components/ui/Button';
import { cn } from '@/lib/utils';
import type { DeepWikiSymbol, DeepWikiDoc } from '@/hooks/useDeepWiki';
@@ -276,11 +275,14 @@ export function DocumentViewer({
</h4>
<nav className="space-y-1">
{symbols.map(symbol => (
<a
<div
key={symbol.name}
className="group flex items-center gap-1"
>
<a
href={`#${symbol.anchor.replace('#', '')}`}
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',
'font-mono'
)}
@@ -290,6 +292,24 @@ export function DocumentViewer({
</span>
{symbol.name}
</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>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -177,6 +177,7 @@ export function DeepWikiPage() {
symbols={symbols}
isLoading={docLoading}
error={docError}
filePath={selectedFile ?? undefined}
/>
</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);
}
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)) {
return null;
}
@@ -64,6 +64,8 @@ function toHookCardData(hook: { name: string; description?: string; enabled: boo
trigger: hook.trigger,
matcher: hook.matcher,
command: hook.command || hook.script,
scope: hook.scope,
index: hook.index,
};
}
@@ -200,8 +202,19 @@ export function HookManagerPage() {
};
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 {
await deleteHook(hookName);
await deleteHook({
name: hook.name,
scope: hook.scope,
trigger: hook.trigger,
index: hook.index,
});
} catch (error) {
console.error('Failed to delete hook:', error);
}