feat: 添加系统命令支持 (:clear)

- 新增系统命令模块 (core/system-commands)
  - 支持 :clear/:cls/:c 清空对话历史
  - 命令注册表支持别名
  - 可扩展的命令执行器

- Server 端支持
  - 新增 /api/system-commands API
  - WebSocket 处理系统命令消息
  - 会话清空 API 端点

- UI 端支持
  - 新增 SystemCommandMenu 组件
  - 输入 : 时显示命令建议菜单
  - 键盘导航和选择
  - 底部提示添加 : 快捷键
This commit is contained in:
2025-12-17 19:25:42 +08:00
parent 4fc6b61134
commit e0444a966f
21 changed files with 1109 additions and 9 deletions
+90 -5
View File
@@ -3,6 +3,7 @@
*
* 支持响应式:responsive=true 时适配移动端键盘和触摸操作
* 支持斜杠命令:输入 / 时显示命令菜单
* 支持系统命令:输入 : 时显示系统命令菜单
* 支持文件提及:输入 @ 时显示文件搜索菜单
*/
@@ -11,10 +12,12 @@ import { Square, Sparkles } from 'lucide-react';
import { motion } from 'framer-motion';
import clsx from 'clsx';
import { CommandMenu, type CommandMenuItem } from './CommandMenu.js';
import { SystemCommandMenu, type SystemCommandMenuItem } from './SystemCommandMenu.js';
import { FileMenu, type FileMenuItem } from './FileMenu.js';
import { FileMentionTag } from './FileMentionTag.js';
import { AgentModeSelector, type AgentModeType } from './AgentModeSelector.js';
import { useCommands } from '../hooks/useCommands.js';
import { useSystemCommands } from '../hooks/useSystemCommands.js';
import { useFileMention } from '../hooks/useFileMention.js';
interface ChatInputProps {
@@ -54,6 +57,8 @@ export function ChatInput({
const [input, setInput] = useState('');
const [showCommandMenu, setShowCommandMenu] = useState(false);
const [selectedCommandIndex, setSelectedCommandIndex] = useState(0);
const [showSystemCommandMenu, setShowSystemCommandMenu] = useState(false);
const [selectedSystemCommandIndex, setSelectedSystemCommandIndex] = useState(0);
const textareaRef = useRef<HTMLTextAreaElement>(null);
// 命令系统
@@ -63,6 +68,13 @@ export function ChatInput({
filterCommands,
} = useCommands({ autoLoad: enableCommands });
// 系统命令系统
const {
filteredCommands: filteredSystemCommands,
isLoading: systemCommandsLoading,
filterCommands: filterSystemCommands,
} = useSystemCommands({ autoLoad: enableCommands });
// 文件提及系统
const {
isOpen: showFileMenu,
@@ -130,17 +142,41 @@ export function ChatInput({
[enableCommands, filterCommands]
);
// 检测系统命令输入
const checkSystemCommandTrigger = useCallback(
(value: string) => {
if (!enableCommands) return;
// 检测是否在输入系统命令
// 条件:以 : 开头,且 : 后面没有空格(还在输入命令名)
const colonMatch = value.match(/^:(\S*)$/);
if (colonMatch) {
const query = colonMatch[1]; // : 后面的内容
setShowSystemCommandMenu(true);
setSelectedSystemCommandIndex(0);
filterSystemCommands(query);
} else {
setShowSystemCommandMenu(false);
}
},
[enableCommands, filterSystemCommands]
);
// 处理输入变化
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const value = e.target.value;
const cursorPos = e.target.selectionStart;
setInput(value);
// 检查命令触发
// 检查斜杠命令触发
checkCommandTrigger(value);
// 检查系统命令触发
checkSystemCommandTrigger(value);
// 检查文件提及触发(只在非命令输入模式下)
if (enableFileMention && !value.startsWith('/')) {
if (enableFileMention && !value.startsWith('/') && !value.startsWith(':')) {
checkFileTrigger(value, cursorPos);
} else {
closeFileMenu();
@@ -189,12 +225,28 @@ export function ChatInput({
setShowCommandMenu(false);
}, []);
// 选择系统命令
const handleSystemCommandSelect = useCallback((command: SystemCommandMenuItem) => {
// 直接发送系统命令
setInput(`:${command.name}`);
setShowSystemCommandMenu(false);
// 聚焦输入框
textareaRef.current?.focus();
}, []);
// 关闭系统命令菜单
const handleSystemCommandMenuClose = useCallback(() => {
setShowSystemCommandMenu(false);
}, []);
const handleSubmit = () => {
const trimmed = input.trim();
if (!trimmed || isLoading || disabled) return;
// 关闭菜单
// 关闭所有菜单
setShowCommandMenu(false);
setShowSystemCommandMenu(false);
closeFileMenu();
onSend(trimmed);
@@ -222,7 +274,7 @@ export function ChatInput({
}
}
// 命令菜单处理
// 斜杠命令菜单处理
if (showCommandMenu && filteredCommands.length > 0) {
if (['ArrowUp', 'ArrowDown', 'Tab', 'Escape'].includes(e.key)) {
// 这些键由 CommandMenu 处理,阻止默认行为
@@ -238,6 +290,22 @@ export function ChatInput({
}
}
// 系统命令菜单处理
if (showSystemCommandMenu && filteredSystemCommands.length > 0) {
if (['ArrowUp', 'ArrowDown', 'Tab', 'Escape'].includes(e.key)) {
// 这些键由 SystemCommandMenu 处理,阻止默认行为
return;
}
if (e.key === 'Enter' && !e.shiftKey) {
// Enter 选择系统命令
e.preventDefault();
if (filteredSystemCommands[selectedSystemCommandIndex]) {
handleSystemCommandSelect(filteredSystemCommands[selectedSystemCommandIndex]);
}
return;
}
}
// Enter 发送,Shift+Enter 换行
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
@@ -252,7 +320,7 @@ export function ChatInput({
responsive ? 'p-3 md:p-4 safe-area-pb' : 'p-4'
)}
>
{/* Command Menu */}
{/* Command Menu (斜杠命令) */}
{enableCommands && (
<CommandMenu
commands={filteredCommands}
@@ -265,6 +333,19 @@ export function ChatInput({
/>
)}
{/* System Command Menu (系统命令) */}
{enableCommands && (
<SystemCommandMenu
commands={filteredSystemCommands}
isOpen={showSystemCommandMenu}
selectedIndex={selectedSystemCommandIndex}
onSelect={handleSystemCommandSelect}
onClose={handleSystemCommandMenuClose}
onSelectedIndexChange={setSelectedSystemCommandIndex}
isLoading={systemCommandsLoading}
/>
)}
{/* File Menu */}
{enableFileMention && (
<FileMenu
@@ -382,6 +463,10 @@ export function ChatInput({
<kbd className="px-1.5 py-0.5 rounded bg-surface-subtle text-fg-muted font-mono text-[10px]">/</kbd>
<span>commands</span>
</span>
<span className="flex items-center gap-1">
<kbd className="px-1.5 py-0.5 rounded bg-surface-subtle text-fg-muted font-mono text-[10px]">:</kbd>
<span>system</span>
</span>
<span className="flex items-center gap-1">
<kbd className="px-1.5 py-0.5 rounded bg-surface-subtle text-fg-muted font-mono text-[10px]">@</kbd>
<span>files</span>