- 移除 radix-ui、clsx、tailwind-merge、class-variance-authority、react-router-dom - 前后端全面适配 MUI 组件,清理残留 Tailwind 工具类引用 - 优化 Agent Loop 引擎、IPC 处理器、Provider Adapter 等后端模块 - 新增 Agent 网络工具通用设计文档 v2
75 lines
4.6 KiB
TypeScript
75 lines
4.6 KiB
TypeScript
/**
|
|
* CommandPalette — 快速搜索面板 (Cmd+K)
|
|
*/
|
|
|
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
import { Dialog, DialogContent, InputBase, List, ListItemButton, ListItemIcon, ListItemText, Typography, Box, Divider } from '@mui/material';
|
|
import { Search, MessageSquare, Settings, Plus, Trash2 } from 'lucide-react';
|
|
import { useUIStore } from '@renderer/stores/ui-store';
|
|
import { useSessionStore } from '@renderer/stores/session-store';
|
|
import { useAgentStore } from '@renderer/stores/agent-store';
|
|
|
|
interface CommandItem { id: string; icon: typeof Search; label: string; description?: string; action: () => void; }
|
|
|
|
export function CommandPalette(): React.JSX.Element {
|
|
const [open, setOpen] = useState(false);
|
|
const [query, setQuery] = useState('');
|
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
|
|
useEffect(() => {
|
|
const h = (e: KeyboardEvent) => { if (e.key === 'k' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); setOpen((p) => !p); setQuery(''); setSelectedIndex(0); } };
|
|
window.addEventListener('keydown', h); return () => window.removeEventListener('keydown', h);
|
|
}, []);
|
|
|
|
useEffect(() => { if (open) setTimeout(() => inputRef.current?.focus(), 50); }, [open]);
|
|
|
|
const getCommands = useCallback((): CommandItem[] => {
|
|
const cmds: CommandItem[] = [
|
|
{ id: 'new-session', icon: Plus, label: '新建会话', description: '创建一个新的对话会话', action: async () => { if (window.metona?.sessions?.create) { const r = await window.metona.sessions.create() as MetonaSessionInfo; useSessionStore.getState().addSession(r); useSessionStore.getState().setCurrentSession(r.id); useAgentStore.getState().setCurrentSession(r.id); } setOpen(false); } },
|
|
{ id: 'clear', icon: Trash2, label: '清空当前会话', action: () => { useAgentStore.getState().clearMessages(); setOpen(false); } },
|
|
{ id: 'settings', icon: Settings, label: '打开设置', action: () => { useUIStore.getState().openSettings(); setOpen(false); } },
|
|
];
|
|
if (query) {
|
|
const filtered = useSessionStore.getState().sessions.filter((s) => s.title.toLowerCase().includes(query.toLowerCase()));
|
|
for (const s of filtered.slice(0, 5)) cmds.push({ id: `s-${s.id}`, icon: MessageSquare, label: s.title, description: `${s.messageCount} 条消息`, action: () => { useSessionStore.getState().setCurrentSession(s.id); useAgentStore.getState().setCurrentSession(s.id); setOpen(false); } });
|
|
}
|
|
return cmds;
|
|
}, [query]);
|
|
|
|
const commands = getCommands();
|
|
|
|
return (
|
|
<Dialog open={open} onClose={() => setOpen(false)} maxWidth="sm" fullWidth slotProps={{ paper: { sx: { mt: '20vh', maxWidth: 520, borderRadius: 3, overflow: 'hidden' } } }}>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, px: 2, py: 1.5, borderBottom: 1, borderColor: 'divider' }}>
|
|
<Search size={16} style={{ color: '#8b8fa7', flexShrink: 0 }} />
|
|
<InputBase inputRef={inputRef} value={query} onChange={(e) => { setQuery(e.target.value); setSelectedIndex(0); }}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'ArrowDown') { e.preventDefault(); setSelectedIndex((p) => Math.min(p + 1, commands.length - 1)); }
|
|
else if (e.key === 'ArrowUp') { e.preventDefault(); setSelectedIndex((p) => Math.max(p - 1, 0)); }
|
|
else if (e.key === 'Enter') { e.preventDefault(); commands[selectedIndex]?.action(); }
|
|
}}
|
|
placeholder="搜索会话、命令..." sx={{ flex: 1, fontSize: 13 }}
|
|
/>
|
|
</Box>
|
|
<List sx={{ maxHeight: 300, overflowY: 'auto', py: 0.5 }}>
|
|
{commands.length === 0 ? (
|
|
<Typography variant="caption" sx={{ textAlign: 'center', py: 3, display: 'block', color: 'text.secondary' }}>无匹配结果</Typography>
|
|
) : commands.map((cmd, i) => {
|
|
const Icon = cmd.icon;
|
|
return (
|
|
<ListItemButton key={cmd.id} selected={i === selectedIndex} onClick={cmd.action} sx={{ px: 2, py: 1.25 }}>
|
|
<ListItemIcon sx={{ minWidth: 32 }}><Icon size={14} style={{ color: '#818cf8' }} /></ListItemIcon>
|
|
<ListItemText primary={<Typography variant="body2" sx={{ fontSize: 12 }}>{cmd.label}</Typography>} secondary={cmd.description ? <Typography variant="caption">{cmd.description}</Typography> : undefined} />
|
|
</ListItemButton>
|
|
);
|
|
})}
|
|
</List>
|
|
<Divider />
|
|
<Box sx={{ display: 'flex', gap: 2, px: 2, py: 1, fontSize: 10, color: 'text.disabled' }}>
|
|
<span>↑↓ 导航</span><span>↵ 选择</span><span>Esc 关闭</span>
|
|
</Box>
|
|
</Dialog>
|
|
);
|
|
}
|