feat: Enhance multi-cli-plan support with new synthesis types and update related components

This commit is contained in:
catlog22
2026-02-14 23:21:07 +08:00
parent 3a9a66aa3b
commit 0cfee90182
7 changed files with 132 additions and 43 deletions

View File

@@ -32,7 +32,7 @@ function EmptyTabState() {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
return ( return (
<div className="h-full flex flex-col items-center justify-center text-muted-foreground gap-4"> <div className="flex-1 flex flex-col items-center justify-center text-muted-foreground gap-4">
<Terminal className="h-12 w-12 opacity-30" /> <Terminal className="h-12 w-12 opacity-30" />
<div className="text-center"> <div className="text-center">
<p className="text-sm font-medium"> <p className="text-sm font-medium">
@@ -56,7 +56,7 @@ function ExecutionNotFoundState({ executionId }: { executionId: string }) {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
return ( return (
<div className="h-full flex flex-col items-center justify-center text-muted-foreground gap-4"> <div className="flex-1 flex flex-col items-center justify-center text-muted-foreground gap-4">
<Terminal className="h-12 w-12 opacity-30" /> <Terminal className="h-12 w-12 opacity-30" />
<div className="text-center"> <div className="text-center">
<p className="text-sm font-medium"> <p className="text-sm font-medium">
@@ -113,7 +113,7 @@ function CliOutputDisplay({ execution, executionId }: { execution: CliExecutionS
if (!execution.output || execution.output.length === 0) { if (!execution.output || execution.output.length === 0) {
return ( return (
<div className="h-full flex flex-col items-center justify-center text-muted-foreground gap-4"> <div className="flex-1 flex flex-col items-center justify-center text-muted-foreground gap-4">
<Terminal className="h-12 w-12 opacity-30" /> <Terminal className="h-12 w-12 opacity-30" />
<div className="text-center"> <div className="text-center">
<p className="text-sm"> <p className="text-sm">
@@ -185,7 +185,7 @@ export function ContentArea({ paneId, className }: ContentAreaProps) {
return ( return (
<div <div
className={cn( className={cn(
'flex-1 overflow-hidden', 'flex-1 min-h-0 flex flex-col overflow-hidden',
'bg-background', 'bg-background',
className className
)} )}

View File

@@ -3,7 +3,7 @@
// ======================================== // ========================================
// Manages allotment-based split panes for CLI viewer // Manages allotment-based split panes for CLI viewer
import { useCallback, useMemo, useRef, useEffect } from 'react'; import { useMemo, useRef, useEffect, useCallback } from 'react';
import { Allotment } from 'allotment'; import { Allotment } from 'allotment';
import 'allotment/dist/style.css'; import 'allotment/dist/style.css';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
@@ -46,13 +46,6 @@ function isPaneId(child: PaneId | AllotmentLayoutGroup): child is PaneId {
function LayoutGroupRenderer({ group, minSize, onSizeChange }: LayoutGroupRendererProps) { function LayoutGroupRenderer({ group, minSize, onSizeChange }: LayoutGroupRendererProps) {
const panes = useViewerPanes(); const panes = useViewerPanes();
const handleChange = useCallback(
(sizes: number[]) => {
onSizeChange(sizes);
},
[onSizeChange]
);
// Check if all panes in this group exist // Check if all panes in this group exist
const validChildren = useMemo(() => { const validChildren = useMemo(() => {
return group.children.filter(child => { return group.children.filter(child => {
@@ -71,7 +64,7 @@ function LayoutGroupRenderer({ group, minSize, onSizeChange }: LayoutGroupRender
<Allotment <Allotment
vertical={group.direction === 'vertical'} vertical={group.direction === 'vertical'}
defaultSizes={group.sizes} defaultSizes={group.sizes}
onChange={handleChange} onChange={onSizeChange}
className="h-full" className="h-full"
> >
{validChildren.map((child, index) => ( {validChildren.map((child, index) => (

View File

@@ -104,14 +104,12 @@ function MonitorBodyComponent(
return ( return (
<div <div
ref={containerRef} ref={containerRef}
className={cn('flex-1 overflow-y-auto bg-background relative', className)} className={cn('flex-1 min-h-0 overflow-y-auto bg-background relative', className)}
onScroll={handleScroll} onScroll={handleScroll}
> >
<div className="h-full"> {children}
{children} {/* Anchor for scroll to bottom */}
{/* Anchor for scroll to bottom */} <div ref={logsEndRef} />
<div ref={logsEndRef} />
</div>
{/* Show scroll button when user is not at bottom */} {/* Show scroll button when user is not at bottom */}
{showScrollButton && isUserScrolling && ( {showScrollButton && isUserScrolling && (

View File

@@ -82,15 +82,16 @@ export function TaskDrawer({ task, isOpen, onClose }: TaskDrawerProps) {
return () => window.removeEventListener('keydown', handleEsc); return () => window.removeEventListener('keydown', handleEsc);
}, [isOpen, onClose]); }, [isOpen, onClose]);
if (!task || !isOpen) {
return null;
}
// Normalize task to unified flat format (handles old nested, new flat, and raw LiteTask/TaskData) // Normalize task to unified flat format (handles old nested, new flat, and raw LiteTask/TaskData)
// MUST be called before early return to satisfy React hooks rules
const nt = React.useMemo( const nt = React.useMemo(
() => normalizeTask(task as unknown as Record<string, unknown>), () => task ? normalizeTask(task as unknown as Record<string, unknown>) : null,
[task], [task],
); );
if (!task || !isOpen || !nt) {
return null;
}
const taskId = nt.task_id || 'N/A'; const taskId = nt.task_id || 'N/A';
const taskTitle = nt.title || 'Untitled Task'; const taskTitle = nt.title || 'Untitled Task';
const taskDescription = nt.description; const taskDescription = nt.description;

View File

@@ -2173,6 +2173,75 @@ export interface LiteTaskSession {
status?: string; status?: string;
createdAt?: string; createdAt?: string;
updatedAt?: string; updatedAt?: string;
// Multi-cli-plan specific fields
rounds?: RoundSynthesis[];
}
// Multi-cli-plan synthesis types
export interface SolutionFileAction {
file: string;
line: number;
action: 'modify' | 'create' | 'delete';
}
export interface SolutionTask {
id: string;
name: string;
depends_on: string[];
files: SolutionFileAction[];
key_point: string | null;
}
export interface Solution {
name: string;
source_cli: string[];
feasibility: number;
effort: string;
risk: string;
summary: string;
pros: string[];
cons: string[];
affected_files: SolutionFileAction[];
implementation_plan: {
approach: string;
tasks: SolutionTask[];
execution_flow: string;
milestones: string[];
};
dependencies: {
internal: string[];
external: string[];
};
technical_concerns: string[];
}
export interface SynthesisConvergence {
score: number;
new_insights: boolean;
recommendation: 'converged' | 'continue' | 'user_input_needed';
rationale: string;
}
export interface SynthesisCrossVerification {
agreements: string[];
disagreements: Array<{
topic: string;
gemini: string;
codex: string;
resolution: string | null;
}>;
resolution: string;
}
export interface RoundSynthesis {
round: number;
timestamp: string;
cli_executions: Record<string, { status: string; duration_ms: number; model: string }>;
solutions: Solution[];
convergence: SynthesisConvergence;
cross_verification: SynthesisCrossVerification;
clarification_questions: string[];
user_feedback_incorporated?: string;
} }
export interface LiteTasksResponse { export interface LiteTasksResponse {

View File

@@ -30,6 +30,17 @@ import {
Clock, Clock,
AlertCircle, AlertCircle,
FileCode, FileCode,
ThumbsUp,
ThumbsDown,
Target,
GitCompare,
HelpCircle,
Cpu,
Timer,
Sparkles,
Layers,
CheckCheck,
ArrowRight,
} from 'lucide-react'; } from 'lucide-react';
import { useLiteTasks } from '@/hooks/useLiteTasks'; import { useLiteTasks } from '@/hooks/useLiteTasks';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
@@ -37,7 +48,7 @@ import { Badge } from '@/components/ui/Badge';
import { Card, CardContent } from '@/components/ui/Card'; import { Card, CardContent } from '@/components/ui/Card';
import { TabsNavigation } from '@/components/ui/TabsNavigation'; import { TabsNavigation } from '@/components/ui/TabsNavigation';
import { TaskDrawer } from '@/components/shared/TaskDrawer'; import { TaskDrawer } from '@/components/shared/TaskDrawer';
import { fetchLiteSessionContext, type LiteTask, type LiteTaskSession, type LiteSessionContext } from '@/lib/api'; import { fetchLiteSessionContext, type LiteTask, type LiteTaskSession, type LiteSessionContext, type RoundSynthesis } from '@/lib/api';
import { LiteContextContent } from '@/components/lite-tasks/LiteContextContent'; import { LiteContextContent } from '@/components/lite-tasks/LiteContextContent';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
@@ -532,25 +543,36 @@ function ExpandedMultiCliPanel({
{/* Discussion Tab */} {/* Discussion Tab */}
{activeTab === 'discussion' && ( {activeTab === 'discussion' && (
<div className="space-y-3"> <div className="space-y-3">
<Card className="border-border"> {/* Rounds Detail */}
<CardContent className="p-4"> {session.rounds && session.rounds.length > 0 ? (
<div className="flex items-center gap-2 mb-3"> session.rounds.map((round, idx) => (
<MessagesSquare className="h-5 w-5 text-primary" /> <RoundDetailCard
<h4 className="font-medium text-foreground"> key={round.round || idx}
{formatMessage({ id: 'liteTasks.multiCli.discussionRounds' })} round={round}
</h4> isLast={idx === session.rounds!.length - 1}
<Badge variant="secondary" className="text-xs">{roundCount} {formatMessage({ id: 'liteTasks.rounds' })}</Badge> />
</div> ))
<p className="text-sm text-muted-foreground"> ) : (
{formatMessage({ id: 'liteTasks.multiCli.discussionDescription' })} <Card className="border-border">
</p> <CardContent className="p-4">
{goal && ( <div className="flex items-center gap-2 mb-3">
<div className="mt-3 p-3 bg-muted/50 rounded-lg"> <MessagesSquare className="h-5 w-5 text-primary" />
<p className="text-sm text-foreground">{goal}</p> <h4 className="font-medium text-foreground">
{formatMessage({ id: 'liteTasks.multiCli.discussionRounds' })}
</h4>
<Badge variant="secondary" className="text-xs">{roundCount} {formatMessage({ id: 'liteTasks.rounds' })}</Badge>
</div> </div>
)} <p className="text-sm text-muted-foreground">
</CardContent> {formatMessage({ id: 'liteTasks.multiCli.discussionDescription' })}
</Card> </p>
{goal && (
<div className="mt-3 p-3 bg-muted/50 rounded-lg">
<p className="text-sm text-foreground">{goal}</p>
</div>
)}
</CardContent>
</Card>
)}
</div> </div>
)} )}

View File

@@ -118,6 +118,9 @@ interface LiteTaskDetail {
explorations: unknown[]; explorations: unknown[];
clarifications: unknown | null; clarifications: unknown | null;
diagnoses?: Diagnoses; diagnoses?: Diagnoses;
// Multi-cli-plan specific
rounds?: RoundSynthesis[];
latestSynthesis?: RoundSynthesis | null;
} }
/** /**
@@ -923,6 +926,9 @@ export async function getLiteTaskDetail(workflowDir: string, type: string, sessi
tasks: extractTasksFromSyntheses(syntheses), tasks: extractTasksFromSyntheses(syntheses),
explorations, explorations,
clarifications, clarifications,
// Multi-cli-plan specific fields
rounds: syntheses,
latestSynthesis,
}; };
return detail; return detail;