Files
Claude-Code-Workflow/ccw/frontend/src/stores/executionMonitorStore.ts
catlog22 f8ff9eaa7f 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
2026-02-20 21:49:05 +08:00

292 lines
9.0 KiB
TypeScript

// ========================================
// Execution Monitor Store
// ========================================
// Zustand store for execution monitoring in Terminal Dashboard.
// Tracks active executions, handles WebSocket messages, and provides control actions.
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
// ========== Types ==========
export type ExecutionStatus = 'pending' | 'running' | 'paused' | 'completed' | 'failed' | 'cancelled';
export interface StepInfo {
id: string;
name: string;
status: ExecutionStatus;
output?: string;
error?: string;
startedAt?: string;
completedAt?: string;
}
export interface ExecutionInfo {
executionId: string;
flowId: string;
flowName: string;
sessionKey: string;
status: ExecutionStatus;
totalSteps: number;
completedSteps: number;
currentStepId?: string;
steps: StepInfo[];
startedAt: string;
completedAt?: string;
}
export type ExecutionWSMessageType =
| 'EXECUTION_STARTED'
| 'EXECUTION_STEP_START'
| 'EXECUTION_STEP_PROGRESS'
| 'EXECUTION_STEP_COMPLETE'
| 'EXECUTION_STEP_FAILED'
| 'EXECUTION_PAUSED'
| 'EXECUTION_RESUMED'
| 'EXECUTION_STOPPED'
| 'EXECUTION_COMPLETED';
export interface ExecutionWSMessage {
type: ExecutionWSMessageType;
payload: {
executionId: string;
flowId: string;
sessionKey: string;
stepId?: string;
stepName?: string;
progress?: number;
output?: string;
error?: string;
timestamp: string;
};
}
// ========== State Interface ==========
interface ExecutionMonitorState {
activeExecutions: Record<string, ExecutionInfo>;
currentExecutionId: string | null;
isPanelOpen: boolean;
}
interface ExecutionMonitorActions {
handleExecutionMessage: (msg: ExecutionWSMessage) => void;
selectExecution: (executionId: string | null) => void;
pauseExecution: (executionId: string) => void;
resumeExecution: (executionId: string) => void;
stopExecution: (executionId: string) => void;
setPanelOpen: (open: boolean) => void;
clearExecution: (executionId: string) => void;
clearAllExecutions: () => void;
}
type ExecutionMonitorStore = ExecutionMonitorState & ExecutionMonitorActions;
// ========== Initial State ==========
const initialState: ExecutionMonitorState = {
activeExecutions: {},
currentExecutionId: null,
isPanelOpen: false,
};
// ========== Store ==========
export const useExecutionMonitorStore = create<ExecutionMonitorStore>()(
devtools(
(set) => ({
...initialState,
handleExecutionMessage: (msg: ExecutionWSMessage) => {
const { type, payload } = msg;
const { executionId, flowId, sessionKey, stepId, stepName, output, error, timestamp } = payload;
set((state) => {
const existing = state.activeExecutions[executionId];
switch (type) {
case 'EXECUTION_STARTED':
return {
activeExecutions: {
...state.activeExecutions,
[executionId]: {
executionId,
flowId,
flowName: stepName || 'Workflow',
sessionKey,
status: 'running',
totalSteps: 0,
completedSteps: 0,
steps: [],
startedAt: timestamp,
},
},
currentExecutionId: executionId,
isPanelOpen: true,
};
case 'EXECUTION_STEP_START':
if (!existing) return state;
return {
activeExecutions: {
...state.activeExecutions,
[executionId]: {
...existing,
status: 'running',
currentStepId: stepId,
steps: [
...existing.steps.filter(s => s.id !== stepId),
{
id: stepId || '',
name: stepName || '',
status: 'running',
startedAt: timestamp,
},
],
},
},
};
case 'EXECUTION_STEP_PROGRESS':
if (!existing || !stepId) return state;
return {
activeExecutions: {
...state.activeExecutions,
[executionId]: {
...existing,
steps: existing.steps.map(s =>
s.id === stepId
? { ...s, output: (s.output || '') + (output || '') }
: s
),
},
},
};
case 'EXECUTION_STEP_COMPLETE':
if (!existing) return state;
return {
activeExecutions: {
...state.activeExecutions,
[executionId]: {
...existing,
completedSteps: existing.completedSteps + 1,
steps: existing.steps.map(s =>
s.id === stepId
? { ...s, status: 'completed', completedAt: timestamp }
: s
),
},
},
};
case 'EXECUTION_STEP_FAILED':
if (!existing) return state;
return {
activeExecutions: {
...state.activeExecutions,
[executionId]: {
...existing,
status: 'paused',
steps: existing.steps.map(s =>
s.id === stepId
? { ...s, status: 'failed', error, completedAt: timestamp }
: s
),
},
},
};
case 'EXECUTION_PAUSED':
if (!existing) return state;
return {
activeExecutions: {
...state.activeExecutions,
[executionId]: { ...existing, status: 'paused' },
},
};
case 'EXECUTION_RESUMED':
if (!existing) return state;
return {
activeExecutions: {
...state.activeExecutions,
[executionId]: { ...existing, status: 'running' },
},
};
case 'EXECUTION_STOPPED':
if (!existing) return state;
return {
activeExecutions: {
...state.activeExecutions,
[executionId]: { ...existing, status: 'cancelled', completedAt: timestamp },
},
};
case 'EXECUTION_COMPLETED':
if (!existing) return state;
return {
activeExecutions: {
...state.activeExecutions,
[executionId]: { ...existing, status: 'completed', completedAt: timestamp },
},
};
default:
return state;
}
}, false, `handleExecutionMessage/${type}`);
},
selectExecution: (executionId: string | null) => {
set({ currentExecutionId: executionId }, false, 'selectExecution');
},
pauseExecution: (executionId: string) => {
// TODO: Call API to pause execution
console.log('[ExecutionMonitor] Pause execution:', executionId);
},
resumeExecution: (executionId: string) => {
// TODO: Call API to resume execution
console.log('[ExecutionMonitor] Resume execution:', executionId);
},
stopExecution: (executionId: string) => {
// TODO: Call API to stop execution
console.log('[ExecutionMonitor] Stop execution:', executionId);
},
setPanelOpen: (open: boolean) => {
set({ isPanelOpen: open }, false, 'setPanelOpen');
},
clearExecution: (executionId: string) => {
set((state) => {
const next = { ...state.activeExecutions };
delete next[executionId];
return {
activeExecutions: next,
currentExecutionId: state.currentExecutionId === executionId ? null : state.currentExecutionId,
};
}, false, 'clearExecution');
},
clearAllExecutions: () => {
set({ activeExecutions: {}, currentExecutionId: null }, false, 'clearAllExecutions');
},
}),
{ name: 'ExecutionMonitorStore' }
)
);
// ========== Selectors ==========
export const selectActiveExecutions = (state: ExecutionMonitorStore) => state.activeExecutions;
export const selectCurrentExecution = (state: ExecutionMonitorStore) =>
state.currentExecutionId ? state.activeExecutions[state.currentExecutionId] : null;
export const selectIsPanelOpen = (state: ExecutionMonitorStore) => state.isPanelOpen;
export const selectActiveExecutionCount = (state: ExecutionMonitorStore) =>
Object.values(state.activeExecutions).filter(e => e.status === 'running' || e.status === 'paused').length;