mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
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:
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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();
|
||||
|
||||
136
ccw/frontend/src/components/terminal-dashboard/BottomPanel.tsx
Normal file
136
ccw/frontend/src/components/terminal-dashboard/BottomPanel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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) => (
|
||||
|
||||
Reference in New Issue
Block a user