From 5a482f78ff9aeebe4744bf2ef30738a53e7a7016 Mon Sep 17 00:00:00 2001 From: kurihada Date: Fri, 12 Dec 2025 20:15:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E9=9B=86=E6=88=90=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=E7=AE=A1=E7=90=86=E9=9D=A2=E6=9D=BF=E5=88=B0=20web=20?= =?UTF-8?q?=E5=92=8C=20desktop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 CommandPanel 组件用于命令列表/搜索/CRUD - 新增 CommandEditor 组件用于命令编辑/创建 - web/desktop 工具栏添加 Terminal 图标按钮 - 点击按钮打开命令管理面板 --- packages/desktop/src/App.tsx | 6 + packages/desktop/src/pages/Chat.tsx | 19 +- packages/ui/src/components/CommandEditor.tsx | 396 ++++++++++++++++++ packages/ui/src/components/CommandPanel.tsx | 403 +++++++++++++++++++ packages/ui/src/index.ts | 2 + packages/web/src/App.tsx | 6 + packages/web/src/pages/Chat.tsx | 19 +- 7 files changed, 847 insertions(+), 4 deletions(-) create mode 100644 packages/ui/src/components/CommandEditor.tsx create mode 100644 packages/ui/src/components/CommandPanel.tsx diff --git a/packages/desktop/src/App.tsx b/packages/desktop/src/App.tsx index 332a5b1..2762dab 100644 --- a/packages/desktop/src/App.tsx +++ b/packages/desktop/src/App.tsx @@ -7,6 +7,7 @@ import { Sidebar, FileBrowser, ConfigPanel, + CommandPanel, Toaster, listSessions, createSession, @@ -19,6 +20,7 @@ export function App() { const [isInitializing, setIsInitializing] = useState(true); const [showFileBrowser, setShowFileBrowser] = useState(false); const [showConfig, setShowConfig] = useState(false); + const [showCommands, setShowCommands] = useState(false); const [sessionTitleUpdate, setSessionTitleUpdate] = useState<{ sessionId: string; name: string } | null>(null); // 初始化:加载或创建会话 @@ -89,6 +91,7 @@ export function App() { showFileBrowser={showFileBrowser} onToggleFileBrowser={() => setShowFileBrowser(!showFileBrowser)} onOpenConfig={() => setShowConfig(true)} + onOpenCommands={() => setShowCommands(true)} /> ) : (
@@ -112,6 +115,9 @@ export function App() { {/* 配置面板 */} {showConfig && setShowConfig(false)} />} + {/* 命令面板 */} + {showCommands && setShowCommands(false)} />} + {/* Toast 通知 */}
diff --git a/packages/desktop/src/pages/Chat.tsx b/packages/desktop/src/pages/Chat.tsx index 16f7518..638efc2 100644 --- a/packages/desktop/src/pages/Chat.tsx +++ b/packages/desktop/src/pages/Chat.tsx @@ -3,7 +3,7 @@ */ import { useEffect, useRef } from 'react'; -import { WifiOff, MessageSquare, Settings, FolderOpen } from 'lucide-react'; +import { WifiOff, MessageSquare, Settings, FolderOpen, Terminal } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import { useChat, @@ -20,6 +20,7 @@ interface ChatPageProps { showFileBrowser?: boolean; onToggleFileBrowser?: () => void; onOpenConfig?: () => void; + onOpenCommands?: () => void; } export function ChatPage({ @@ -28,6 +29,7 @@ export function ChatPage({ showFileBrowser, onToggleFileBrowser, onOpenConfig, + onOpenCommands, }: ChatPageProps) { const { messages, @@ -121,8 +123,21 @@ export function ChatPage({ {/* 工具栏按钮 */} - {(onOpenConfig || onToggleFileBrowser) && ( + {(onOpenConfig || onToggleFileBrowser || onOpenCommands) && (
+ {/* 命令按钮 */} + {onOpenCommands && ( + + + + )} + {/* 配置按钮 */} {onOpenConfig && ( void; + /** 保存成功回调 */ + onSaved?: () => void; + /** 是否启用响应式布局 */ + responsive?: boolean; +} + +export function CommandEditor({ + commandName, + onClose, + onSaved, + responsive = false, +}: CommandEditorProps) { + const isEditMode = !!commandName; + + // 表单状态 + const [name, setName] = useState(''); + const [description, setDescription] = useState(''); + const [template, setTemplate] = useState(''); + const [agent, setAgent] = useState(''); + const [model, setModel] = useState(''); + const [subtask, setSubtask] = useState(false); + const [scope, setScope] = useState<'user' | 'project'>('user'); + + // UI 状态 + const [loading, setLoading] = useState(false); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + const [originalData, setOriginalData] = useState(null); + + // 编辑模式:加载现有命令 + useEffect(() => { + if (isEditMode && commandName) { + setLoading(true); + setError(null); + + getCommandContent(commandName) + .then((result) => { + if (result.success && result.data) { + const data = result.data; + setOriginalData(data); + setName(data.name); + setDescription(data.description || ''); + setTemplate(data.template); + setAgent(data.agent || ''); + setModel(data.model || ''); + setSubtask(data.subtask || false); + } else { + setError(result.error || 'Failed to load command'); + } + }) + .catch((err) => { + setError(err instanceof Error ? err.message : 'Failed to load command'); + }) + .finally(() => { + setLoading(false); + }); + } + }, [isEditMode, commandName]); + + // 保存命令 + const handleSave = async () => { + // 验证 + if (!name.trim()) { + toast.error('Command name is required'); + return; + } + if (!template.trim()) { + toast.error('Template is required'); + return; + } + + setSaving(true); + setError(null); + + try { + if (isEditMode) { + // 更新模式 + const input: UpdateCommandInput = { + description: description || undefined, + template, + agent: agent || undefined, + model: model || undefined, + subtask, + }; + + const result = await updateCommand(commandName!, input); + if (result.success) { + toast.success('Command updated'); + onSaved?.(); + onClose(); + } else { + setError(result.error || 'Failed to update command'); + } + } else { + // 创建模式 + const input: CreateCommandInput = { + name: name.trim(), + description: description || undefined, + template, + agent: agent || undefined, + model: model || undefined, + subtask, + scope, + }; + + const result = await createCommand(input); + if (result.success) { + toast.success('Command created'); + onSaved?.(); + onClose(); + } else { + setError(result.error || 'Failed to create command'); + } + } + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to save command'); + } finally { + setSaving(false); + } + }; + + // 是否为内置命令(不可编辑) + const isBuiltin = originalData?.source === 'builtin'; + + return ( + + + e.stopPropagation()} + className={cn( + 'bg-gray-800 max-h-[90vh] overflow-auto', + responsive + ? 'w-full md:w-full md:max-w-2xl md:mx-4 rounded-t-2xl md:rounded-lg' + : 'rounded-lg w-full max-w-2xl mx-4' + )} + > + {/* Header */} +
+ {responsive && ( +
+ )} +

+ {isEditMode ? `Edit Command: /${commandName}` : 'Create Command'} +

+ +
+ + {/* Content */} + {loading ? ( +
+
+
+ ) : ( +
+ {/* Error */} + {error && ( + + + {error} + + )} + + {/* Builtin Warning */} + {isBuiltin && ( +
+ + Builtin commands cannot be modified +
+ )} + + {/* Name */} +
+ + setName(e.target.value)} + placeholder="deploy/staging" + disabled={isEditMode || isBuiltin} + className="font-mono" + /> +

+ Command name. Use / for nested commands (e.g., deploy/staging) +

+
+ + {/* Description */} +
+ + setDescription(e.target.value)} + placeholder="Deploy to staging environment" + disabled={isBuiltin} + /> +
+ + {/* Template */} +
+ +