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

@@ -0,0 +1,154 @@
// ========================================
// Orchestrator Execution Hooks
// ========================================
// React Query hooks for executing flows in terminal sessions.
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useExecutionMonitorStore } from '@/stores/executionMonitorStore';
import { useSessionManagerStore } from '@/stores/sessionManagerStore';
import { toast } from '@/stores/notificationStore';
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
// ========== Types ==========
export interface SessionConfig {
tool?: 'claude' | 'gemini' | 'qwen' | 'codex' | 'opencode';
model?: string;
preferredShell?: 'bash' | 'pwsh' | 'cmd';
}
export interface ExecuteInSessionRequest {
sessionConfig?: SessionConfig;
sessionKey?: string;
variables?: Record<string, unknown>;
stepTimeout?: number;
errorStrategy?: 'pause' | 'skip' | 'stop';
}
export interface ExecuteInSessionResponse {
success: boolean;
data: {
executionId: string;
flowId: string;
sessionKey: string;
status: 'pending' | 'running';
totalSteps: number;
startedAt: string;
};
error?: string;
}
// ========== Helper ==========
function withPath(url: string, projectPath?: string | null): string {
const p = typeof projectPath === 'string' ? projectPath.trim() : '';
if (!p) return url;
const sep = url.includes('?') ? '&' : '?';
return `${url}${sep}path=${encodeURIComponent(p)}`;
}
// ========== Hook ==========
export function useExecuteFlowInSession() {
const queryClient = useQueryClient();
const projectPath = useWorkflowStore(selectProjectPath);
const handleExecutionMessage = useExecutionMonitorStore((s) => s.handleExecutionMessage);
const setPanelOpen = useExecutionMonitorStore((s) => s.setPanelOpen);
const lockSession = useSessionManagerStore((s) => s.lockSession);
return useMutation({
mutationFn: async (params: {
flowId: string;
sessionConfig?: SessionConfig;
sessionKey?: string;
}): Promise<ExecuteInSessionResponse> => {
const url = withPath(`/api/orchestrator/flows/${params.flowId}/execute-in-session`, projectPath);
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sessionConfig: params.sessionConfig,
sessionKey: params.sessionKey,
}),
});
return response.json();
},
onSuccess: (data) => {
if (data.success) {
const { executionId, flowId, sessionKey, startedAt } = data.data;
// Initialize execution in store
handleExecutionMessage({
type: 'EXECUTION_STARTED',
payload: {
executionId,
flowId,
sessionKey,
stepName: flowId,
timestamp: startedAt,
},
});
// Lock the session
lockSession(sessionKey, `Executing workflow: ${flowId}`, executionId);
// Open the execution monitor panel
setPanelOpen(true);
// Update query cache
queryClient.setQueryData(['activeExecution'], data.data);
}
},
onError: (error) => {
console.error('[ExecuteFlowInSession] Error:', error);
toast.error(
'Execution Failed',
'Could not start workflow execution in terminal session.'
);
},
});
}
// ========== Session Lock Hooks ==========
export function useLockSession() {
return useMutation({
mutationFn: async (params: {
sessionKey: string;
reason: string;
executionId?: string;
}) => {
const response = await fetch(`/api/sessions/${params.sessionKey}/lock`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ reason: params.reason, executionId: params.executionId }),
});
return response.json();
},
});
}
export function useUnlockSession() {
return useMutation({
mutationFn: async (sessionKey: string) => {
const response = await fetch(`/api/sessions/${sessionKey}/unlock`, {
method: 'POST',
});
return response.json();
},
});
}
// ========== Flow to Session Conversion Hook ==========
export function usePrepareFlowForExecution() {
const projectPath = useWorkflowStore(selectProjectPath);
return useMutation({
mutationFn: async (flowId: string) => {
const url = withPath(`/api/orchestrator/flows/${flowId}`, projectPath);
const response = await fetch(url);
return response.json();
},
});
}