feat: update CLI roadmap planning agent to generate roadmap.md instead of execution-plan.json and issues.jsonl; enhance QueuePanel with orchestrator tab and status management; improve issue listing with summary output

This commit is contained in:
catlog22
2026-02-17 23:43:53 +08:00
parent b3f420ac31
commit 4a5f7ce7f7
8 changed files with 422 additions and 236 deletions

View File

@@ -1,13 +1,12 @@
// ========================================
// QueuePanel Component
// ========================================
// Queue list panel for the terminal dashboard middle column.
// Consumes existing useIssueQueue() React Query hook for queue data
// and bridges queueExecutionStore for execution status per item.
// Integrates with issueQueueIntegrationStore for association chain
// highlighting and selection state.
// Queue list panel for the terminal dashboard with tab switching.
// Tab 1 (Queue): Issue queue items from useIssueQueue() hook.
// Tab 2 (Orchestrator): Active orchestration plans from orchestratorStore.
// Integrates with issueQueueIntegrationStore for association chain.
import { useMemo, useCallback } from 'react';
import { useState, useMemo, useCallback, memo } from 'react';
import { useIntl } from 'react-intl';
import {
ListChecks,
@@ -20,8 +19,18 @@ import {
Zap,
Ban,
Terminal,
Workflow,
Circle,
CheckCircle2,
SkipForward,
Pause,
Play,
Square,
RotateCcw,
AlertCircle,
} from 'lucide-react';
import { Badge } from '@/components/ui/Badge';
import { Button } from '@/components/ui/Button';
import { cn } from '@/lib/utils';
import { useIssueQueue } from '@/hooks/useIssues';
import {
@@ -32,9 +41,20 @@ import {
useQueueExecutionStore,
selectByQueueItem,
} from '@/stores/queueExecutionStore';
import {
useOrchestratorStore,
selectActivePlans,
selectActivePlanCount,
type OrchestrationRunState,
} from '@/stores/orchestratorStore';
import type { StepStatus, OrchestrationStatus } from '@/types/orchestrator';
import type { QueueItem } from '@/lib/api';
// ========== Status Config ==========
// ========== Tab Type ==========
type QueueTab = 'queue' | 'orchestrator';
// ========== Queue Tab: Status Config ==========
type QueueItemStatus = QueueItem['status'];
@@ -51,7 +71,7 @@ const STATUS_CONFIG: Record<QueueItemStatus, {
blocked: { variant: 'outline', icon: Ban, label: 'Blocked' },
};
// ========== Queue Item Row ==========
// ========== Queue Tab: Item Row ==========
function QueueItemRow({
item,
@@ -66,7 +86,6 @@ function QueueItemRow({
const config = STATUS_CONFIG[item.status] ?? STATUS_CONFIG.pending;
const StatusIcon = config.icon;
// Bridge to queueExecutionStore for execution status
const executions = useQueueExecutionStore(selectByQueueItem(item.item_id));
const activeExec = executions.find((e) => e.status === 'running') ?? executions[0];
@@ -129,58 +148,14 @@ function QueueItemRow({
);
}
// ========== Empty State ==========
// ========== Queue Tab: Content ==========
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-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' })}
</p>
</div>
</div>
);
}
// ========== Error State ==========
function QueueErrorState({ error }: { error: Error }) {
const { formatMessage } = useIntl();
return (
<div className="flex-1 flex items-center justify-center text-destructive p-4">
<div className="text-center">
<AlertTriangle className="h-6 w-6 mx-auto mb-1.5 opacity-30" />
<p className="text-sm">{formatMessage({ id: 'terminalDashboard.queuePanel.error' })}</p>
<p className="text-xs mt-1 opacity-70">{error.message}</p>
</div>
</div>
);
}
// ========== Main Component ==========
export function QueuePanel({ embedded = false }: { embedded?: boolean }) {
function QueueTabContent({ embedded = false }: { embedded?: boolean }) {
const { formatMessage } = useIntl();
const queueQuery = useIssueQueue();
const associationChain = useIssueQueueIntegrationStore(selectAssociationChain);
const buildAssociationChain = useIssueQueueIntegrationStore((s) => s.buildAssociationChain);
// Flatten all queue items from grouped_items
const allItems = useMemo(() => {
if (!queueQuery.data) return [];
const grouped = queueQuery.data.grouped_items ?? {};
@@ -188,18 +163,10 @@ export function QueuePanel({ embedded = false }: { embedded?: boolean }) {
for (const group of Object.values(grouped)) {
items.push(...group);
}
// Sort by execution_order
items.sort((a, b) => a.execution_order - b.execution_order);
return items;
}, [queueQuery.data]);
// Count active items (pending + ready + executing)
const activeCount = useMemo(() => {
return allItems.filter(
(item) => item.status === 'pending' || item.status === 'ready' || item.status === 'executing'
).length;
}, [allItems]);
const handleSelect = useCallback(
(queueItemId: string) => {
buildAssociationChain(queueItemId, 'queue');
@@ -207,74 +174,341 @@ export function QueuePanel({ embedded = false }: { embedded?: boolean }) {
[buildAssociationChain]
);
// Loading state
if (queueQuery.isLoading) {
return (
<div className="flex flex-col h-full">
{!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 className="flex-1 flex items-center justify-center">
<Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />
</div>
);
}
if (queueQuery.error) {
return (
<div className="flex-1 flex items-center justify-center text-destructive p-4">
<div className="text-center">
<AlertTriangle className="h-6 w-6 mx-auto mb-1.5 opacity-30" />
<p className="text-sm">{formatMessage({ id: 'terminalDashboard.queuePanel.error' })}</p>
<p className="text-xs mt-1 opacity-70">{queueQuery.error.message}</p>
</div>
</div>
);
}
// Error state
if (queueQuery.error) {
if (allItems.length === 0) {
return (
<div className="flex flex-col h-full">
{!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 className="flex-1 flex items-center justify-center text-muted-foreground p-4">
<div className="text-center">
<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' })}
</p>
</div>
</div>
);
}
return (
<div className="flex flex-col h-full">
{/* 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>
)}
<div className="flex-1 min-h-0 overflow-y-auto p-1.5 space-y-0.5">
{allItems.map((item) => (
<QueueItemRow
key={item.item_id}
item={item}
isHighlighted={associationChain?.queueItemId === item.item_id}
onSelect={() => handleSelect(item.item_id)}
/>
))}
</div>
);
}
{/* Queue Item List */}
{allItems.length === 0 ? (
<QueueEmptyState compact={embedded} />
) : (
<div className="flex-1 min-h-0 overflow-y-auto p-1.5 space-y-0.5">
{allItems.map((item) => (
<QueueItemRow
key={item.item_id}
item={item}
isHighlighted={associationChain?.queueItemId === item.item_id}
onSelect={() => handleSelect(item.item_id)}
/>
))}
</div>
// ========== Orchestrator Tab: Status Badge ==========
const orchestratorStatusClass: Record<OrchestrationStatus, string> = {
pending: 'bg-muted text-muted-foreground border-border',
running: 'bg-primary/10 text-primary border-primary/50',
paused: 'bg-amber-500/10 text-amber-500 border-amber-500/50',
completed: 'bg-green-500/10 text-green-500 border-green-500/50',
failed: 'bg-destructive/10 text-destructive border-destructive/50',
cancelled: 'bg-muted text-muted-foreground border-border',
};
function OrchestratorStatusBadge({ status }: { status: OrchestrationStatus }) {
const { formatMessage } = useIntl();
return (
<span className={cn('px-2 py-0.5 rounded text-[10px] font-medium border', orchestratorStatusClass[status])}>
{formatMessage({ id: `orchestrator.status.${status}` })}
</span>
);
}
// ========== Orchestrator Tab: Step Icon ==========
function StepIcon({ status }: { status: StepStatus }) {
switch (status) {
case 'running':
return <Loader2 className="w-3.5 h-3.5 text-primary animate-spin" />;
case 'completed':
return <CheckCircle2 className="w-3.5 h-3.5 text-green-500" />;
case 'failed':
return <XCircle className="w-3.5 h-3.5 text-destructive" />;
case 'skipped':
return <SkipForward className="w-3.5 h-3.5 text-muted-foreground" />;
case 'paused':
return <Pause className="w-3.5 h-3.5 text-amber-500" />;
case 'cancelled':
return <Square className="w-3.5 h-3.5 text-muted-foreground" />;
default:
return <Circle className="w-3.5 h-3.5 text-muted-foreground" />;
}
}
// ========== Orchestrator Tab: Plan Controls ==========
function PlanControls({ planId, status, failedStepId }: {
planId: string;
status: OrchestrationStatus;
failedStepId: string | null;
}) {
const pauseOrchestration = useOrchestratorStore((s) => s.pauseOrchestration);
const resumeOrchestration = useOrchestratorStore((s) => s.resumeOrchestration);
const stopOrchestration = useOrchestratorStore((s) => s.stopOrchestration);
const retryStep = useOrchestratorStore((s) => s.retryStep);
const skipStep = useOrchestratorStore((s) => s.skipStep);
if (status === 'completed' || status === 'cancelled') return null;
const isPausedOnError = status === 'paused' && failedStepId !== null;
const isPausedByUser = status === 'paused' && failedStepId === null;
return (
<div className="flex items-center gap-1.5 mt-2">
{status === 'running' && (
<>
<Button variant="outline" size="sm" className="h-6 text-xs gap-1 px-2" onClick={() => pauseOrchestration(planId)}>
<Pause className="w-3 h-3" />
</Button>
<Button variant="destructive" size="sm" className="h-6 text-xs gap-1 px-2" onClick={() => stopOrchestration(planId)}>
<Square className="w-3 h-3" />
</Button>
</>
)}
{isPausedByUser && (
<>
<Button variant="outline" size="sm" className="h-6 text-xs gap-1 px-2" onClick={() => resumeOrchestration(planId)}>
<Play className="w-3 h-3" />
</Button>
<Button variant="destructive" size="sm" className="h-6 text-xs gap-1 px-2" onClick={() => stopOrchestration(planId)}>
<Square className="w-3 h-3" />
</Button>
</>
)}
{isPausedOnError && failedStepId && (
<>
<Button variant="outline" size="sm" className="h-6 text-xs gap-1 px-2" onClick={() => retryStep(planId, failedStepId)}>
<RotateCcw className="w-3 h-3" />
</Button>
<Button variant="outline" size="sm" className="h-6 text-xs gap-1 px-2" onClick={() => skipStep(planId, failedStepId)}>
<SkipForward className="w-3 h-3" />
</Button>
<Button variant="destructive" size="sm" className="h-6 text-xs gap-1 px-2" onClick={() => stopOrchestration(planId)}>
<Square className="w-3 h-3" />
</Button>
</>
)}
</div>
);
}
// ========== Orchestrator Tab: Plan Card ==========
const PlanCard = memo(function PlanCard({ runState }: { runState: OrchestrationRunState }) {
const { plan, status, stepStatuses, currentStepIndex } = runState;
const { completedCount, totalCount, progress } = useMemo(() => {
const statuses = Object.values(stepStatuses);
const total = statuses.length;
const completed = statuses.filter((s) => s.status === 'completed' || s.status === 'skipped').length;
return { completedCount: completed, totalCount: total, progress: total > 0 ? (completed / total) * 100 : 0 };
}, [stepStatuses]);
const failedStepId = useMemo(() => {
for (const [stepId, stepState] of Object.entries(stepStatuses)) {
if (stepState.status === 'failed') return stepId;
}
return null;
}, [stepStatuses]);
return (
<div className="border rounded-md border-border bg-card p-3">
<div className="flex items-center gap-2 mb-2">
<h4 className="text-xs font-semibold text-foreground truncate flex-1">{plan.name}</h4>
<OrchestratorStatusBadge status={status} />
<span className="text-[10px] text-muted-foreground tabular-nums shrink-0">
{completedCount}/{totalCount}
</span>
</div>
<div className="h-1.5 bg-muted rounded-full overflow-hidden mb-2">
<div
className={cn(
'h-full transition-all duration-300',
status === 'failed' && 'bg-destructive',
status === 'completed' && 'bg-green-500',
status === 'cancelled' && 'bg-muted-foreground',
(status === 'running' || status === 'pending') && 'bg-primary',
status === 'paused' && 'bg-amber-500',
)}
style={{ width: `${progress}%` }}
/>
</div>
<div className="space-y-0.5 max-h-48 overflow-y-auto">
{plan.steps.map((step, index) => {
const stepState = stepStatuses[step.id];
if (!stepState) return null;
const isCurrent = index === currentStepIndex && status === 'running';
return (
<div
key={step.id}
className={cn(
'flex items-center gap-2 px-2 py-1 rounded text-xs',
isCurrent && 'bg-primary/5',
stepState.status === 'failed' && 'bg-destructive/5',
)}
>
<StepIcon status={stepState.status} />
<span className={cn(
'truncate flex-1',
stepState.status === 'completed' && 'text-muted-foreground',
stepState.status === 'skipped' && 'text-muted-foreground line-through',
stepState.status === 'failed' && 'text-destructive',
)}>
{step.name}
</span>
{stepState.retryCount > 0 && (
<span className="text-[10px] text-muted-foreground">×{stepState.retryCount}</span>
)}
</div>
);
})}
</div>
{failedStepId && stepStatuses[failedStepId]?.error && (
<div className="flex items-start gap-1.5 mt-2 px-2">
<AlertCircle className="w-3 h-3 text-destructive shrink-0 mt-0.5" />
<span className="text-[10px] text-destructive/80 break-words">
{stepStatuses[failedStepId].error}
</span>
</div>
)}
<PlanControls planId={plan.id} status={status} failedStepId={failedStepId} />
</div>
);
});
// ========== Orchestrator Tab: Content ==========
function OrchestratorTabContent() {
const { formatMessage } = useIntl();
const activePlans = useOrchestratorStore(selectActivePlans);
const planEntries = useMemo(() => Object.entries(activePlans), [activePlans]);
if (planEntries.length === 0) {
return (
<div className="flex-1 flex items-center justify-center text-muted-foreground p-4">
<div className="text-center">
<Workflow className="h-6 w-6 mx-auto mb-1.5 opacity-30" />
<p className="text-sm">
{formatMessage({ id: 'terminalDashboard.orchestratorPanel.noPlans', defaultMessage: 'No active orchestrations' })}
</p>
<p className="text-xs mt-1 opacity-70">
{formatMessage({ id: 'terminalDashboard.orchestratorPanel.noPlansHint', defaultMessage: 'Run a flow from the Orchestrator to see progress here' })}
</p>
</div>
</div>
);
}
return (
<div className="flex-1 min-h-0 overflow-y-auto p-3 space-y-3">
{planEntries.map(([planId, runState]) => (
<PlanCard key={planId} runState={runState} />
))}
</div>
);
}
// ========== Main Component ==========
export function QueuePanel({ embedded = false }: { embedded?: boolean }) {
const { formatMessage } = useIntl();
const [activeTab, setActiveTab] = useState<QueueTab>('queue');
const orchestratorCount = useOrchestratorStore(selectActivePlanCount);
const queueQuery = useIssueQueue();
const queueActiveCount = useMemo(() => {
if (!queueQuery.data) return 0;
const grouped = queueQuery.data.grouped_items ?? {};
let count = 0;
for (const items of Object.values(grouped)) {
count += items.filter(
(item) => item.status === 'pending' || item.status === 'ready' || item.status === 'executing'
).length;
}
return count;
}, [queueQuery.data]);
return (
<div className="flex flex-col h-full">
{/* Tab bar */}
{!embedded && (
<div className="flex items-center border-b border-border shrink-0">
<button
type="button"
className={cn(
'flex-1 flex items-center justify-center gap-1.5 px-3 py-2 text-xs font-medium transition-colors',
activeTab === 'queue'
? 'text-foreground border-b-2 border-primary'
: 'text-muted-foreground hover:text-foreground'
)}
onClick={() => setActiveTab('queue')}
>
<ListChecks className="w-3.5 h-3.5" />
{formatMessage({ id: 'terminalDashboard.queuePanel.title' })}
{queueActiveCount > 0 && (
<Badge variant="secondary" className="text-[10px] px-1.5 py-0 ml-0.5">
{queueActiveCount}
</Badge>
)}
</button>
<button
type="button"
className={cn(
'flex-1 flex items-center justify-center gap-1.5 px-3 py-2 text-xs font-medium transition-colors',
activeTab === 'orchestrator'
? 'text-foreground border-b-2 border-primary'
: 'text-muted-foreground hover:text-foreground'
)}
onClick={() => setActiveTab('orchestrator')}
>
<Workflow className="w-3.5 h-3.5" />
{formatMessage({ id: 'terminalDashboard.toolbar.orchestrator', defaultMessage: 'Orchestrator' })}
{orchestratorCount > 0 && (
<Badge variant="secondary" className="text-[10px] px-1.5 py-0 ml-0.5">
{orchestratorCount}
</Badge>
)}
</button>
</div>
)}
{/* Tab content */}
{activeTab === 'queue' ? (
<QueueTabContent embedded={embedded} />
) : (
<OrchestratorTabContent />
)}
</div>
);

View File

@@ -19,7 +19,7 @@ import { Button } from '@/components/ui/Button';
import { useTerminalPanelStore } from '@/stores/terminalPanelStore';
import { useCliSessionStore, type CliSessionMeta } from '@/stores/cliSessionStore';
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
import { QueueExecutionListView } from './QueueExecutionListView';
import { QueuePanel } from '@/components/terminal-dashboard/QueuePanel';
import {
fetchCliSessionBuffer,
sendCliSessionText,
@@ -273,7 +273,7 @@ export function TerminalMainArea({ onClose }: TerminalMainAreaProps) {
{/* Content */}
{panelView === 'queue' ? (
/* Queue View */
<QueueExecutionListView />
<QueuePanel />
) : activeTerminalId ? (
/* Terminal View */
<div className="flex-1 min-h-0">

View File

@@ -85,7 +85,12 @@
"modeYolo": "Yolo",
"quickCreate": "Quick Create",
"configure": "Configure...",
"fullscreen": "Fullscreen"
"fullscreen": "Fullscreen",
"orchestrator": "Orchestrator"
},
"orchestratorPanel": {
"noPlans": "No active orchestrations",
"noPlansHint": "Run a flow from the Orchestrator to see progress here"
},
"cliConfig": {
"title": "Create CLI Session",

View File

@@ -85,7 +85,12 @@
"modeYolo": "Yolo",
"quickCreate": "快速创建",
"configure": "配置...",
"fullscreen": "全屏"
"fullscreen": "全屏",
"orchestrator": "编排器"
},
"orchestratorPanel": {
"noPlans": "没有活跃的编排任务",
"noPlansHint": "从编排器运行流程后,进度将显示在这里"
},
"cliConfig": {
"title": "创建 CLI 会话",

View File

@@ -13,7 +13,6 @@ import {
SessionDetailPage,
HistoryPage,
OrchestratorPage,
LoopMonitorPage,
IssueHubPage,
SkillsManagerPage,
CommandsManagerPage,
@@ -91,7 +90,7 @@ const routes: RouteObject[] = [
},
{
path: 'loops',
element: <LoopMonitorPage />,
element: <Navigate to="/terminal-dashboard" replace />,
},
{
path: 'cli-viewer',
@@ -207,6 +206,7 @@ export const ROUTES = {
PROJECT: '/project',
HISTORY: '/history',
ORCHESTRATOR: '/orchestrator',
/** @deprecated Redirects to /terminal-dashboard */
LOOPS: '/loops',
CLI_VIEWER: '/cli-viewer',
ISSUES: '/issues',

View File

@@ -1463,9 +1463,17 @@ async function initAction(issueId: string | undefined, options: IssueOptions): P
* list - List issues or tasks
*/
async function listAction(issueId: string | undefined, options: IssueOptions): Promise<void> {
// Always compute summary from ALL issues (unfiltered)
const allIssues = readIssues();
const statusCounts: Record<string, number> = {};
for (const i of allIssues) {
statusCounts[i.status] = (statusCounts[i.status] || 0) + 1;
}
const summary = { total: allIssues.length, by_status: statusCounts };
if (!issueId) {
// List all issues
let issues = readIssues();
let issues = allIssues;
// Filter by status if specified
if (options.status) {
@@ -1483,18 +1491,19 @@ async function listAction(issueId: string | undefined, options: IssueOptions): P
tags: i.tags || [],
bound_solution_id: i.bound_solution_id
}));
console.log(JSON.stringify(briefIssues, null, 2));
console.log(JSON.stringify({ _summary: summary, issues: briefIssues }, null, 2));
return;
}
if (options.json) {
console.log(JSON.stringify(issues, null, 2));
console.log(JSON.stringify({ _summary: summary, issues }, null, 2));
return;
}
if (issues.length === 0) {
console.log(chalk.yellow('No issues found'));
console.log(chalk.gray('Create one with: ccw issue init <issue-id>'));
printIssueSummary(summary);
return;
}
@@ -1523,6 +1532,8 @@ async function listAction(issueId: string | undefined, options: IssueOptions): P
(issue.title || '').substring(0, 30)
);
}
printIssueSummary(summary, options.status ? issues.length : undefined);
return;
}
@@ -1537,7 +1548,7 @@ async function listAction(issueId: string | undefined, options: IssueOptions): P
const tasks = solution?.tasks || [];
if (options.json) {
console.log(JSON.stringify({ issue, solution, tasks }, null, 2));
console.log(JSON.stringify({ _summary: summary, issue, solution, tasks }, null, 2));
return;
}
@@ -1549,6 +1560,7 @@ async function listAction(issueId: string | undefined, options: IssueOptions): P
if (tasks.length === 0) {
console.log(chalk.yellow('No tasks (bind a solution first)'));
printIssueSummary(summary);
return;
}
@@ -1563,6 +1575,32 @@ async function listAction(issueId: string | undefined, options: IssueOptions): P
task.title.substring(0, 30)
);
}
printIssueSummary(summary);
}
/**
* Print issue summary line (total + per-status counts)
*/
function printIssueSummary(summary: { total: number; by_status: Record<string, number> }, filteredCount?: number): void {
const parts = Object.entries(summary.by_status)
.sort(([a], [b]) => a.localeCompare(b))
.map(([status, count]) => {
const color = {
'registered': chalk.gray,
'planning': chalk.blue,
'planned': chalk.cyan,
'queued': chalk.yellow,
'executing': chalk.yellow,
'completed': chalk.green,
'failed': chalk.red,
'paused': chalk.magenta
}[status] || chalk.white;
return color(`${status}: ${count}`);
});
const filterInfo = filteredCount !== undefined ? ` (showing ${filteredCount})` : '';
console.log(chalk.gray(`\nTotal: ${summary.total}${filterInfo} | ${parts.join(', ')}`));
}
/**