diff --git a/ccw/frontend/src/components/ui/CommandCombobox.tsx b/ccw/frontend/src/components/ui/CommandCombobox.tsx new file mode 100644 index 00000000..bd947685 --- /dev/null +++ b/ccw/frontend/src/components/ui/CommandCombobox.tsx @@ -0,0 +1,180 @@ +// ======================================== +// Command Combobox Component +// ======================================== +// Searchable dropdown for selecting slash commands + +import { useState, useRef, useEffect, useCallback, useMemo } from 'react'; +import { ChevronDown, Search } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { useCommands } from '@/hooks/useCommands'; +import type { Command } from '@/lib/api'; + +interface CommandComboboxProps { + value: string; + onChange: (value: string) => void; + placeholder?: string; + className?: string; +} + +export function CommandCombobox({ value, onChange, placeholder, className }: CommandComboboxProps) { + const [open, setOpen] = useState(false); + const [search, setSearch] = useState(''); + const containerRef = useRef(null); + const inputRef = useRef(null); + + const { commands, isLoading } = useCommands({ + filter: { showDisabled: false }, + }); + + // Group commands by group field + const groupedFiltered = useMemo(() => { + const filtered = search + ? commands.filter( + (c) => + c.name.toLowerCase().includes(search.toLowerCase()) || + c.description.toLowerCase().includes(search.toLowerCase()) || + c.aliases?.some((a) => a.toLowerCase().includes(search.toLowerCase())) + ) + : commands; + + const groups: Record = {}; + for (const cmd of filtered) { + const group = cmd.group || 'other'; + if (!groups[group]) groups[group] = []; + groups[group].push(cmd); + } + return groups; + }, [commands, search]); + + const totalFiltered = useMemo( + () => Object.values(groupedFiltered).reduce((sum, cmds) => sum + cmds.length, 0), + [groupedFiltered] + ); + + // Close on outside click + useEffect(() => { + function handleClickOutside(e: MouseEvent) { + if (containerRef.current && !containerRef.current.contains(e.target as Node)) { + setOpen(false); + } + } + if (open) { + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + } + }, [open]); + + const handleSelect = useCallback( + (name: string) => { + onChange(name); + setOpen(false); + setSearch(''); + }, + [onChange] + ); + + const handleInputChange = useCallback((e: React.ChangeEvent) => { + setSearch(e.target.value); + if (!open) setOpen(true); + }, [open]); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === 'Escape') { + setOpen(false); + setSearch(''); + } + }, + [] + ); + + // Display label for current value + const selectedCommand = commands.find((c) => c.name === value); + const displayValue = value + ? selectedCommand + ? `/${selectedCommand.name}` + : `/${value}` + : ''; + + return ( +
+ {/* Trigger button */} + + + {/* Dropdown */} + {open && ( +
+ {/* Search input */} +
+ + +
+ + {/* Command list */} +
+ {isLoading ? ( +
Loading...
+ ) : totalFiltered === 0 ? ( +
+ No commands found +
+ ) : ( + Object.entries(groupedFiltered) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([group, cmds]) => ( +
+
+ {group} +
+ {cmds.map((cmd) => ( + + ))} +
+ )) + )} +
+
+ )} +
+ ); +} diff --git a/ccw/frontend/src/locales/en/common.json b/ccw/frontend/src/locales/en/common.json index 245c7802..06e49475 100644 --- a/ccw/frontend/src/locales/en/common.json +++ b/ccw/frontend/src/locales/en/common.json @@ -267,71 +267,6 @@ "expandAria": "Expand sidebar" } }, - "liteTasks": { - "title": "Lite Tasks", - "type": { - "plan": "Lite Plan", - "fix": "Lite Fix", - "multiCli": "Multi-CLI Plan" - }, - "quickCards": { - "tasks": "Tasks", - "context": "Context" - }, - "multiCli": { - "discussion": "Discussion", - "discussionRounds": "Discussion Rounds", - "discussionDescription": "Multi-CLI collaborative planning with iterative analysis and cross-verification", - "summary": "Summary", - "goal": "Goal", - "solution": "Solution", - "implementation": "Implementation", - "feasibility": "Feasibility", - "risk": "Risk", - "planSummary": "Plan Summary" - }, - "createdAt": "Created", - "rounds": "rounds", - "tasksCount": "tasks", - "untitled": "Untitled Task", - "discussionTopic": "Discussion Topic", - "contextPanel": { - "loading": "Loading context data...", - "error": "Failed to load context", - "empty": "No context data available", - "explorations": "Explorations", - "explorationsCount": "{count} explorations", - "diagnoses": "Diagnoses", - "diagnosesCount": "{count} diagnoses", - "contextPackage": "Context Package", - "focusPaths": "Focus Paths", - "summary": "Summary", - "taskDescription": "Task Description", - "complexity": "Complexity" - }, - "status": { - "completed": "Completed", - "inProgress": "In Progress", - "blocked": "Blocked", - "pending": "Pending" - }, - "subtitle": "{count} sessions", - "empty": { - "title": "No {type} sessions", - "message": "No sessions found for this type" - }, - "noResults": { - "title": "No results", - "message": "No sessions match your search" - }, - "searchPlaceholder": "Search sessions...", - "sortBy": "Sort by", - "sort": { - "date": "Date", - "name": "Name", - "tasks": "Tasks" - } - }, "askQuestion": { "defaultTitle": "Questions", "description": "Please answer the following questions", diff --git a/ccw/frontend/src/locales/en/lite-tasks.json b/ccw/frontend/src/locales/en/lite-tasks.json index 7afa33c9..9e207a2e 100644 --- a/ccw/frontend/src/locales/en/lite-tasks.json +++ b/ccw/frontend/src/locales/en/lite-tasks.json @@ -4,9 +4,29 @@ "type": { "plan": "Lite Plan", "fix": "Lite Fix", - "multiCli": "Multi-CLI" + "multiCli": "Multi-CLI Plan" }, + "quickCards": { + "tasks": "Tasks", + "context": "Context" + }, + "multiCli": { + "discussion": "Discussion", + "discussionRounds": "Discussion Rounds", + "discussionDescription": "Multi-CLI collaborative planning with iterative analysis and cross-verification", + "summary": "Summary", + "goal": "Goal", + "solution": "Solution", + "implementation": "Implementation", + "feasibility": "Feasibility", + "risk": "Risk", + "planSummary": "Plan Summary" + }, + "createdAt": "Created", "rounds": "rounds", + "tasksCount": "tasks", + "untitled": "Untitled Task", + "discussionTopic": "Discussion Topic", "empty": { "title": "No {type} sessions", "message": "Create a new session to get started." @@ -27,13 +47,10 @@ "focusPaths": "Focus Paths", "acceptanceCriteria": "Acceptance Criteria", "dependsOn": "Depends On", - "tasksCount": "tasks", "emptyDetail": { "title": "No tasks in this session", "message": "This session does not contain any tasks yet." }, - "untitled": "Untitled Task", - "discussionTopic": "Discussion Topic", "notFound": { "title": "Lite Task Not Found", "message": "The requested lite task session could not be found." @@ -43,29 +60,23 @@ "context": "Context" }, "contextPanel": { - "loading": "Loading context...", + "loading": "Loading context data...", "error": "Failed to load context", - "empty": "No context data available for this session.", + "empty": "No context data available", "explorations": "Explorations", - "explorationsCount": "{count} angles", - "contextPackage": "Context Package", + "explorationsCount": "{count} explorations", "diagnoses": "Diagnoses", - "diagnosesCount": "{count} items", + "diagnosesCount": "{count} diagnoses", + "contextPackage": "Context Package", "focusPaths": "Focus Paths", "summary": "Summary", - "complexity": "Complexity", - "taskDescription": "Task Description" - }, - "quickCards": { - "tasks": "Tasks", - "explorations": "Explorations", - "context": "Context", - "diagnoses": "Diagnoses" + "taskDescription": "Task Description", + "complexity": "Complexity" }, "status": { - "completed": "completed", - "inProgress": "in progress", - "blocked": "blocked", - "pending": "pending" + "completed": "Completed", + "inProgress": "In Progress", + "blocked": "Blocked", + "pending": "Pending" } } diff --git a/ccw/frontend/src/locales/zh/common.json b/ccw/frontend/src/locales/zh/common.json index 7ac11e92..b5ab715d 100644 --- a/ccw/frontend/src/locales/zh/common.json +++ b/ccw/frontend/src/locales/zh/common.json @@ -261,71 +261,6 @@ "expandAria": "展开侧边栏" } }, - "liteTasks": { - "title": "轻量任务", - "type": { - "plan": "轻量规划", - "fix": "轻量修复", - "multiCli": "多CLI规划" - }, - "quickCards": { - "tasks": "任务", - "context": "上下文" - }, - "multiCli": { - "discussion": "讨论", - "discussionRounds": "讨论轮次", - "discussionDescription": "多CLI协作规划,迭代分析与交叉验证", - "summary": "摘要", - "goal": "目标", - "solution": "解决方案", - "implementation": "实现方式", - "feasibility": "可行性", - "risk": "风险", - "planSummary": "规划摘要" - }, - "createdAt": "创建时间", - "rounds": "轮", - "tasksCount": "个任务", - "untitled": "未命名任务", - "discussionTopic": "讨论主题", - "contextPanel": { - "loading": "加载上下文数据中...", - "error": "加载上下文失败", - "empty": "无可用上下文数据", - "explorations": "探索", - "explorationsCount": "{count} 个探索", - "diagnoses": "诊断", - "diagnosesCount": "{count} 个诊断", - "contextPackage": "上下文包", - "focusPaths": "关注路径", - "summary": "摘要", - "taskDescription": "任务描述", - "complexity": "复杂度" - }, - "status": { - "completed": "已完成", - "inProgress": "进行中", - "blocked": "已阻塞", - "pending": "待处理" - }, - "subtitle": "{count} 个会话", - "empty": { - "title": "无 {type} 会话", - "message": "未找到该类型的会话" - }, - "noResults": { - "title": "无结果", - "message": "没有符合搜索条件的会话" - }, - "searchPlaceholder": "搜索会话...", - "sortBy": "排序方式", - "sort": { - "date": "日期", - "name": "名称", - "tasks": "任务" - } - }, "askQuestion": { "defaultTitle": "问题", "description": "请回答以下问题", diff --git a/ccw/frontend/src/locales/zh/lite-tasks.json b/ccw/frontend/src/locales/zh/lite-tasks.json index 8eea1ab2..71d4e70f 100644 --- a/ccw/frontend/src/locales/zh/lite-tasks.json +++ b/ccw/frontend/src/locales/zh/lite-tasks.json @@ -4,9 +4,29 @@ "type": { "plan": "轻量规划", "fix": "轻量修复", - "multiCli": "多 CLI 规划" + "multiCli": "多CLI规划" }, + "quickCards": { + "tasks": "任务", + "context": "上下文" + }, + "multiCli": { + "discussion": "讨论", + "discussionRounds": "讨论轮次", + "discussionDescription": "多CLI协作规划,迭代分析与交叉验证", + "summary": "摘要", + "goal": "目标", + "solution": "解决方案", + "implementation": "实现方式", + "feasibility": "可行性", + "risk": "风险", + "planSummary": "规划摘要" + }, + "createdAt": "创建时间", "rounds": "轮", + "tasksCount": "个任务", + "untitled": "未命名任务", + "discussionTopic": "讨论主题", "empty": { "title": "没有 {type} 会话", "message": "创建新会话以开始使用。" @@ -27,13 +47,10 @@ "focusPaths": "关注路径", "acceptanceCriteria": "验收标准", "dependsOn": "依赖于", - "tasksCount": "个任务", "emptyDetail": { "title": "此会话中没有任务", "message": "此会话尚不包含任何任务。" }, - "untitled": "无标题任务", - "discussionTopic": "讨论主题", "notFound": { "title": "未找到轻量任务", "message": "无法找到请求的轻量任务会话。" @@ -43,29 +60,23 @@ "context": "上下文" }, "contextPanel": { - "loading": "加载上下文中...", + "loading": "加载上下文数据中...", "error": "加载上下文失败", - "empty": "此会话暂无上下文数据。", - "explorations": "探索结果", - "explorationsCount": "{count} 个角度", - "contextPackage": "上下文包", + "empty": "无可用上下文数据", + "explorations": "探索", + "explorationsCount": "{count} 个探索", "diagnoses": "诊断", - "diagnosesCount": "{count} 个条目", + "diagnosesCount": "{count} 个诊断", + "contextPackage": "上下文包", "focusPaths": "关注路径", "summary": "摘要", - "complexity": "复杂度", - "taskDescription": "任务描述" - }, - "quickCards": { - "tasks": "任务", - "explorations": "探索", - "context": "上下文", - "diagnoses": "诊断" + "taskDescription": "任务描述", + "complexity": "复杂度" }, "status": { "completed": "已完成", "inProgress": "进行中", - "blocked": "已阻止", + "blocked": "已阻塞", "pending": "待处理" } } diff --git a/ccw/frontend/src/pages/LiteTaskDetailPage.tsx b/ccw/frontend/src/pages/LiteTaskDetailPage.tsx index 07dcce2a..3b012bfc 100644 --- a/ccw/frontend/src/pages/LiteTaskDetailPage.tsx +++ b/ccw/frontend/src/pages/LiteTaskDetailPage.tsx @@ -53,7 +53,7 @@ import type { LiteTask, LiteTaskSession } from '@/lib/api'; type SessionType = 'lite-plan' | 'lite-fix' | 'multi-cli-plan'; type LitePlanTab = 'tasks' | 'plan' | 'diagnoses' | 'context' | 'summary'; -type MultiCliTab = 'tasks' | 'discussion' | 'context' | 'summary'; +type MultiCliTab = 'tasks' | 'discussion' | 'context'; type TaskTabValue = 'task' | 'context'; diff --git a/ccw/frontend/src/pages/LiteTasksPage.tsx b/ccw/frontend/src/pages/LiteTasksPage.tsx index ef324b0d..9e18a860 100644 --- a/ccw/frontend/src/pages/LiteTasksPage.tsx +++ b/ccw/frontend/src/pages/LiteTasksPage.tsx @@ -33,7 +33,6 @@ import { CheckCircle2, Clock, AlertCircle, - Target, FileCode, } from 'lucide-react'; import { useLiteTasks } from '@/hooks/useLiteTasks'; @@ -138,55 +137,73 @@ function ExpandedSessionPanel({ {/* Tasks Tab */} {activeTab === 'tasks' && (
- {tasks.map((task, index) => ( - { - e.stopPropagation(); - onTaskClick(task); - }} - > - -
-
- - {task.task_id || `#${index + 1}`} - -

- {task.title || formatMessage({ id: 'liteTasks.untitled' })} -

-
- {/* Right: Meta info */} -
- {/* Dependencies - show task IDs */} - {task.context?.depends_on && task.context.depends_on.length > 0 && ( -
- - {task.context.depends_on.map((depId, idx) => ( - - {depId} - - ))} -
- )} - {/* Target Files Count */} - {task.flow_control?.target_files && task.flow_control.target_files.length > 0 && ( - - {task.flow_control.target_files.length} - file{task.flow_control.target_files.length > 1 ? 's' : ''} + {tasks.map((task, index) => { + const filesCount = task.flow_control?.target_files?.length || 0; + const stepsCount = task.flow_control?.implementation_approach?.length || 0; + const criteriaCount = task.context?.acceptance?.length || 0; + const depsCount = task.context?.depends_on?.length || 0; + + return ( + { + e.stopPropagation(); + onTaskClick(task); + }} + > + +
+
+ + {task.task_id || `#${index + 1}`} - )} +

+ {task.title || formatMessage({ id: 'liteTasks.untitled' })} +

+
+ {/* Meta badges - right side, single row */} +
+ {task.meta?.type && ( + {task.meta.type} + )} + {filesCount > 0 && ( + + + {filesCount} files + + )} + {stepsCount > 0 && ( + + {stepsCount} steps + + )} + {criteriaCount > 0 && ( + + {criteriaCount} criteria + + )} + {depsCount > 0 && ( +
+ + {task.context.depends_on.map((depId, idx) => ( + + {depId} + + ))} +
+ )} +
-
- {task.description && ( -

- {task.description} -

- )} - - - ))} + {task.description && ( +

+ {task.description} +

+ )} + + + ); + })}
)} @@ -328,9 +345,117 @@ function ContextContent({ icon={} title={formatMessage({ id: 'liteTasks.contextPanel.contextPackage' })} > -
-            {JSON.stringify(contextData.context, null, 2)}
-          
+
+ {contextData.context.task_description && ( +
+ {formatMessage({ id: 'liteTasks.contextPanel.taskDescription' })}:{' '} + {contextData.context.task_description as string} +
+ )} + + {contextData.context.constraints && contextData.context.constraints.length > 0 && ( +
+
+ 约束: +
+
+ {contextData.context.constraints.map((c, i) => ( +
+ + {c as string} +
+ ))} +
+
+ )} + + {contextData.context.focus_paths && contextData.context.focus_paths.length > 0 && ( +
+ {formatMessage({ id: 'liteTasks.contextPanel.focusPaths' })}:{' '} +
+ {contextData.context.focus_paths.map((p, i) => ( + + {p as string} + + ))} +
+
+ )} + + {contextData.context.relevant_files && contextData.context.relevant_files.length > 0 && ( +
+
+ 相关文件:{' '} + + {contextData.context.relevant_files.length} + +
+
+ {contextData.context.relevant_files.map((f, i) => { + const filePath = typeof f === 'string' ? f : (f as { path: string; reason?: string }).path; + const reason = typeof f === 'string' ? undefined : (f as { path: string; reason?: string }).reason; + return ( +
+ {i + 1}. + + {filePath as string} + + {reason && ( + + ({reason}) + + )} +
+ ); + })} +
+
+ )} + + {contextData.context.dependencies && contextData.context.dependencies.length > 0 && ( +
+
+ 依赖: +
+
+ {contextData.context.dependencies.map((d, i) => { + const depInfo = typeof d === 'string' + ? { name: d, type: '', version: '' } + : d as { name: string; type?: string; version?: string }; + return ( + + {depInfo.name} + {depInfo.version && @{depInfo.version}} + + ); + })} +
+
+ )} + + {contextData.context.session_id && ( +
+ 会话ID:{' '} + {contextData.context.session_id as string} +
+ )} + + {contextData.context.metadata && ( +
+
+ 元数据: +
+
+ {Object.entries(contextData.context.metadata as Record).map(([k, v]) => ( +
+ {k}: + {String(v)} +
+ ))} +
+
+ )} +
)} @@ -405,7 +530,7 @@ function ContextSection({ ); } -type MultiCliExpandedTab = 'tasks' | 'discussion' | 'context' | 'summary'; +type MultiCliExpandedTab = 'tasks' | 'discussion' | 'context'; /** * ExpandedMultiCliPanel - Multi-tab panel shown when a multi-cli session is expanded @@ -581,17 +706,6 @@ function ExpandedMultiCliPanel({ {formatMessage({ id: 'liteTasks.quickCards.context' })} -
{/* Tasks Tab */} @@ -657,43 +771,46 @@ function ExpandedMultiCliPanel({ }} > -
-
+
+
{task.task_id || `T${index + 1}`} -
-

