mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
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:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user