feat: Add specialized context rendering for multi-cli-plan sessions

- Add MultiCliContextPackage type for multi-cli-plan context-package.json
- Create MultiCliContextContent component with sections for:
  - Selected solution (feasibility, effort, risk, CLI sources)
  - Implementation plan (approach, tasks with files, execution flow, milestones)
  - Dependencies (internal and external)
  - CLI consensus (agreements, resolved conflicts)
  - Technical concerns and constraints
- Update ExpandedMultiCliPanel to detect and use appropriate context renderer
- Add i18n translations for new context labels
This commit is contained in:
catlog22
2026-02-14 23:41:28 +08:00
parent 0bc0a13587
commit 9aa4dd1f6f
4 changed files with 367 additions and 4 deletions

View File

@@ -2244,6 +2244,42 @@ export interface RoundSynthesis {
user_feedback_incorporated?: string;
}
// Multi-cli-plan context-package.json structure
export interface MultiCliContextPackage {
solution?: {
name: string;
source_cli: string[];
feasibility: number;
effort: string;
risk: string;
summary: string;
};
implementation_plan?: {
approach: string;
tasks: Array<{
id: string;
name: string;
depends_on: string[];
files: SolutionFileAction[];
key_point: string | null;
}>;
execution_flow: string;
milestones: string[];
};
dependencies?: {
internal: string[];
external: string[];
};
technical_concerns?: string[];
consensus?: {
agreements: string[];
resolved_conflicts?: string;
};
constraints?: string[];
task_description?: string;
session_id?: string;
}
export interface LiteTasksResponse {
litePlan?: LiteTaskSession[];
liteFix?: LiteTaskSession[];

View File

@@ -36,7 +36,22 @@
"agreements": "Agreements",
"disagreements": "Disagreements",
"resolution": "Resolution",
"clarificationQuestions": "Clarification Questions"
"clarificationQuestions": "Clarification Questions",
"context": {
"solution": "Selected Solution",
"implementationPlan": "Implementation Plan",
"approach": "Approach",
"tasks": "Tasks",
"milestones": "Milestones",
"dependencies": "Dependencies",
"internal": "Internal",
"external": "External",
"consensus": "CLI Consensus",
"resolvedConflicts": "Resolved Conflicts",
"technicalConcerns": "Technical Concerns",
"constraints": "Constraints",
"sessionInfo": "Session Info"
}
},
"createdAt": "Created",
"rounds": "rounds",

View File

@@ -36,7 +36,22 @@
"agreements": "共识",
"disagreements": "分歧",
"resolution": "解决方式",
"clarificationQuestions": "澄清问题"
"clarificationQuestions": "澄清问题",
"context": {
"solution": "选定方案",
"implementationPlan": "实现计划",
"approach": "实现路径",
"tasks": "任务列表",
"milestones": "里程碑",
"dependencies": "依赖项",
"internal": "内部依赖",
"external": "外部依赖",
"consensus": "CLI 共识",
"resolvedConflicts": "已解决分歧",
"technicalConcerns": "技术关注点",
"constraints": "约束条件",
"sessionInfo": "会话信息"
}
},
"createdAt": "创建时间",
"rounds": "轮",

View File

