mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
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
292 lines
9.0 KiB
TypeScript
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;
|