mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
feat: add CommandCombobox component for selecting slash commands and update PropertyPanel to use it
refactor: remove unused liteTasks localization from common.json and zh/common.json refactor: consolidate liteTasks localization into lite-tasks.json and zh/lite-tasks.json refactor: simplify MultiCliTab type in LiteTaskDetailPage refactor: enhance task display in LiteTasksPage with additional metadata
This commit is contained in:
180
ccw/frontend/src/components/ui/CommandCombobox.tsx
Normal file
180
ccw/frontend/src/components/ui/CommandCombobox.tsx
Normal file
@@ -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<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(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<string, Command[]> = {};
|
||||
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<HTMLInputElement>) => {
|
||||
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 (
|
||||
<div ref={containerRef} className="relative">
|
||||
{/* Trigger button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setOpen(!open);
|
||||
if (!open) {
|
||||
setTimeout(() => inputRef.current?.focus(), 0);
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
||||
!value && 'text-muted-foreground',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span className={cn('truncate font-mono', !value && 'text-muted-foreground')}>
|
||||
{displayValue || placeholder || '/command-name'}
|
||||
</span>
|
||||
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</button>
|
||||
|
||||
{/* Dropdown */}
|
||||
{open && (
|
||||
<div className="absolute z-50 mt-1 w-full rounded-md border border-border bg-card shadow-md">
|
||||
{/* Search input */}
|
||||
<div className="flex items-center border-b border-border px-3">
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<input
|
||||
ref={inputRef}
|
||||
value={search}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder || '/command-name'}
|
||||
className="flex h-9 w-full bg-transparent py-2 text-sm outline-none placeholder:text-muted-foreground font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Command list */}
|
||||
<div className="max-h-64 overflow-y-auto p-1">
|
||||
{isLoading ? (
|
||||
<div className="py-4 text-center text-sm text-muted-foreground">Loading...</div>
|
||||
) : totalFiltered === 0 ? (
|
||||
<div className="py-4 text-center text-sm text-muted-foreground">
|
||||
No commands found
|
||||
</div>
|
||||
) : (
|
||||
Object.entries(groupedFiltered)
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([group, cmds]) => (
|
||||
<div key={group}>
|
||||
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
||||
{group}
|
||||
</div>
|
||||
{cmds.map((cmd) => (
|
||||
<button
|
||||
key={cmd.name}
|
||||
type="button"
|
||||
onClick={() => handleSelect(cmd.name)}
|
||||
className={cn(
|
||||
'flex w-full flex-col items-start rounded-sm px-2 py-1.5 text-sm cursor-pointer hover:bg-accent hover:text-accent-foreground',
|
||||
value === cmd.name && 'bg-accent/50'
|
||||
)}
|
||||
>
|
||||
<span className="font-mono text-foreground">/{cmd.name}</span>
|
||||
{cmd.description && (
|
||||
<span className="text-xs text-muted-foreground truncate w-full text-left">
|
||||
{cmd.description}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "请回答以下问题",
|
||||
|
||||
@@ -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": "待处理"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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' && (
|
||||
<div className="space-y-2">
|
||||
{tasks.map((task, index) => (
|
||||
<Card
|
||||
key={task.id || index}
|
||||
className="cursor-pointer hover:shadow-sm hover:border-primary/50 transition-all border-border"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onTaskClick(task);
|
||||
}}
|
||||
>
|
||||
<CardContent className="p-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||
<Badge className="text-xs font-mono shrink-0 bg-primary/10 text-primary border-primary/20">
|
||||
{task.task_id || `#${index + 1}`}
|
||||
</Badge>
|
||||
<h4 className="text-sm font-medium text-foreground flex-1 line-clamp-1">
|
||||
{task.title || formatMessage({ id: 'liteTasks.untitled' })}
|
||||
</h4>
|
||||
</div>
|
||||
{/* Right: Meta info */}
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
{/* Dependencies - show task IDs */}
|
||||
{task.context?.depends_on && task.context.depends_on.length > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-xs text-muted-foreground">→</span>
|
||||
{task.context.depends_on.map((depId, idx) => (
|
||||
<Badge key={idx} variant="outline" className="h-5 px-2 py-0.5 text-xs font-mono border-primary/30 text-primary">
|
||||
{depId}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{/* Target Files Count */}
|
||||
{task.flow_control?.target_files && task.flow_control.target_files.length > 0 && (
|
||||
<Badge variant="secondary" className="h-4 px-1.5 py-0 text-[10px] gap-0.5">
|
||||
<span className="font-semibold">{task.flow_control.target_files.length}</span>
|
||||
<span>file{task.flow_control.target_files.length > 1 ? 's' : ''}</span>
|
||||
{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 (
|
||||
<Card
|
||||
key={task.id || index}
|
||||
className="cursor-pointer hover:shadow-sm hover:border-primary/50 transition-all border-border border-l-4 border-l-primary/50"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onTaskClick(task);
|
||||
}}
|
||||
>
|
||||
<CardContent className="p-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||
<Badge className="text-xs font-mono shrink-0 bg-primary/10 text-primary border-primary/20">
|
||||
{task.task_id || `#${index + 1}`}
|
||||
</Badge>
|
||||
)}
|
||||
<h4 className="text-sm font-medium text-foreground line-clamp-1">
|
||||
{task.title || formatMessage({ id: 'liteTasks.untitled' })}
|
||||
</h4>
|
||||
</div>
|
||||
{/* Meta badges - right side, single row */}
|
||||
<div className="flex items-center gap-1.5 shrink-0">
|
||||
{task.meta?.type && (
|
||||
<Badge variant="info" className="text-[10px] px-1.5 py-0 whitespace-nowrap">{task.meta.type}</Badge>
|
||||
)}
|
||||
{filesCount > 0 && (
|
||||
<Badge variant="secondary" className="text-[10px] px-1.5 py-0 gap-0.5 whitespace-nowrap">
|
||||
<FileCode className="h-2.5 w-2.5" />
|
||||
{filesCount} files
|
||||
</Badge>
|
||||
)}
|
||||
{stepsCount > 0 && (
|
||||
<Badge variant="secondary" className="text-[10px] px-1.5 py-0 whitespace-nowrap">
|
||||
{stepsCount} steps
|
||||
</Badge>
|
||||
)}
|
||||
{criteriaCount > 0 && (
|
||||
<Badge variant="secondary" className="text-[10px] px-1.5 py-0 whitespace-nowrap">
|
||||
{criteriaCount} criteria
|
||||
</Badge>
|
||||
)}
|
||||
{depsCount > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-[10px] text-muted-foreground">→</span>
|
||||
{task.context.depends_on.map((depId, idx) => (
|
||||
<Badge key={idx} variant="outline" className="text-[10px] px-1.5 py-0 font-mono border-primary/30 text-primary whitespace-nowrap">
|
||||
{depId}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{task.description && (
|
||||
<p className="text-xs text-muted-foreground mt-1.5 pl-[calc(1.5rem+0.75rem)] line-clamp-2">
|
||||
{task.description}
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
{task.description && (
|
||||
<p className="text-xs text-muted-foreground mt-1.5 pl-[calc(1.5rem+0.75rem)] line-clamp-2">
|
||||
{task.description}
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -328,9 +345,117 @@ function ContextContent({
|
||||
icon={<Package className="h-4 w-4" />}
|
||||
title={formatMessage({ id: 'liteTasks.contextPanel.contextPackage' })}
|
||||
>
|
||||
<pre className="text-xs text-muted-foreground overflow-auto max-h-48 bg-muted/50 rounded p-2 whitespace-pre-wrap">
|
||||
{JSON.stringify(contextData.context, null, 2)}
|
||||
</pre>
|
||||
<div className="space-y-2 text-xs">
|
||||
{contextData.context.task_description && (
|
||||
<div className="text-muted-foreground">
|
||||
<span className="font-medium text-foreground">{formatMessage({ id: 'liteTasks.contextPanel.taskDescription' })}:</span>{' '}
|
||||
{contextData.context.task_description as string}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{contextData.context.constraints && contextData.context.constraints.length > 0 && (
|
||||
<div>
|
||||
<div className="text-muted-foreground mb-1">
|
||||
<span className="font-medium text-foreground">约束:</span>
|
||||
</div>
|
||||
<div className="space-y-1 pl-2">
|
||||
{contextData.context.constraints.map((c, i) => (
|
||||
<div key={i} className="text-muted-foreground flex items-start gap-1">
|
||||
<span className="text-primary/50">•</span>
|
||||
<span>{c as string}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{contextData.context.focus_paths && contextData.context.focus_paths.length > 0 && (
|
||||
<div className="text-muted-foreground">
|
||||
<span className="font-medium text-foreground">{formatMessage({ id: 'liteTasks.contextPanel.focusPaths' })}:</span>{' '}
|
||||
<div className="flex flex-wrap gap-1 mt-0.5">
|
||||
{contextData.context.focus_paths.map((p, i) => (
|
||||
<Badge key={i} variant="secondary" className="text-[10px] font-mono">
|
||||
{p as string}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{contextData.context.relevant_files && contextData.context.relevant_files.length > 0 && (
|
||||
<div>
|
||||
<div className="text-muted-foreground mb-1">
|
||||
<span className="font-medium text-foreground">相关文件:</span>{' '}
|
||||
<Badge variant="outline" className="text-[10px] align-middle">
|
||||
{contextData.context.relevant_files.length}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="space-y-0.5 pl-2 max-h-32 overflow-y-auto">
|
||||
{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 (
|
||||
<div key={i} className="group flex items-start gap-1 text-muted-foreground hover:bg-muted/30 rounded px-1 py-0.5">
|
||||
<span className="text-primary/50 shrink-0">{i + 1}.</span>
|
||||
<span className="font-mono text-xs truncate flex-1" title={filePath as string}>
|
||||
{filePath as string}
|
||||
</span>
|
||||
{reason && (
|
||||
<span className="text-[10px] text-muted-foreground/60 truncate ml-1" title={reason}>
|
||||
({reason})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{contextData.context.dependencies && contextData.context.dependencies.length > 0 && (
|
||||
<div>
|
||||
<div className="text-muted-foreground mb-1">
|
||||
<span className="font-medium text-foreground">依赖:</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{contextData.context.dependencies.map((d, i) => {
|
||||
const depInfo = typeof d === 'string'
|
||||
? { name: d, type: '', version: '' }
|
||||
: d as { name: string; type?: string; version?: string };
|
||||
return (
|
||||
<Badge key={i} variant="outline" className="text-[10px]">
|
||||
{depInfo.name}
|
||||
{depInfo.version && <span className="ml-1 opacity-70">@{depInfo.version}</span>}
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{contextData.context.session_id && (
|
||||
<div className="text-muted-foreground">
|
||||
<span className="font-medium text-foreground">会话ID:</span>{' '}
|
||||
<span className="font-mono bg-muted/50 px-1.5 py-0.5 rounded">{contextData.context.session_id as string}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{contextData.context.metadata && (
|
||||
<div>
|
||||
<div className="text-muted-foreground mb-1">
|
||||
<span className="font-medium text-foreground">元数据:</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-x-3 gap-y-0.5 pl-2 text-muted-foreground">
|
||||
{Object.entries(contextData.context.metadata as Record<string, unknown>).map(([k, v]) => (
|
||||
<div key={k} className="flex items-center gap-1">
|
||||
<span className="font-mono text-[10px] text-primary/60">{k}:</span>
|
||||
<span className="truncate">{String(v)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ContextSection>
|
||||
)}
|
||||
|
||||
@@ -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({
|
||||
<Package className="h-3.5 w-3.5" />
|
||||
{formatMessage({ id: 'liteTasks.quickCards.context' })}
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setActiveTab('summary'); }}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors ${
|
||||
activeTab === 'summary'
|
||||
? 'bg-primary/10 text-primary border-primary/30'
|
||||
: 'bg-muted/50 text-muted-foreground border-border hover:bg-muted'
|
||||
}`}
|
||||
>
|
||||
<FileText className="h-3.5 w-3.5" />
|
||||
{formatMessage({ id: 'liteTasks.multiCli.summary' })}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tasks Tab */}
|
||||
@@ -657,43 +771,46 @@ function ExpandedMultiCliPanel({
|
||||
}}
|
||||
>
|
||||
<CardContent className="p-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex items-start gap-3 flex-1 min-w-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||
<Badge className="text-xs font-mono shrink-0 bg-primary/10 text-primary border-primary/20">
|
||||
{task.task_id || `T${index + 1}`}
|
||||
</Badge>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="text-sm font-medium text-foreground line-clamp-1">
|
||||
{task.title || formatMessage({ id: 'liteTasks.untitled' })}
|
||||
</h4>
|
||||
{/* Meta badges */}
|
||||
<div className="flex flex-wrap items-center gap-1.5 mt-1.5">
|
||||
{task.meta?.type && (
|
||||
<Badge variant="info" className="text-[10px] px-1.5 py-0">{task.meta.type}</Badge>
|
||||
)}
|
||||
{filesCount > 0 && (
|
||||
<Badge variant="secondary" className="text-[10px] px-1.5 py-0 gap-0.5">
|
||||
<FileCode className="h-2.5 w-2.5" />
|
||||
{filesCount} files
|
||||
<h4 className="text-sm font-medium text-foreground line-clamp-1">
|
||||
{task.title || formatMessage({ id: 'liteTasks.untitled' })}
|
||||
</h4>
|
||||
</div>
|
||||
{/* Meta badges - right side, single row */}
|
||||
<div className="flex items-center gap-1.5 shrink-0">
|
||||
{task.meta?.type && (
|
||||
<Badge variant="info" className="text-[10px] px-1.5 py-0 whitespace-nowrap">{task.meta.type}</Badge>
|
||||
)}
|
||||
{filesCount > 0 && (
|
||||
<Badge variant="secondary" className="text-[10px] px-1.5 py-0 gap-0.5 whitespace-nowrap">
|
||||
<FileCode className="h-2.5 w-2.5" />
|
||||
{filesCount} files
|
||||
</Badge>
|
||||
)}
|
||||
{stepsCount > 0 && (
|
||||
<Badge variant="secondary" className="text-[10px] px-1.5 py-0 whitespace-nowrap">
|
||||
{stepsCount} steps
|
||||
</Badge>
|
||||
)}
|
||||
{criteriaCount > 0 && (
|
||||
<Badge variant="secondary" className="text-[10px] px-1.5 py-0 whitespace-nowrap">
|
||||
{criteriaCount} criteria
|
||||
</Badge>
|
||||
)}
|
||||
{depsCount > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-[10px] text-muted-foreground">→</span>
|
||||
{task.context.depends_on.map((depId, idx) => (
|
||||
<Badge key={idx} variant="outline" className="text-[10px] px-1.5 py-0 font-mono border-primary/30 text-primary whitespace-nowrap">
|
||||
{depId}
|
||||
</Badge>
|
||||
)}
|
||||
{stepsCount > 0 && (
|
||||
<Badge variant="secondary" className="text-[10px] px-1.5 py-0">
|
||||
{stepsCount} steps
|
||||
</Badge>
|
||||
)}
|
||||
{criteriaCount > 0 && (
|
||||
<Badge variant="secondary" className="text-[10px] px-1.5 py-0">
|
||||
{criteriaCount} criteria
|
||||
</Badge>
|
||||
)}
|
||||
{depsCount > 0 && (
|
||||
<Badge variant="outline" className="text-[10px] px-1.5 py-0 border-primary/30 text-primary">
|
||||
{depsCount} deps
|
||||
</Badge>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -757,51 +874,6 @@ function ExpandedMultiCliPanel({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Summary Tab */}
|
||||
{activeTab === 'summary' && (
|
||||
<div className="space-y-3">
|
||||
<Card className="border-border">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Target className="h-5 w-5 text-primary" />
|
||||
<h4 className="font-medium text-foreground">
|
||||
{formatMessage({ id: 'liteTasks.multiCli.planSummary' })}
|
||||
</h4>
|
||||
</div>
|
||||
{goal && (
|
||||
<div className="mb-3">
|
||||
<p className="text-xs text-muted-foreground mb-1">{formatMessage({ id: 'liteTasks.multiCli.goal' })}</p>
|
||||
<p className="text-sm text-foreground">{goal}</p>
|
||||
</div>
|
||||
)}
|
||||
{solution && (
|
||||
<div className="mb-3">
|
||||
<p className="text-xs text-muted-foreground mb-1">{formatMessage({ id: 'liteTasks.multiCli.solution' })}</p>
|
||||
<p className="text-sm text-foreground">{solution}</p>
|
||||
</div>
|
||||
)}
|
||||
{implementationChain && (
|
||||
<div className="mb-3">
|
||||
<p className="text-xs text-muted-foreground mb-1">{formatMessage({ id: 'liteTasks.multiCli.implementation' })}</p>
|
||||
<code className="block px-3 py-2 rounded bg-muted border border-border text-xs font-mono">
|
||||
{implementationChain}
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2 pt-2 border-t border-border/50">
|
||||
<span className="text-xs text-muted-foreground">{formatMessage({ id: 'liteTasks.quickCards.tasks' })}:</span>
|
||||
<Badge variant="secondary" className="text-xs">{taskCount}</Badge>
|
||||
{feasibility > 0 && (
|
||||
<>
|
||||
<span className="text-xs text-muted-foreground ml-2">{formatMessage({ id: 'liteTasks.multiCli.feasibility' })}:</span>
|
||||
<Badge variant="success" className="text-xs">{feasibility}%</Badge>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-1">{formatMessage({ id: 'orchestrator.propertyPanel.labels.command' })}</label>
|
||||
<Input
|
||||
<CommandCombobox
|
||||
value={data.command || ''}
|
||||
onChange={(e) => onChange({ command: e.target.value })}
|
||||
onChange={(value) => onChange({ command: value })}
|
||||
placeholder={formatMessage({ id: 'orchestrator.propertyPanel.placeholders.commandName' })}
|
||||
className="font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user