feat(orchestrator): redesign orchestrator page as template editor with terminal execution

Phase 1: Orchestrator Simplification
- Remove ExecutionMonitor from OrchestratorPage
- Replace "Run Workflow" button with "Send to Terminal" button
- Update i18n texts for template editor context

Phase 2: Session Lock Mechanism
- Add 'locked' status to TerminalStatus type
- Extend TerminalMeta with isLocked, lockReason, lockedByExecutionId, lockedAt
- Implement lockSession/unlockSession in sessionManagerStore
- Create SessionLockConfirmDialog component for input interception

Phase 3: Execution Monitor Panel
- Create executionMonitorStore for execution state management
- Create ExecutionMonitorPanel component with step progress display
- Add execution panel to DashboardToolbar and TerminalDashboardPage
- Support WebSocket message handling for execution updates

Phase 4: Execution Bridge
- Add POST /api/orchestrator/flows/:id/execute-in-session endpoint
- Create useExecuteFlowInSession hook for frontend API calls
- Broadcast EXECUTION_STARTED and CLI_SESSION_LOCKED WebSocket messages
- Lock session when execution starts, unlock on completion
This commit is contained in:
catlog22
2026-02-20 21:49:05 +08:00
parent b38750f0cf
commit f8ff9eaa7f
13 changed files with 1156 additions and 234 deletions

View File

@@ -22,6 +22,7 @@ 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';
import { ExecutionMonitorPanel } from '@/components/terminal-dashboard/ExecutionMonitorPanel';
import { FileSidebarPanel } from '@/components/terminal-dashboard/FileSidebarPanel';
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
@@ -128,6 +129,16 @@ export function TerminalDashboardPage() {
>
<InspectorContent />
</FloatingPanel>
<FloatingPanel
isOpen={activePanel === 'execution'}
onClose={closePanel}
title={formatMessage({ id: 'terminalDashboard.toolbar.executionMonitor', defaultMessage: 'Execution Monitor' })}
side="right"
width={380}
>
<ExecutionMonitorPanel />
</FloatingPanel>
</AssociationHighlightProvider>
</div>
);

View File