@@ -39,6 +39,12 @@ import {
Timer,
Sparkles,
CheckCheck,
Route,
Flag,
AlertOctagon,
Link2,
ShieldCheck,
Settings2,
} from 'lucide-react';
import { useLiteTasks } from '@/hooks/useLiteTasks';
import { Button } from '@/components/ui/Button';
@@ -46,7 +52,7 @@ import { Badge } from '@/components/ui/Badge';
import { Card, CardContent } from '@/components/ui/Card';
import { TabsNavigation } from '@/components/ui/TabsNavigation';
import { TaskDrawer } from '@/components/shared/TaskDrawer';
import { fetchLiteSessionContext, type LiteTask, type LiteTaskSession, type LiteSessionContext, type RoundSynthesis } from '@/lib/api';
import { fetchLiteSessionContext, type LiteTask, type LiteTaskSession, type LiteSessionContext, type RoundSynthesis, type MultiCliContextPackage } from '@/lib/api';
import { LiteContextContent } from '@/components/lite-tasks/LiteContextContent';
import { useNavigate } from 'react-router-dom';
@@ -540,6 +546,292 @@ function RoundDetailCard({ round, isLast }: { round: RoundSynthesis; isLast: boo
);
}
/**
* MultiCliContextContent - Display context-package.json for multi-cli-plan sessions
*/
function MultiCliContextContent({ data }: { data: MultiCliContextPackage }) {
const { formatMessage } = useIntl();
const [expandedSections, setExpandedSections] = React.useState<Set<string>>(new Set(['solution', 'plan']));
const toggleSection = (section: string) => {
setExpandedSections(prev => {
const next = new Set(prev);
if (next.has(section)) {
next.delete(section);
} else {
next.add(section);
}
return next;
});
};
const isExpanded = (section: string) => expandedSections.has(section);
// Section wrapper component
const Section = ({ id, icon, title, badge, children, defaultExpanded = true }: {
id: string;
icon: React.ReactNode;
title: string;
badge?: React.ReactNode;
children: React.ReactNode;
defaultExpanded?: boolean;
}) => {
// Initialize expanded state based on defaultExpanded if not already in set
React.useEffect(() => {
if (defaultExpanded && !expandedSections.has(id)) {
setExpandedSections(prev => new Set(prev).add(id));
}
}, [id, defaultExpanded]);
return (
<Card className="border-border">
<button
type="button"
className="w-full flex items-center gap-2 p-3 text-left hover:bg-muted/50 transition-colors"
onClick={() => toggleSection(id)}
>
<span className="text-muted-foreground">{icon}</span>
<span className="text-sm font-medium text-foreground flex-1">{title}</span>
{badge}
{isExpanded(id) ? (
<ChevronDown className="h-4 w-4 text-muted-foreground" />
) : (
<ChevronRight className="h-4 w-4 text-muted-foreground" />
)}
</button>
{isExpanded(id) && (
<CardContent className="px-3 pb-3 pt-0">
{children}
</CardContent>
)}
</Card>
);
};
return (
<div className="space-y-3">
{/* Solution Section */}
{data.solution && (
<Section
id="solution"
icon={<Sparkles className="h-4 w-4" />}
title={formatMessage({ id: 'liteTasks.multiCli.context.solution' })}
badge={
<div className="flex items-center gap-1">
{data.solution.source_cli.map(cli => (
<Badge key={cli} variant="outline" className="text-[10px] px-1.5 py-0">{cli}</Badge>
))}
</div>
}
>
<div className="space-y-2">
<h4 className="font-medium text-foreground text-sm">{data.solution.name}</h4>
<p className="text-xs text-muted-foreground">{data.solution.summary}</p>
<div className="flex flex-wrap items-center gap-2">
<Badge variant="success" className="text-[10px]">
{Math.round(data.solution.feasibility * 100)}% {formatMessage({ id: 'liteTasks.multiCli.feasible' })}
</Badge>
<Badge variant="warning" className="text-[10px]">{data.solution.effort}</Badge>
<Badge variant={data.solution.risk === 'high' ? 'destructive' : data.solution.risk === 'low' ? 'success' : 'warning'} className="text-[10px]">
{data.solution.risk} {formatMessage({ id: 'liteTasks.multiCli.risk' })}
</Badge>
</div>
</div>
</Section>
)}
{/* Implementation Plan Section */}
{data.implementation_plan && (
<Section
id="plan"
icon={<Route className="h-4 w-4" />}
title={formatMessage({ id: 'liteTasks.multiCli.context.implementationPlan' })}
badge={<Badge variant="secondary" className="text-[10px]">{data.implementation_plan.tasks?.length || 0} tasks</Badge>}
>
<div className="space-y-3">
{/* Approach */}
<div className="text-xs text-muted-foreground">
<span className="font-medium text-foreground">{formatMessage({ id: 'liteTasks.multiCli.context.approach' })}:</span>{' '}
{data.implementation_plan.approach}
</div>
{/* Execution Flow */}
{data.implementation_plan.execution_flow && (
<div className="p-2 bg-muted/50 rounded-md">
<code className="text-xs font-mono text-foreground">{data.implementation_plan.execution_flow}</code>
</div>
)}
{/* Tasks */}
{data.implementation_plan.tasks && data.implementation_plan.tasks.length > 0 && (
<div className="space-y-2">
<div className="text-xs font-medium text-foreground">{formatMessage({ id: 'liteTasks.multiCli.context.tasks' })}</div>
<div className="space-y-1.5">
{data.implementation_plan.tasks.map((task, idx) => (
<div key={task.id || idx} className="flex items-start gap-2 p-2 bg-muted/30 rounded-md">
<Badge variant="outline" className="text-[10px] font-mono shrink-0">{task.id}</Badge>
<div className="flex-1 min-w-0">
<div className="text-xs font-medium text-foreground">{task.name}</div>
{task.key_point && (
<div className="text-[10px] text-muted-foreground mt-0.5">{task.key_point}</div>
)}
{task.files && task.files.length > 0 && (
<div className="flex flex-wrap gap-1 mt-1">
{task.files.map((f, i) => (
<Badge key={i} variant="secondary" className="text-[9px] font-mono">
{f.action === 'create' ? '+' : f.action === 'delete' ? '-' : '~'} {f.file}
</Badge>
))}
</div>
)}
</div>
</div>
))}
</div>
</div>
)}
{/* Milestones */}
{data.implementation_plan.milestones && data.implementation_plan.milestones.length > 0 && (
<div className="space-y-1.5">
<div className="flex items-center gap-2 text-xs font-medium text-foreground">
<Flag className="h-3 w-3" />
{formatMessage({ id: 'liteTasks.multiCli.context.milestones' })}
</div>
<div className="flex flex-wrap gap-1.5">
{data.implementation_plan.milestones.map((milestone, i) => (
<Badge key={i} variant="info" className="text-[10px]">{milestone}</Badge>
))}
</div>
</div>
)}
</div>
</Section>
)}
{/* Dependencies Section */}
{data.dependencies && (data.dependencies.internal?.length > 0 || data.dependencies.external?.length > 0) && (
<Section
id="deps"
icon={<Link2 className="h-4 w-4" />}
title={formatMessage({ id: 'liteTasks.multiCli.context.dependencies' })}
badge={<Badge variant="secondary" className="text-[10px]">{(data.dependencies.internal?.length || 0) + (data.dependencies.external?.length || 0)}</Badge>}
>
<div className="space-y-2">
{data.dependencies.internal && data.dependencies.internal.length > 0 && (
<div>
<div className="text-xs font-medium text-foreground mb-1">{formatMessage({ id: 'liteTasks.multiCli.context.internal' })}</div>
<div className="flex flex-wrap gap-1">
{data.dependencies.internal.map((dep, i) => (
<Badge key={i} variant="outline" className="text-[10px] font-mono">{dep}</Badge>
))}
</div>
</div>
)}
{data.dependencies.external && data.dependencies.external.length > 0 && (
<div>
<div className="text-xs font-medium text-foreground mb-1">{formatMessage({ id: 'liteTasks.multiCli.context.external' })}</div>
<div className="flex flex-wrap gap-1">
{data.dependencies.external.map((dep, i) => (
<Badge key={i} variant="secondary" className="text-[10px] font-mono">{dep}</Badge>
))}
</div>
</div>
)}
</div>
</Section>
)}
{/* Consensus Section */}
{data.consensus && data.consensus.agreements?.length > 0 && (
<Section
id="consensus"
icon={<ShieldCheck className="h-4 w-4" />}
title={formatMessage({ id: 'liteTasks.multiCli.context.consensus' })}
badge={<Badge variant="success" className="text-[10px]">{data.consensus.agreements.length}</Badge>}
>
<div className="space-y-2">
<ul className="space-y-1">
{data.consensus.agreements.map((agreement, i) => (
<li key={i} className="flex items-start gap-2 text-xs text-muted-foreground">
<CheckCircle2 className="h-3 w-3 text-success shrink-0 mt-0.5" />
<span>{agreement}</span>
</li>
))}
</ul>
{data.consensus.resolved_conflicts && (
<div className="mt-2 p-2 bg-success/10 border border-success/20 rounded-md text-xs text-muted-foreground">
<span className="font-medium text-success">{formatMessage({ id: 'liteTasks.multiCli.context.resolvedConflicts' })}:</span>{' '}
{data.consensus.resolved_conflicts}
</div>
)}
</div>
</Section>
)}
{/* Technical Concerns Section */}
{data.technical_concerns && data.technical_concerns.length > 0 && (
<Section
id="concerns"
icon={<AlertOctagon className="h-4 w-4" />}
title={formatMessage({ id: 'liteTasks.multiCli.context.technicalConcerns' })}
badge={<Badge variant="warning" className="text-[10px]">{data.technical_concerns.length}</Badge>}
>
<ul className="space-y-1">
{data.technical_concerns.map((concern, i) => (
<li key={i} className="flex items-start gap-2 text-xs text-muted-foreground">
<AlertCircle className="h-3 w-3 text-warning shrink-0 mt-0.5" />
<span>{concern}</span>
</li>
))}
</ul>
</Section>
)}
{/* Constraints Section */}
{data.constraints && data.constraints.length > 0 && (
<Section
id="constraints"
icon={<Settings2 className="h-4 w-4" />}
title={formatMessage({ id: 'liteTasks.multiCli.context.constraints' })}
badge={<Badge variant="secondary" className="text-[10px]">{data.constraints.length}</Badge>}
>
<div className="flex flex-wrap gap-1.5">
{data.constraints.map((constraint, i) => (
<Badge key={i} variant="outline" className="text-[10px]">{constraint}</Badge>
))}
</div>
</Section>
)}
{/* Session Info */}
{(data.task_description || data.session_id) && (
<Section
id="info"
icon={<Package className="h-4 w-4" />}
title={formatMessage({ id: 'liteTasks.multiCli.context.sessionInfo' })}
defaultExpanded={false}
>
<div className="space-y-2 text-xs">
{data.task_description && (
<div className="text-muted-foreground">
<span className="font-medium text-foreground">{formatMessage({ id: 'liteTasks.contextPanel.taskDescription' })}:</span>{' '}
{data.task_description}
</div>
)}
{data.session_id && (
<div className="text-muted-foreground">
<span className="font-medium text-foreground">{formatMessage({ id: 'liteTasks.contextPanel.sessionId' })}:</span>{' '}
<span className="font-mono bg-muted/50 px-1.5 py-0.5 rounded">{data.session_id}</span>
</div>
)}
</div>
</Section>
)}
</div>
);
}
type MultiCliExpandedTab = 'tasks' | 'discussion' | 'context';
/**
@@ -882,7 +1174,12 @@ function ExpandedMultiCliPanel({
</div>
)}
{!contextLoading && !contextError && contextData && (
// Detect multi-cli-plan context by checking for solution field
(contextData.context as MultiCliContextPackage)?.solution ? (
<MultiCliContextContent data={contextData.context as MultiCliContextPackage} />
) : (
<LiteContextContent contextData={contextData} session={session} />
)
)}
{!contextLoading && !contextError && !contextData && !session.path && (
<div className="flex flex-col items-center justify-center py-8 text-center">