- {task.title || formatMessage({ id: 'liteTasks.untitled' })} -

- {/* Meta badges */} -
- {task.meta?.type && ( - {task.meta.type} - )} - {filesCount > 0 && ( - - - {filesCount} files +

+ {task.title || formatMessage({ id: 'liteTasks.untitled' })} +

+
+ {/* Meta badges - right side, single row */} +
+ {task.meta?.type && ( + {task.meta.type} + )} + {filesCount > 0 && ( + + + {filesCount} files + + )} + {stepsCount > 0 && ( + + {stepsCount} steps + + )} + {criteriaCount > 0 && ( + + {criteriaCount} criteria + + )} + {depsCount > 0 && ( +
+ + {task.context.depends_on.map((depId, idx) => ( + + {depId} - )} - {stepsCount > 0 && ( - - {stepsCount} steps - - )} - {criteriaCount > 0 && ( - - {criteriaCount} criteria - - )} - {depsCount > 0 && ( - - {depsCount} deps - - )} + ))}
-
+ )}
@@ -757,51 +874,6 @@ function ExpandedMultiCliPanel({
)} - {/* Summary Tab */} - {activeTab === 'summary' && ( -
- - -
- -

- {formatMessage({ id: 'liteTasks.multiCli.planSummary' })} -

-
- {goal && ( -
-

{formatMessage({ id: 'liteTasks.multiCli.goal' })}

-

{goal}

-
- )} - {solution && ( -
-

{formatMessage({ id: 'liteTasks.multiCli.solution' })}

-

{solution}

-
- )} - {implementationChain && ( -
-

{formatMessage({ id: 'liteTasks.multiCli.implementation' })}

- - {implementationChain} - -
- )} -
- {formatMessage({ id: 'liteTasks.quickCards.tasks' })}: - {taskCount} - {feasibility > 0 && ( - <> - {formatMessage({ id: 'liteTasks.multiCli.feasibility' })}: - {feasibility}% - - )} -
-
-
-
- )}
); } diff --git a/ccw/frontend/src/pages/orchestrator/PropertyPanel.tsx b/ccw/frontend/src/pages/orchestrator/PropertyPanel.tsx index 45533b3c..52145a7f 100644 --- a/ccw/frontend/src/pages/orchestrator/PropertyPanel.tsx +++ b/ccw/frontend/src/pages/orchestrator/PropertyPanel.tsx @@ -9,6 +9,7 @@ import { Settings, X, Terminal, FileText, GitBranch, GitMerge, Trash2 } from 'lu import { cn } from '@/lib/utils'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; +import { CommandCombobox } from '@/components/ui/CommandCombobox'; import { MultiNodeSelector, type NodeOption } from '@/components/ui/MultiNodeSelector'; import { ContextAssembler } from '@/components/ui/ContextAssembler'; import { useFlowStore } from '@/stores'; @@ -97,11 +98,10 @@ function SlashCommandProperties({
- onChange({ command: e.target.value })} + onChange={(value) => onChange({ command: value })} placeholder={formatMessage({ id: 'orchestrator.propertyPanel.placeholders.commandName' })} - className="font-mono" />