refactor(terminal-dashboard): move agent list to execution monitor panel

- Remove AgentList component from left sidebar
- Integrate orchestration plans display into ExecutionMonitorPanel
- Execution Monitor now shows both workflow executions and orchestration plans
- Cleaner sidebar with only session tree
This commit is contained in:
catlog22
2026-02-20 22:06:21 +08:00
parent 7e5d47fe8d
commit b2c1d32c86
2 changed files with 243 additions and 144 deletions

View File

@@ -1,9 +1,10 @@
// ========================================
// Execution Monitor Panel
// ========================================
// Panel for monitoring workflow executions in Terminal Dashboard.
// Displays execution progress, step list, and control buttons.
// Panel for monitoring workflow executions and orchestration plans in Terminal Dashboard.
// Displays execution progress, step list, control buttons, and active orchestration plans.
import { useMemo } from 'react';
import { useIntl } from 'react-intl';
import {
Play,
@@ -15,6 +16,7 @@ import {
Loader2,
Clock,
Terminal,
Bot,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/Button';
@@ -25,7 +27,10 @@ import {
selectCurrentExecution,
selectActiveExecutions,
} from '@/stores/executionMonitorStore';
import { useOrchestratorStore, selectActivePlans } from '@/stores';
import type { ExecutionStatus, StepInfo } from '@/stores/executionMonitorStore';
import type { OrchestrationRunState } from '@/stores/orchestratorStore';
import type { OrchestrationStatus } from '@/types/orchestrator';
// ========== Status Config ==========
@@ -38,6 +43,18 @@ const statusConfig: Record<ExecutionStatus, { label: string; color: string; bgCo
cancelled: { label: 'Cancelled', color: 'text-muted-foreground', bgColor: 'bg-muted' },
};
const ORCHESTRATION_STATUS_CONFIG: Record<
OrchestrationStatus,
{ variant: 'default' | 'info' | 'success' | 'destructive' | 'secondary' | 'warning'; messageId: string }
> = {
running: { variant: 'info', messageId: 'terminalDashboard.agentList.statusRunning' },
completed: { variant: 'success', messageId: 'terminalDashboard.agentList.statusCompleted' },
failed: { variant: 'destructive', messageId: 'terminalDashboard.agentList.statusFailed' },
paused: { variant: 'warning', messageId: 'terminalDashboard.agentList.statusPaused' },
pending: { variant: 'secondary', messageId: 'terminalDashboard.agentList.statusPending' },
cancelled: { variant: 'secondary', messageId: 'terminalDashboard.agentList.statusPending' },
};
// ========== Step Status Icon ==========
function StepStatusIcon({ status }: { status: ExecutionStatus }) {
@@ -97,6 +114,60 @@ function StepListItem({ step, isCurrent }: StepListItemProps) {
);
}
// ========== Orchestration Plan Item ==========
function OrchestrationPlanItem({
runState,
}: {
runState: OrchestrationRunState;
}) {
const { formatMessage } = useIntl();
const { plan, status, stepStatuses } = runState;
const totalSteps = plan.steps.length;
const completedSteps = useMemo(
() =>
Object.values(stepStatuses).filter(
(s) => s.status === 'completed' || s.status === 'skipped'
).length,
[stepStatuses]
);
const config = ORCHESTRATION_STATUS_CONFIG[status] ?? ORCHESTRATION_STATUS_CONFIG.pending;
const isRunning = status === 'running';
return (
<div
className={cn(
'flex items-center gap-2 px-3 py-2 rounded-md',
'hover:bg-muted/30 transition-colors'
)}
>
<div className="shrink-0">
{isRunning ? (
<Loader2 className="w-4 h-4 text-primary animate-spin" />
) : (
<Bot className="w-4 h-4 text-muted-foreground" />
)}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">{plan.name}</p>
<p className="text-xs text-muted-foreground">
{formatMessage(
{ id: 'terminalDashboard.agentList.stepLabel' },
{ current: completedSteps, total: totalSteps }
)}
</p>
</div>
<Badge variant={config.variant} className="text-xs px-2 py-0 shrink-0">
{formatMessage({ id: config.messageId })}
</Badge>
</div>
);
}
// ========== Main Component ==========
export function ExecutionMonitorPanel() {
@@ -110,10 +181,17 @@ export function ExecutionMonitorPanel() {
const stopExecution = useExecutionMonitorStore((s) => s.stopExecution);
const clearExecution = useExecutionMonitorStore((s) => s.clearExecution);
const executions = Object.values(activeExecutions);
const hasExecutions = executions.length > 0;
// Orchestration plans
const activePlans = useOrchestratorStore(selectActivePlans);
const planEntries = useMemo(
() => Object.entries(activePlans),
[activePlans]
);
if (!hasExecutions) {
const executions = Object.values(activeExecutions);
const hasContent = executions.length > 0 || planEntries.length > 0;
if (!hasContent) {
return (
<div className="flex flex-col items-center justify-center h-full text-muted-foreground p-8">
<Terminal className="w-10 h-10 mb-3 opacity-30" />
@@ -129,152 +207,177 @@ export function ExecutionMonitorPanel() {
return (
<div className="flex flex-col h-full">
{/* Execution selector (if multiple) */}
{executions.length > 1 && (
<div className="border-b border-border p-2 shrink-0">
<div className="flex flex-wrap gap-1">
{executions.map((exec) => (
<button
key={exec.executionId}
onClick={() => selectExecution(exec.executionId)}
className={cn(
'px-2 py-1 text-xs rounded-md transition-colors truncate max-w-[120px]',
currentExecution?.executionId === exec.executionId
? 'bg-primary text-primary-foreground'
: 'bg-muted hover:bg-muted/80 text-muted-foreground'
)}
>
{exec.flowName}
</button>
{/* Orchestration Plans Section */}
{planEntries.length > 0 && (
<div className="border-b border-border shrink-0">
<div className="flex items-center gap-2 px-4 py-2 bg-muted/20">
<Bot className="w-4 h-4 text-muted-foreground" />
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
{formatMessage({ id: 'terminalDashboard.agentList.title' })}
</h3>
<Badge variant="secondary" className="text-[10px] px-1.5 py-0 ml-auto">
{planEntries.length}
</Badge>
</div>
<div className="divide-y divide-border/50">
{planEntries.map(([planId, runState]) => (
<OrchestrationPlanItem key={planId} runState={runState} />
))}
</div>
</div>
)}
{currentExecution && (
{/* Workflow Executions Section */}
{executions.length > 0 && (
<>
{/* Header */}
<div className="p-4 border-b border-border space-y-3 shrink-0">
<div className="flex items-center gap-3">
<h3 className="text-sm font-semibold text-foreground truncate flex-1">
{currentExecution.flowName}
</h3>
<Badge
variant="secondary"
className={cn('shrink-0', statusConfig[currentExecution.status].bgColor)}
>
{statusConfig[currentExecution.status].label}
</Badge>
</div>
{/* Progress */}
<div className="space-y-1">
<div className="flex justify-between text-xs text-muted-foreground">
<span>
{formatMessage(
{ id: 'executionMonitor.progress', defaultMessage: '{completed}/{total} steps' },
{ completed: currentExecution.completedSteps, total: currentExecution.totalSteps || currentExecution.steps.length }
)}
</span>
<span>
{Math.round(
(currentExecution.completedSteps / (currentExecution.totalSteps || currentExecution.steps.length || 1)) * 100
)}%
</span>
{/* Execution selector (if multiple) */}
{executions.length > 1 && (
<div className="border-b border-border p-2 shrink-0">
<div className="flex flex-wrap gap-1">
{executions.map((exec) => (
<button
key={exec.executionId}
onClick={() => selectExecution(exec.executionId)}
className={cn(
'px-2 py-1 text-xs rounded-md transition-colors truncate max-w-[120px]',
currentExecution?.executionId === exec.executionId
? 'bg-primary text-primary-foreground'
: 'bg-muted hover:bg-muted/80 text-muted-foreground'
)}
>
{exec.flowName}
</button>
))}
</div>
<Progress
value={
(currentExecution.completedSteps / (currentExecution.totalSteps || currentExecution.steps.length || 1)) * 100
}
className="h-2"
/>
</div>
)}
{/* Meta info */}
<div className="flex items-center gap-3 text-xs text-muted-foreground">
<span className="flex items-center gap-1">
<Clock className="w-3 h-3" />
{new Date(currentExecution.startedAt).toLocaleTimeString()}
</span>
<span className="flex items-center gap-1">
<Terminal className="w-3 h-3" />
<span className="truncate max-w-[100px]">{currentExecution.sessionKey.slice(0, 20)}...</span>
</span>
</div>
</div>
{currentExecution && (
<>
{/* Header */}
<div className="p-4 border-b border-border space-y-3 shrink-0">
<div className="flex items-center gap-3">
<h3 className="text-sm font-semibold text-foreground truncate flex-1">
{currentExecution.flowName}
</h3>
<Badge
variant="secondary"
className={cn('shrink-0', statusConfig[currentExecution.status].bgColor)}
>
{statusConfig[currentExecution.status].label}
</Badge>
</div>
{/* Step list */}
<div className="flex-1 overflow-y-auto p-2 space-y-1">
{currentExecution.steps.map((step) => (
<StepListItem
key={step.id}
step={step}
isCurrent={step.id === currentExecution.currentStepId}
/>
))}
</div>
{/* Progress */}
<div className="space-y-1">
<div className="flex justify-between text-xs text-muted-foreground">
<span>
{formatMessage(
{ id: 'executionMonitor.progress', defaultMessage: '{completed}/{total} steps' },
{ completed: currentExecution.completedSteps, total: currentExecution.totalSteps || currentExecution.steps.length }
)}
</span>
<span>
{Math.round(
(currentExecution.completedSteps / (currentExecution.totalSteps || currentExecution.steps.length || 1)) * 100
)}%
</span>
</div>
<Progress
value={
(currentExecution.completedSteps / (currentExecution.totalSteps || currentExecution.steps.length || 1)) * 100
}
className="h-2"
/>
</div>
{/* Control bar */}
<div className="p-3 border-t border-border flex items-center gap-2 shrink-0">
{currentExecution.status === 'running' && (
<>
<Button
variant="outline"
size="sm"
onClick={() => pauseExecution(currentExecution.executionId)}
className="gap-1.5"
>
<Pause className="w-3.5 h-3.5" />
{formatMessage({ id: 'executionMonitor.pause', defaultMessage: 'Pause' })}
</Button>
<Button
variant="destructive"
size="sm"
onClick={() => stopExecution(currentExecution.executionId)}
className="gap-1.5"
>
<Square className="w-3.5 h-3.5" />
{formatMessage({ id: 'executionMonitor.stop', defaultMessage: 'Stop' })}
</Button>
</>
)}
{/* Meta info */}
<div className="flex items-center gap-3 text-xs text-muted-foreground">
<span className="flex items-center gap-1">
<Clock className="w-3 h-3" />
{new Date(currentExecution.startedAt).toLocaleTimeString()}
</span>
<span className="flex items-center gap-1">
<Terminal className="w-3 h-3" />
<span className="truncate max-w-[100px]">{currentExecution.sessionKey.slice(0, 20)}...</span>
</span>
</div>
</div>
{currentExecution.status === 'paused' && (
<>
<Button
variant="default"
size="sm"
onClick={() => resumeExecution(currentExecution.executionId)}
className="gap-1.5"
>
<Play className="w-3.5 h-3.5" />
{formatMessage({ id: 'executionMonitor.resume', defaultMessage: 'Resume' })}
</Button>
<Button
variant="destructive"
size="sm"
onClick={() => stopExecution(currentExecution.executionId)}
className="gap-1.5"
>
<Square className="w-3.5 h-3.5" />
{formatMessage({ id: 'executionMonitor.stop', defaultMessage: 'Stop' })}
</Button>
</>
)}
{/* Step list */}
<div className="flex-1 overflow-y-auto p-2 space-y-1">
{currentExecution.steps.map((step) => (
<StepListItem
key={step.id}
step={step}
isCurrent={step.id === currentExecution.currentStepId}
/>
))}
</div>
{(currentExecution.status === 'completed' ||
currentExecution.status === 'failed' ||
currentExecution.status === 'cancelled') && (
<Button
variant="outline"
size="sm"
onClick={() => clearExecution(currentExecution.executionId)}
className="gap-1.5 ml-auto"
>
{formatMessage({ id: 'executionMonitor.clear', defaultMessage: 'Clear' })}
</Button>
)}
</div>
{/* Control bar */}
<div className="p-3 border-t border-border flex items-center gap-2 shrink-0">
{currentExecution.status === 'running' && (
<>
<Button
variant="outline"
size="sm"
onClick={() => pauseExecution(currentExecution.executionId)}
className="gap-1.5"
>
<Pause className="w-3.5 h-3.5" />
{formatMessage({ id: 'executionMonitor.pause', defaultMessage: 'Pause' })}
</Button>
<Button
variant="destructive"
size="sm"
onClick={() => stopExecution(currentExecution.executionId)}
className="gap-1.5"
>
<Square className="w-3.5 h-3.5" />
{formatMessage({ id: 'executionMonitor.stop', defaultMessage: 'Stop' })}
</Button>
</>
)}
{currentExecution.status === 'paused' && (
<>
<Button
variant="default"
size="sm"
onClick={() => resumeExecution(currentExecution.executionId)}
className="gap-1.5"
>
<Play className="w-3.5 h-3.5" />
{formatMessage({ id: 'executionMonitor.resume', defaultMessage: 'Resume' })}
</Button>
<Button
variant="destructive"
size="sm"
onClick={() => stopExecution(currentExecution.executionId)}
className="gap-1.5"
>
<Square className="w-3.5 h-3.5" />
{formatMessage({ id: 'executionMonitor.stop', defaultMessage: 'Stop' })}
</Button>
</>
)}
{(currentExecution.status === 'completed' ||
currentExecution.status === 'failed' ||
currentExecution.status === 'cancelled') && (
<Button
variant="outline"
size="sm"
onClick={() => clearExecution(currentExecution.executionId)}
className="gap-1.5 ml-auto"
>
{formatMessage({ id: 'executionMonitor.clear', defaultMessage: 'Clear' })}
</Button>
)}
</div>
</>
)}
</>
)}
</div>

View File

@@ -2,11 +2,11 @@
// Terminal Dashboard Page (V2)
// ========================================
// Terminal-first layout with fixed session sidebar + floating panels + right file sidebar.
// Left sidebar: SessionGroupTree + AgentList (always visible)
// Left sidebar: SessionGroupTree (always visible)
// Main area: TerminalGrid (tmux-style split panes)
// Right sidebar: FileSidebarPanel (file tree, resizable)
// Top: DashboardToolbar with panel toggles and layout presets
// Floating panels: Issues, Queue, Inspector (overlay, mutually exclusive)
// Floating panels: Issues, Queue, Inspector, Execution Monitor (overlay, mutually exclusive)
// Fullscreen mode: Uses global isImmersiveMode to hide app chrome (Header + Sidebar)
import { useState, useCallback } from 'react';
@@ -18,7 +18,6 @@ import { DashboardToolbar, type PanelId } from '@/components/terminal-dashboard/
import { TerminalGrid } from '@/components/terminal-dashboard/TerminalGrid';
import { FloatingPanel } from '@/components/terminal-dashboard/FloatingPanel';
import { SessionGroupTree } from '@/components/terminal-dashboard/SessionGroupTree';
import { AgentList } from '@/components/terminal-dashboard/AgentList';
import { IssuePanel } from '@/components/terminal-dashboard/IssuePanel';
import { QueuePanel } from '@/components/terminal-dashboard/QueuePanel';
import { InspectorContent } from '@/components/terminal-dashboard/BottomInspector';
@@ -74,9 +73,6 @@ export function TerminalDashboardPage() {
<div className="flex-1 min-h-0 overflow-y-auto">
<SessionGroupTree />
</div>
<div className="shrink-0">
<AgentList />
</div>
</div>
</Allotment.Pane>
)}