Files
metona-ai-desktop/src/components/CommandPalette.tsx
T
thzxx f4532a2bb2 refactor: 移除多余依赖,统一 MUI 为唯一 UI 库
- 移除 radix-ui、clsx、tailwind-merge、class-variance-authority、react-router-dom

- 前后端全面适配 MUI 组件,清理残留 Tailwind 工具类引用

- 优化 Agent Loop 引擎、IPC 处理器、Provider Adapter 等后端模块

- 新增 Agent 网络工具通用设计文档 v2
2026-07-05 19:15:48 +08:00

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>
);
}