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

View File

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