diff --git a/ccw/frontend/src/components/cli-viewer/ContentArea.tsx b/ccw/frontend/src/components/cli-viewer/ContentArea.tsx index 3840afc0..e7781158 100644 --- a/ccw/frontend/src/components/cli-viewer/ContentArea.tsx +++ b/ccw/frontend/src/components/cli-viewer/ContentArea.tsx @@ -32,7 +32,7 @@ function EmptyTabState() { const { formatMessage } = useIntl(); return ( -
+

@@ -56,7 +56,7 @@ function ExecutionNotFoundState({ executionId }: { executionId: string }) { const { formatMessage } = useIntl(); return ( -

+

@@ -113,7 +113,7 @@ function CliOutputDisplay({ execution, executionId }: { execution: CliExecutionS if (!execution.output || execution.output.length === 0) { return ( -

+

@@ -185,7 +185,7 @@ export function ContentArea({ paneId, className }: ContentAreaProps) { return (

{ - onSizeChange(sizes); - }, - [onSizeChange] - ); - // Check if all panes in this group exist const validChildren = useMemo(() => { return group.children.filter(child => { @@ -71,7 +64,7 @@ function LayoutGroupRenderer({ group, minSize, onSizeChange }: LayoutGroupRender {validChildren.map((child, index) => ( diff --git a/ccw/frontend/src/components/shared/CliStreamMonitor/MonitorBody/index.tsx b/ccw/frontend/src/components/shared/CliStreamMonitor/MonitorBody/index.tsx index 40c66ae4..2e66b0bc 100644 --- a/ccw/frontend/src/components/shared/CliStreamMonitor/MonitorBody/index.tsx +++ b/ccw/frontend/src/components/shared/CliStreamMonitor/MonitorBody/index.tsx @@ -104,14 +104,12 @@ function MonitorBodyComponent( return (
-
- {children} - {/* Anchor for scroll to bottom */} -
-
+ {children} + {/* Anchor for scroll to bottom */} +
{/* Show scroll button when user is not at bottom */} {showScrollButton && isUserScrolling && ( diff --git a/ccw/frontend/src/components/shared/TaskDrawer.tsx b/ccw/frontend/src/components/shared/TaskDrawer.tsx index a70693b7..c01d0fe1 100644 --- a/ccw/frontend/src/components/shared/TaskDrawer.tsx +++ b/ccw/frontend/src/components/shared/TaskDrawer.tsx @@ -82,15 +82,16 @@ export function TaskDrawer({ task, isOpen, onClose }: TaskDrawerProps) { return () => window.removeEventListener('keydown', handleEsc); }, [isOpen, onClose]); - if (!task || !isOpen) { - return null; - } - // 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( - () => normalizeTask(task as unknown as Record), + () => task ? normalizeTask(task as unknown as Record) : null, [task], ); + + if (!task || !isOpen || !nt) { + return null; + } const taskId = nt.task_id || 'N/A'; const taskTitle = nt.title || 'Untitled Task'; const taskDescription = nt.description; diff --git a/ccw/frontend/src/lib/api.ts b/ccw/frontend/src/lib/api.ts index 73d9e914..5d188a1b 100644 --- a/ccw/frontend/src/lib/api.ts +++ b/ccw/frontend/src/lib/api.ts @@ -2173,6 +2173,75 @@ export interface LiteTaskSession { status?: string; createdAt?: 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; + solutions: Solution[]; + convergence: SynthesisConvergence; + cross_verification: SynthesisCrossVerification; + clarification_questions: string[]; + user_feedback_incorporated?: string; } export interface LiteTasksResponse { diff --git a/ccw/frontend/src/pages/LiteTasksPage.tsx b/ccw/frontend/src/pages/LiteTasksPage.tsx index b8875b8a..5c45b914 100644 --- a/ccw/frontend/src/pages/LiteTasksPage.tsx +++ b/ccw/frontend/src/pages/LiteTasksPage.tsx @@ -30,6 +30,17 @@ import { Clock, AlertCircle, FileCode, + ThumbsUp, + ThumbsDown, + Target, + GitCompare, + HelpCircle, + Cpu, + Timer, + Sparkles, + Layers, + CheckCheck, + ArrowRight, } from 'lucide-react'; import { useLiteTasks } from '@/hooks/useLiteTasks'; import { Button } from '@/components/ui/Button'; @@ -37,7 +48,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 } from '@/lib/api'; +import { fetchLiteSessionContext, type LiteTask, type LiteTaskSession, type LiteSessionContext, type RoundSynthesis } from '@/lib/api'; import { LiteContextContent } from '@/components/lite-tasks/LiteContextContent'; import { useNavigate } from 'react-router-dom'; @@ -532,25 +543,36 @@ function ExpandedMultiCliPanel({ {/* Discussion Tab */} {activeTab === 'discussion' && (
- - -
- -

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

- {roundCount} {formatMessage({ id: 'liteTasks.rounds' })} -
-

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

- {goal && ( -
-

{goal}

+ {/* Rounds Detail */} + {session.rounds && session.rounds.length > 0 ? ( + session.rounds.map((round, idx) => ( + + )) + ) : ( + + +
+ +

+ {formatMessage({ id: 'liteTasks.multiCli.discussionRounds' })} +

+ {roundCount} {formatMessage({ id: 'liteTasks.rounds' })}
- )} -
-
+

+ {formatMessage({ id: 'liteTasks.multiCli.discussionDescription' })} +

+ {goal && ( +
+

{goal}

+
+ )} + + + )}
)} diff --git a/ccw/src/core/lite-scanner.ts b/ccw/src/core/lite-scanner.ts index fff457f8..4259240f 100644 --- a/ccw/src/core/lite-scanner.ts +++ b/ccw/src/core/lite-scanner.ts @@ -118,6 +118,9 @@ interface LiteTaskDetail { explorations: unknown[]; clarifications: unknown | null; 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), explorations, clarifications, + // Multi-cli-plan specific fields + rounds: syntheses, + latestSynthesis, }; return detail;