@@ -1,10 +1,11 @@
// ========================================
// Flow Toolbar Component
// ========================================
// Toolbar for flow operations: Save, Load, Import Template, Export, Run, Monitor
// Toolbar for flow operations: Save, Load, Import Template, Export, Send to Terminal
import { useState, useCallback, useEffect } from 'react';
import { useIntl } from 'react-intl';
import { useNavigate } from 'react-router-dom';
import {
Save,
FolderOpen,
@@ -15,8 +16,7 @@ import {
Loader2,
ChevronDown,
Library,
Play,
Activity,
Terminal,
Maximize2,
Minimize2,
} from 'lucide-react';
@@ -24,8 +24,6 @@ import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { useFlowStore, toast } from '@/stores';
import { useExecutionStore } from '@/stores/executionStore';
import { useExecuteFlow } from '@/hooks/useFlows';
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
import type { Flow } from '@/types/flow';
@@ -36,6 +34,7 @@ interface FlowToolbarProps {
export function FlowToolbar({ className, onOpenTemplateLibrary }: FlowToolbarProps) {
const { formatMessage } = useIntl();
const navigate = useNavigate();
const [isFlowListOpen, setIsFlowListOpen] = useState(false);
const [flowName, setFlowName] = useState('');
const [isSaving, setIsSaving] = useState(false);
@@ -55,18 +54,6 @@ export function FlowToolbar({ className, onOpenTemplateLibrary }: FlowToolbarPro
const duplicateFlow = useFlowStore((state) => state.duplicateFlow);
const fetchFlows = useFlowStore((state) => state.fetchFlows);
// Execution store
const currentExecution = useExecutionStore((state) => state.currentExecution);
const isMonitorPanelOpen = useExecutionStore((state) => state.isMonitorPanelOpen);
const setMonitorPanelOpen = useExecutionStore((state) => state.setMonitorPanelOpen);
const startExecution = useExecutionStore((state) => state.startExecution);
// Mutations
const executeFlow = useExecuteFlow();
const isExecuting = currentExecution?.status === 'running';
const isPaused = currentExecution?.status === 'paused';
// Load flows on mount
useEffect(() => {
fetchFlows();
@@ -194,24 +181,26 @@ export function FlowToolbar({ className, onOpenTemplateLibrary }: FlowToolbarPro
toast.success(formatMessage({ id: 'orchestrator.notifications.flowExported' }), formatMessage({ id: 'orchestrator.notifications.flowExported' }));
}, [currentFlow]);
// Handle run workflow
const handleRun = useCallback(async () => {
if (!currentFlow) return;
try {
// Open monitor panel automatically
setMonitorPanelOpen(true);
const result = await executeFlow.mutateAsync(currentFlow.id);
startExecution(result.execId, currentFlow.id);
} catch (error) {
console.error('Failed to execute flow:', error);
toast.error(formatMessage({ id: 'orchestrator.notifications.executionFailed' }), formatMessage({ id: 'orchestrator.notifications.couldNotExecute' }));
// Handle send to terminal execution
const handleSendToTerminal = useCallback(async () => {
if (!currentFlow) {
toast.error(formatMessage({ id: 'orchestrator.notifications.noFlow' }), formatMessage({ id: 'orchestrator.notifications.saveBeforeExecute' }));
return;
}
}, [currentFlow, executeFlow, startExecution, setMonitorPanelOpen]);
// Handle monitor toggle
const handleToggleMonitor = useCallback(() => {
setMonitorPanelOpen(!isMonitorPanelOpen);
}, [isMonitorPanelOpen, setMonitorPanelOpen]);
// Save flow first if modified
if (isModified) {
const saved = await saveFlow();
if (!saved) {
toast.error(formatMessage({ id: 'orchestrator.notifications.saveFailed' }), formatMessage({ id: 'orchestrator.notifications.couldNotSave' }));
return;
}
}
// Navigate to terminal dashboard with flow execution request
navigate(`/terminal?executeFlow=${currentFlow.id}`);
toast.success(formatMessage({ id: 'orchestrator.notifications.flowSent' }), formatMessage({ id: 'orchestrator.notifications.sentToTerminal' }, { name: currentFlow.name }));
}, [currentFlow, isModified, saveFlow, navigate, formatMessage]);
return (
<div className={cn('flex items-center gap-3 p-3 bg-card border-b border-border', className)}>
@@ -346,29 +335,15 @@ export function FlowToolbar({ className, onOpenTemplateLibrary }: FlowToolbarPro
<div className="w-px h-6 bg-border" />
{/* Run & Monitor Group */}
<Button
variant={isMonitorPanelOpen ? 'secondary' : 'outline'}
size="sm"
onClick={handleToggleMonitor}
title={formatMessage({ id: 'orchestrator.monitor.toggleMonitor' })}
>
<Activity className={cn('w-4 h-4 mr-1', (isExecuting || isPaused) && 'text-primary animate-pulse')} />
{formatMessage({ id: 'orchestrator.toolbar.monitor' })}
</Button>
{/* Execute in Terminal */}
<Button
variant="default"
size="sm"
onClick={handleRun}
disabled={!currentFlow || isExecuting || isPaused || executeFlow.isPending}
onClick={handleSendToTerminal}
disabled={!currentFlow}
>
{executeFlow.isPending ? (
<Loader2 className="w-4 h-4 mr-1 animate-spin" />
) : (
<Play className="w-4 h-4 mr-1" />
)}
{formatMessage({ id: 'orchestrator.toolbar.runWorkflow' })}
<Terminal className="w-4 h-4 mr-1" />
{formatMessage({ id: 'orchestrator.toolbar.sendToTerminal' })}
</Button>
<div className="w-px h-6 bg-border" />

View File

@@ -1,27 +1,25 @@
// ========================================
// Orchestrator Page
// ========================================
// Visual workflow editor with React Flow, drag-drop node palette, and property panel
// Visual workflow template editor with React Flow, drag-drop node palette, and property panel
// Execution functionality moved to Terminal Dashboard
import { useEffect, useState, useCallback } from 'react';
import * as Collapsible from '@radix-ui/react-collapsible';
import { ChevronRight } from 'lucide-react';
import { useFlowStore } from '@/stores';
import { useExecutionStore } from '@/stores/executionStore';
import { Button } from '@/components/ui/Button';
import { FlowCanvas } from './FlowCanvas';
import { LeftSidebar } from './LeftSidebar';
import { PropertyPanel } from './PropertyPanel';
import { FlowToolbar } from './FlowToolbar';
import { TemplateLibrary } from './TemplateLibrary';
import { ExecutionMonitor } from './ExecutionMonitor';
export function OrchestratorPage() {
const fetchFlows = useFlowStore((state) => state.fetchFlows);
const isPaletteOpen = useFlowStore((state) => state.isPaletteOpen);
const setIsPaletteOpen = useFlowStore((state) => state.setIsPaletteOpen);
const isPropertyPanelOpen = useFlowStore((state) => state.isPropertyPanelOpen);
const isMonitorPanelOpen = useExecutionStore((state) => state.isMonitorPanelOpen);
const [isTemplateLibraryOpen, setIsTemplateLibraryOpen] = useState(false);
// Load flows on mount
@@ -60,15 +58,12 @@ export function OrchestratorPage() {
<FlowCanvas className="absolute inset-0" />
{/* Property Panel as overlay - only shown when a node is selected */}
{!isMonitorPanelOpen && isPropertyPanelOpen && (
{isPropertyPanelOpen && (
<div className="absolute top-2 right-2 bottom-2 z-10">
<PropertyPanel className="h-full" />
</div>
)}
</div>
{/* Execution Monitor Panel (Right) */}
<ExecutionMonitor />
</div>
{/* Template Library Dialog */}