mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
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:
291
ccw/frontend/src/stores/executionMonitorStore.ts
Normal file
291
ccw/frontend/src/stores/executionMonitorStore.ts
Normal file
@@ -0,0 +1,291 @@
|
||||
// ========================================
|
||||
// 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;
|
||||
Reference in New Issue
Block a user