feat: Implement workflow phases for test generation and execution

- Added Phase 1: Session Start to detect input mode and create test workflow session.
- Added Phase 2: Test Context Gather to gather test context via coverage analysis or codebase scan.
- Added Phase 3: Test Concept Enhanced to analyze test requirements using Gemini and generate multi-layered test requirements.
- Added Phase 4: Test Task Generate to create test-specific tasks based on analysis results.
- Added Phase 5: Test Cycle Execute to manage iterative test execution and fix cycles with adaptive strategies.
- Introduced BottomPanel component for terminal dashboard with Queue and Inspector tabs.
This commit is contained in:
catlog22
2026-02-14 21:35:55 +08:00
parent 0d805efe87
commit d535ab4749
27 changed files with 2004 additions and 2363 deletions

View File

@@ -31,6 +31,7 @@ interface FlowchartNodeData extends Record<string, unknown> {
type: 'pre-analysis' | 'implementation' | 'section';
dependsOn?: string[];
status?: 'pending' | 'in_progress' | 'completed' | 'blocked' | 'skipped';
showStepStatus?: boolean;
}
// Status icon component

View File

@@ -11,7 +11,7 @@ import { Badge } from '../ui/Badge';
import { Button } from '../ui/Button';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '../ui/Tabs';
import type { NormalizedTask, LiteTask } from '@/lib/api';
import { buildFlowControl } from '@/lib/api';
import { buildFlowControl, normalizeTask } from '@/lib/api';
import type { TaskData } from '@/types/store';
// ========== Types ==========
@@ -86,8 +86,11 @@ export function TaskDrawer({ task, isOpen, onClose }: TaskDrawerProps) {
return null;
}
// Use NormalizedTask fields (works for both old nested and new flat formats)
const nt = task as NormalizedTask;
// Normalize task to unified flat format (handles old nested, new flat, and raw LiteTask/TaskData)
const nt = React.useMemo(
() => normalizeTask(task as unknown as Record<string, unknown>),
[task],
);
const taskId = nt.task_id || 'N/A';
const taskTitle = nt.title || 'Untitled Task';
const taskDescription = nt.description;
@@ -103,6 +106,13 @@ export function TaskDrawer({ task, isOpen, onClose }: TaskDrawerProps) {
const taskFiles = nt.files || flowControl?.target_files || [];
const taskScope = nt.scope;
// Detect if task supports status tracking (new format has explicit status/status_history)
const rawData = nt._raw as Record<string, unknown> | undefined;
const sourceRaw = (rawData?._raw as Record<string, unknown>) || rawData;
const hasStatusTracking = sourceRaw
? (sourceRaw.status !== undefined || sourceRaw.status_history !== undefined)
: false;
const statusConfig = taskStatusConfig[taskStatus] || taskStatusConfig.pending;
const StatusIcon = statusConfig.icon;
@@ -135,10 +145,12 @@ export function TaskDrawer({ task, isOpen, onClose }: TaskDrawerProps) {
<div className="flex-1 min-w-0 mr-4">
<div className="flex items-center gap-2 mb-2">
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-mono font-semibold bg-primary/10 text-primary border border-primary/20">{taskId}</span>
<Badge variant={statusConfig.variant} className="gap-1">
<StatusIcon className="h-3 w-3" />
{formatMessage({ id: statusConfig.label })}
</Badge>
{hasStatusTracking && (
<Badge variant={statusConfig.variant} className="gap-1">
<StatusIcon className="h-3 w-3" />
{formatMessage({ id: statusConfig.label })}
</Badge>
)}
</div>
<h2 id="drawer-title" className="text-lg font-semibold text-foreground">
{taskTitle}
@@ -352,9 +364,9 @@ export function TaskDrawer({ task, isOpen, onClose }: TaskDrawerProps) {
{/* Empty State */}
{!taskDescription &&
!(task as LiteTask).meta?.scope &&
!((task as LiteTask).context?.acceptance?.length) &&
!((task as LiteTask).context?.focus_paths?.length) &&
!taskScope &&
!acceptanceCriteria.length &&
!focusPaths.length &&
!(flowControl?.pre_analysis?.length) &&
!(flowControl?.implementation_approach?.length) && (
<div className="text-center py-12">

View File

@@ -1,10 +1,9 @@
// ========================================
// BottomInspector Component
// ========================================
// Collapsible bottom panel showing the full association chain
// (Issue -> Queue -> Session) for the currently selected entity.
// Consumes issueQueueIntegrationStore for association chain data
// and useAssociationHighlight context for the highlighted chain.
// Association chain visualization (Issue -> Queue -> Session).
// Exports InspectorContent (pure content) for embedding in BottomPanel,
// and BottomInspector (legacy standalone collapsible wrapper).
import { useState, useCallback, useMemo } from 'react';
import { useIntl } from 'react-intl';
@@ -90,7 +89,86 @@ function formatTimestamp(ts: string): string {
}
}
// ========== Main Component ==========
// ========== InspectorContent (Pure content, no collapsible wrapper) ==========
export function InspectorContent() {
const { formatMessage } = useIntl();
const associationChain = useIssueQueueIntegrationStore(selectAssociationChain);
const { chain: highlightedChain } = useAssociationHighlight();
const activeChain = highlightedChain ?? associationChain;
const chainDetails = useMemo(() => {
if (!activeChain) return null;
const executions = Object.values(useQueueExecutionStore.getState().executions);
const sessions = useCliSessionStore.getState().sessions;
let queueStatus: string | undefined;
let executionTimestamp: string | undefined;
if (activeChain.queueItemId) {
const exec = executions.find((e) => e.queueItemId === activeChain.queueItemId);
if (exec) {
queueStatus = exec.status;
executionTimestamp = exec.startedAt;
}
}
let sessionStatus: string | undefined;
let sessionTimestamp: string | undefined;
if (activeChain.sessionId) {
const session = sessions[activeChain.sessionId];
if (session) {
sessionStatus = 'active';
sessionTimestamp = session.createdAt;
}
}
return { queueStatus, executionTimestamp, sessionStatus, sessionTimestamp };
}, [activeChain]);
const hasChain = activeChain !== null;
return (
<div className="h-full overflow-y-auto px-4 py-3">
{hasChain ? (
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">
{formatMessage({ id: 'terminalDashboard.inspector.associationChain' })}
</p>
<div className="flex items-center gap-1 flex-wrap">
<ChainNode
icon={AlertCircle}
label="Issue"
entityId={activeChain.issueId}
/>
<ChainNode
icon={ListChecks}
label="Queue"
entityId={activeChain.queueItemId}
status={chainDetails?.queueStatus}
timestamp={chainDetails?.executionTimestamp}
/>
<ChainNode
icon={Terminal}
label="Session"
entityId={activeChain.sessionId}
status={chainDetails?.sessionStatus}
timestamp={chainDetails?.sessionTimestamp}
isLast
/>
</div>
</div>
) : (
<p className="text-xs text-muted-foreground">
{formatMessage({ id: 'terminalDashboard.inspector.noSelection' })}
</p>
)}
</div>
);
}
// ========== Legacy Standalone Component ==========
export function BottomInspector() {
const { formatMessage } = useIntl();

View File

@@ -0,0 +1,136 @@
// ========================================
// BottomPanel Component
// ========================================
// Full-width collapsible bottom panel with Queue + Inspector tabs.
// Replaces the separate BottomInspector + middle-column QueuePanel layout.
// Queue tab shows inline count badge; Inspector tab shows chain indicator.
import { useState, useCallback, useMemo } from 'react';
import { useIntl } from 'react-intl';
import { ListChecks, Info, ChevronDown, ChevronUp } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Badge } from '@/components/ui/Badge';
import { QueuePanel } from './QueuePanel';
import { InspectorContent } from './BottomInspector';
import { useIssueQueue } from '@/hooks/useIssues';
import {
useIssueQueueIntegrationStore,
selectAssociationChain,
} from '@/stores/issueQueueIntegrationStore';
// ========== Types ==========
type TabId = 'queue' | 'inspector';
// ========== Component ==========
export function BottomPanel() {
const { formatMessage } = useIntl();
const [isOpen, setIsOpen] = useState(false);
const [activeTab, setActiveTab] = useState<TabId>('queue');
const queueQuery = useIssueQueue();
const associationChain = useIssueQueueIntegrationStore(selectAssociationChain);
// Count queue items for badge
const queueCount = useMemo(() => {
if (!queueQuery.data) return 0;
const grouped = queueQuery.data.grouped_items ?? {};
let count = 0;
for (const items of Object.values(grouped)) {
count += items.length;
}
return count;
}, [queueQuery.data]);
const hasChain = associationChain !== null;
const toggle = useCallback(() => {
setIsOpen((prev) => !prev);
}, []);
const handleTabClick = useCallback((tab: TabId) => {
setActiveTab(tab);
setIsOpen(true);
}, []);
return (
<div
className={cn(
'border-t border-border bg-muted/30 shrink-0 transition-all duration-200',
)}
>
{/* Tab bar (always visible, ~36px) */}
<div className="flex items-center gap-0 shrink-0">
{/* Queue tab */}
<button
onClick={() => handleTabClick('queue')}
className={cn(
'flex items-center gap-1.5 px-3 py-1.5 text-xs transition-colors border-b-2',
activeTab === 'queue' && isOpen
? 'border-b-primary text-foreground font-medium'
: 'border-b-transparent text-muted-foreground hover:text-foreground',
)}
>
<ListChecks className="w-3.5 h-3.5" />
{formatMessage({ id: 'terminalDashboard.bottomPanel.queueTab' })}
{queueCount > 0 && (
<Badge variant="info" className="text-[10px] px-1.5 py-0 ml-0.5">
{queueCount}
</Badge>
)}
</button>
{/* Inspector tab */}
<button
onClick={() => handleTabClick('inspector')}
className={cn(
'flex items-center gap-1.5 px-3 py-1.5 text-xs transition-colors border-b-2',
activeTab === 'inspector' && isOpen
? 'border-b-primary text-foreground font-medium'
: 'border-b-transparent text-muted-foreground hover:text-foreground',
)}
>
<Info className="w-3.5 h-3.5" />
{formatMessage({ id: 'terminalDashboard.bottomPanel.inspectorTab' })}
{hasChain && (
<span className="ml-1 w-2 h-2 rounded-full bg-primary shrink-0" />
)}
</button>
{/* Collapse/expand toggle at right */}
<button
onClick={toggle}
className="ml-auto px-3 py-1.5 text-muted-foreground hover:text-foreground transition-colors"
title={formatMessage({
id: isOpen
? 'terminalDashboard.bottomPanel.collapse'
: 'terminalDashboard.bottomPanel.expand',
})}
>
{isOpen ? (
<ChevronDown className="w-3.5 h-3.5" />
) : (
<ChevronUp className="w-3.5 h-3.5" />
)}
</button>
</div>
{/* Collapsible content area */}
<div
className={cn(
'overflow-hidden transition-all duration-200',
isOpen ? 'max-h-[280px] opacity-100' : 'max-h-0 opacity-0',
)}
>
<div className="h-[280px] border-t border-border/50">
{activeTab === 'queue' ? (
<QueuePanel embedded />
) : (
<InspectorContent />
)}
</div>
</div>
</div>
);
}

View File

@@ -131,12 +131,23 @@ function QueueItemRow({
// ========== Empty State ==========
function QueueEmptyState() {
function QueueEmptyState({ compact = false }: { compact?: boolean }) {
const { formatMessage } = useIntl();
if (compact) {
return (
<div className="flex items-center gap-2 text-muted-foreground px-3 py-2">
<ListChecks className="h-4 w-4 opacity-30 shrink-0" />
<span className="text-xs">{formatMessage({ id: 'terminalDashboard.queuePanel.noItems' })}</span>
<span className="text-[10px] opacity-70">{formatMessage({ id: 'terminalDashboard.queuePanel.noItemsDesc' })}</span>
</div>
);
}
return (
<div className="flex-1 flex items-center justify-center text-muted-foreground p-4">
<div className="text-center">
<ListChecks className="h-10 w-10 mx-auto mb-3 opacity-40" />
<ListChecks className="h-6 w-6 mx-auto mb-1.5 opacity-30" />
<p className="text-sm">{formatMessage({ id: 'terminalDashboard.queuePanel.noItems' })}</p>
<p className="text-xs mt-1 opacity-70">
{formatMessage({ id: 'terminalDashboard.queuePanel.noItemsDesc' })}
@@ -163,7 +174,7 @@ function QueueErrorState({ error }: { error: Error }) {
// ========== Main Component ==========
export function QueuePanel() {
export function QueuePanel({ embedded = false }: { embedded?: boolean }) {
const { formatMessage } = useIntl();
const queueQuery = useIssueQueue();
const associationChain = useIssueQueueIntegrationStore(selectAssociationChain);
@@ -200,12 +211,14 @@ export function QueuePanel() {
if (queueQuery.isLoading) {
return (
<div className="flex flex-col h-full">
<div className="px-3 py-2 border-b border-border shrink-0">
<h3 className="text-sm font-semibold flex items-center gap-2">
<ListChecks className="w-4 h-4" />
{formatMessage({ id: 'terminalDashboard.queuePanel.title' })}
</h3>
</div>
{!embedded && (
<div className="px-3 py-2 border-b border-border shrink-0">
<h3 className="text-sm font-semibold flex items-center gap-2">
<ListChecks className="w-4 h-4" />
{formatMessage({ id: 'terminalDashboard.queuePanel.title' })}
</h3>
</div>
)}
<div className="flex-1 flex items-center justify-center">
<Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />
</div>
@@ -217,12 +230,14 @@ export function QueuePanel() {
if (queueQuery.error) {
return (
<div className="flex flex-col h-full">
<div className="px-3 py-2 border-b border-border shrink-0">
<h3 className="text-sm font-semibold flex items-center gap-2">
<ListChecks className="w-4 h-4" />
{formatMessage({ id: 'terminalDashboard.queuePanel.title' })}
</h3>
</div>
{!embedded && (
<div className="px-3 py-2 border-b border-border shrink-0">
<h3 className="text-sm font-semibold flex items-center gap-2">
<ListChecks className="w-4 h-4" />
{formatMessage({ id: 'terminalDashboard.queuePanel.title' })}
</h3>
</div>
)}
<QueueErrorState error={queueQuery.error} />
</div>
);
@@ -230,23 +245,25 @@ export function QueuePanel() {
return (
<div className="flex flex-col h-full">
{/* Header with flow indicator */}
<div className="px-3 py-2 border-b border-border shrink-0 flex items-center justify-between">
<h3 className="text-sm font-semibold flex items-center gap-2">
<ArrowDownToLine className="w-4 h-4 text-muted-foreground" />
<ListChecks className="w-4 h-4" />
{formatMessage({ id: 'terminalDashboard.queuePanel.title' })}
</h3>
{activeCount > 0 && (
<Badge variant="info" className="text-[10px] px-1.5 py-0">
{activeCount}
</Badge>
)}
</div>
{/* Header with flow indicator (hidden when embedded) */}
{!embedded && (
<div className="px-3 py-2 border-b border-border shrink-0 flex items-center justify-between">
<h3 className="text-sm font-semibold flex items-center gap-2">
<ArrowDownToLine className="w-4 h-4 text-muted-foreground" />
<ListChecks className="w-4 h-4" />
{formatMessage({ id: 'terminalDashboard.queuePanel.title' })}
</h3>
{activeCount > 0 && (
<Badge variant="info" className="text-[10px] px-1.5 py-0">
{activeCount}
</Badge>
)}
</div>
)}
{/* Queue Item List */}
{allItems.length === 0 ? (
<QueueEmptyState />
<QueueEmptyState compact={embedded} />
) : (
<div className="flex-1 min-h-0 overflow-y-auto p-1.5 space-y-0.5">
{allItems.map((item) => (