From 4ee165119b27c1fa12e7493f2f4d3a3cf73f0a04 Mon Sep 17 00:00:00 2001 From: catlog22 Date: Wed, 4 Feb 2026 22:22:27 +0800 Subject: [PATCH] refactor: unify node types into a single PromptTemplate model - Removed individual node components (SlashCommandNode, FileOperationNode, etc.) and replaced them with a unified PromptTemplateNode. - Updated flow types and interfaces to reflect the new single node type system. - Refactored flow execution logic to handle the new unified model, simplifying node execution and context handling. - Adjusted UI components to support the new PromptTemplateNode, including instruction display and context references. - Cleaned up legacy code related to removed node types and ensured compatibility with the new structure. --- ccw/frontend/src/locales/en/orchestrator.json | 67 +- ccw/frontend/src/locales/zh/orchestrator.json | 67 +- .../src/pages/orchestrator/FlowCanvas.tsx | 31 +- .../src/pages/orchestrator/NodePalette.tsx | 56 +- .../src/pages/orchestrator/PropertyPanel.tsx | 670 +++++------------- .../orchestrator/nodes/CliCommandNode.tsx | 112 --- .../orchestrator/nodes/ConditionalNode.tsx | 118 --- .../orchestrator/nodes/FileOperationNode.tsx | 145 ---- .../pages/orchestrator/nodes/ParallelNode.tsx | 129 ---- .../pages/orchestrator/nodes/PromptNode.tsx | 120 ---- .../orchestrator/nodes/PromptTemplateNode.tsx | 132 ++++ .../orchestrator/nodes/SlashCommandNode.tsx | 100 --- .../src/pages/orchestrator/nodes/index.ts | 24 +- ccw/frontend/src/stores/flowStore.ts | 7 +- ccw/frontend/src/types/flow.ts | 285 ++++---- ccw/src/core/routes/orchestrator-routes.ts | 134 ++-- ccw/src/core/services/flow-executor.ts | 467 +++--------- 17 files changed, 647 insertions(+), 2017 deletions(-) delete mode 100644 ccw/frontend/src/pages/orchestrator/nodes/CliCommandNode.tsx delete mode 100644 ccw/frontend/src/pages/orchestrator/nodes/ConditionalNode.tsx delete mode 100644 ccw/frontend/src/pages/orchestrator/nodes/FileOperationNode.tsx delete mode 100644 ccw/frontend/src/pages/orchestrator/nodes/ParallelNode.tsx delete mode 100644 ccw/frontend/src/pages/orchestrator/nodes/PromptNode.tsx create mode 100644 ccw/frontend/src/pages/orchestrator/nodes/PromptTemplateNode.tsx delete mode 100644 ccw/frontend/src/pages/orchestrator/nodes/SlashCommandNode.tsx diff --git a/ccw/frontend/src/locales/en/orchestrator.json b/ccw/frontend/src/locales/en/orchestrator.json index eae1b149..d611bc20 100644 --- a/ccw/frontend/src/locales/en/orchestrator.json +++ b/ccw/frontend/src/locales/en/orchestrator.json @@ -163,70 +163,27 @@ "deleteNode": "Delete Node", "placeholders": { "nodeLabel": "Node label", - "commandName": "/command-name", - "commandArgs": "Command arguments", - "timeout": "60000", - "path": "/path/to/file", - "content": "File content...", - "destinationPath": "/path/to/destination", - "variableName": "variableName", - "condition": "e.g., result.success === true", - "trueLabel": "True", - "falseLabel": "False", - "contextTemplate": "Template with {variable} placeholders", - "promptText": "Enter your prompt here..." + "instruction": "e.g., Execute /workflow:plan for login feature\nor: Analyze code architecture\nor: Save {{analysis}} to ./output/result.json", + "outputName": "e.g., analysis, plan, result" }, "labels": { "label": "Label", - "command": "Command", - "arguments": "Arguments", - "executionMode": "Execution Mode", - "onError": "On Error", - "timeout": "Timeout (ms)", - "operation": "Operation", - "path": "Path", - "content": "Content", - "destinationPath": "Destination Path", - "outputVariable": "Output Variable", - "addToContext": "Add to context", - "condition": "Condition", - "trueLabel": "True Label", - "falseLabel": "False Label", - "joinMode": "Join Mode", - "failFast": "Fail fast (stop all branches on first error)", + "instruction": "Instruction", + "outputName": "Output Name", "tool": "CLI Tool", - "mode": "Mode", - "promptType": "Prompt Type", - "sourceNodes": "Source Nodes", - "contextTemplate": "Context Template", - "promptText": "Prompt Text" + "mode": "Execution Mode", + "contextRefs": "Context References" }, "options": { - "modeMainprocess": "Main Process", - "modeAsync": "Async", - "errorStop": "Stop execution", - "errorContinue": "Continue", - "errorRetry": "Retry", - "operationRead": "Read", - "operationWrite": "Write", - "operationAppend": "Append", - "operationDelete": "Delete", - "operationCopy": "Copy", - "operationMove": "Move", - "joinModeAll": "Wait for all branches", - "joinModeAny": "Complete when any branch finishes", - "joinModeNone": "No synchronization", + "toolNone": "None (auto-select)", "toolGemini": "Gemini", "toolQwen": "Qwen", "toolCodex": "Codex", - "modeAnalysis": "Analysis", - "modeWrite": "Write", - "modeReview": "Review", - "promptTypeOrganize": "Organize", - "promptTypeRefine": "Refine", - "promptTypeSummarize": "Summarize", - "promptTypeTransform": "Transform", - "promptTypeCustom": "Custom" + "toolClaude": "Claude", + "modeAnalysis": "Analysis (read-only)", + "modeWrite": "Write (modify files)", + "modeMainprocess": "Main Process (blocking)", + "modeAsync": "Async (non-blocking)" } } } diff --git a/ccw/frontend/src/locales/zh/orchestrator.json b/ccw/frontend/src/locales/zh/orchestrator.json index f2f0f246..01c8f007 100644 --- a/ccw/frontend/src/locales/zh/orchestrator.json +++ b/ccw/frontend/src/locales/zh/orchestrator.json @@ -162,70 +162,27 @@ "deleteNode": "删除节点", "placeholders": { "nodeLabel": "节点标签", - "commandName": "/命令名称", - "commandArgs": "命令参数", - "timeout": "60000", - "path": "/文件路径", - "content": "文件内容...", - "destinationPath": "/目标路径", - "variableName": "变量名称", - "condition": "例如: result.success === true", - "trueLabel": "真", - "falseLabel": "假", - "contextTemplate": "带有 {variable} 占位符的模板", - "promptText": "在此输入您的提示词..." + "instruction": "例如: 执行 /workflow:plan 用于登录功能\n或: 分析代码架构\n或: 将 {{analysis}} 保存到 ./output/result.json", + "outputName": "例如: analysis, plan, result" }, "labels": { "label": "标签", - "command": "命令", - "arguments": "参数", - "executionMode": "执行模式", - "onError": "出错时", - "timeout": "超时 (毫秒)", - "operation": "操作", - "path": "路径", - "content": "内容", - "destinationPath": "目标路径", - "outputVariable": "输出变量", - "addToContext": "添加到上下文", - "condition": "条件", - "trueLabel": "真标签", - "falseLabel": "假标签", - "joinMode": "加入模式", - "failFast": "快速失败 (首次错误时停止所有分支)", + "instruction": "指令", + "outputName": "输出名称", "tool": "CLI 工具", - "mode": "模式", - "promptType": "提示词类型", - "sourceNodes": "源节点", - "contextTemplate": "上下文模板", - "promptText": "提示词文本" + "mode": "执行模式", + "contextRefs": "上下文引用" }, "options": { - "modeMainprocess": "主进程", - "modeAsync": "异步", - "errorStop": "停止执行", - "errorContinue": "继续", - "errorRetry": "重试", - "operationRead": "读取", - "operationWrite": "写入", - "operationAppend": "追加", - "operationDelete": "删除", - "operationCopy": "复制", - "operationMove": "移动", - "joinModeAll": "等待所有分支", - "joinModeAny": "任一分支完成时完成", - "joinModeNone": "无同步", + "toolNone": "无 (自动选择)", "toolGemini": "Gemini", "toolQwen": "Qwen", "toolCodex": "Codex", - "modeAnalysis": "分析", - "modeWrite": "写入", - "modeReview": "审查", - "promptTypeOrganize": "组织", - "promptTypeRefine": "精炼", - "promptTypeSummarize": "总结", - "promptTypeTransform": "转换", - "promptTypeCustom": "自定义" + "toolClaude": "Claude", + "modeAnalysis": "分析 (只读)", + "modeWrite": "写入 (修改文件)", + "modeMainprocess": "主进程 (阻塞)", + "modeAsync": "异步 (非阻塞)" } } } diff --git a/ccw/frontend/src/pages/orchestrator/FlowCanvas.tsx b/ccw/frontend/src/pages/orchestrator/FlowCanvas.tsx index 8947f748..78170f57 100644 --- a/ccw/frontend/src/pages/orchestrator/FlowCanvas.tsx +++ b/ccw/frontend/src/pages/orchestrator/FlowCanvas.tsx @@ -23,8 +23,7 @@ import { import '@xyflow/react/dist/style.css'; import { useFlowStore } from '@/stores'; -import type { FlowNodeType, FlowNode, FlowEdge } from '@/types/flow'; -import { NODE_TYPE_CONFIGS } from '@/types/flow'; +import type { FlowNode, FlowEdge } from '@/types/flow'; // Custom node types (enhanced with execution status in IMPL-A8) import { nodeTypes } from './nodes'; @@ -116,8 +115,9 @@ function FlowCanvasInner({ className }: FlowCanvasProps) { (event: DragEvent) => { event.preventDefault(); - const nodeType = event.dataTransfer.getData('application/reactflow-node-type') as FlowNodeType; - if (!nodeType || !NODE_TYPE_CONFIGS[nodeType]) { + // Verify the drop is from node palette + const nodeType = event.dataTransfer.getData('application/reactflow-node-type'); + if (!nodeType) { return; } @@ -127,8 +127,8 @@ function FlowCanvasInner({ className }: FlowCanvasProps) { y: event.clientY, }); - // Add node at drop position - addNode(nodeType, position); + // Add prompt-template node at drop position + addNode(position); }, [screenToFlowPosition, addNode] ); @@ -161,24 +161,7 @@ function FlowCanvasInner({ className }: FlowCanvasProps) { /> { - switch (node.type) { - case 'slash-command': - return '#3b82f6'; // blue-500 - case 'file-operation': - return '#22c55e'; // green-500 - case 'conditional': - return '#f59e0b'; // amber-500 - case 'parallel': - return '#a855f7'; // purple-500 - case 'cli-command': - return '#f59e0b'; // amber-500 - case 'prompt': - return '#a855f7'; // purple-500 - default: - return '#6b7280'; // gray-500 - } - }} + nodeColor={() => '#3b82f6'} maskColor="rgba(0, 0, 0, 0.1)" /> > = { - 'slash-command': Terminal, - 'file-operation': FileText, - conditional: GitBranch, - parallel: GitMerge, - 'cli-command': Terminal, - prompt: FileText, -}; - -// Color mapping for node types -const nodeColors: Record = { - 'slash-command': 'bg-blue-500 hover:bg-blue-600', - 'file-operation': 'bg-green-500 hover:bg-green-600', - conditional: 'bg-amber-500 hover:bg-amber-600', - parallel: 'bg-purple-500 hover:bg-purple-600', - 'cli-command': 'bg-amber-500 hover:bg-amber-600', - prompt: 'bg-purple-500 hover:bg-purple-600', -}; - -const nodeBorderColors: Record = { - 'slash-command': 'border-blue-500', - 'file-operation': 'border-green-500', - conditional: 'border-amber-500', - parallel: 'border-purple-500', - 'cli-command': 'border-amber-500', - prompt: 'border-purple-500', -}; - interface NodePaletteProps { className?: string; } -interface NodeTypeCardProps { - type: FlowNodeType; -} - -function NodeTypeCard({ type }: NodeTypeCardProps) { - const config = NODE_TYPE_CONFIGS[type]; - const Icon = nodeIcons[type]; +/** + * Draggable card for the unified Prompt Template node type + */ +function PromptTemplateCard() { + const config = NODE_TYPE_CONFIGS['prompt-template']; // Handle drag start const onDragStart = (event: DragEvent) => { - event.dataTransfer.setData('application/reactflow-node-type', type); + event.dataTransfer.setData('application/reactflow-node-type', 'prompt-template'); event.dataTransfer.effectAllowed = 'move'; }; @@ -66,11 +34,11 @@ function NodeTypeCard({ type }: NodeTypeCardProps) { className={cn( 'group flex items-center gap-3 p-3 rounded-lg border-2 bg-card cursor-grab transition-all', 'hover:shadow-md hover:scale-[1.02] active:cursor-grabbing active:scale-[0.98]', - nodeBorderColors[type] + 'border-blue-500' )} > -
- +
+
{config.label}
@@ -141,9 +109,7 @@ export function NodePalette({ className }: NodePaletteProps) { {isExpanded && (
- {(Object.keys(NODE_TYPE_CONFIGS) as FlowNodeType[]).map((type) => ( - - ))} +
)}
diff --git a/ccw/frontend/src/pages/orchestrator/PropertyPanel.tsx b/ccw/frontend/src/pages/orchestrator/PropertyPanel.tsx index 52145a7f..30f537d1 100644 --- a/ccw/frontend/src/pages/orchestrator/PropertyPanel.tsx +++ b/ccw/frontend/src/pages/orchestrator/PropertyPanel.tsx @@ -1,30 +1,18 @@ // ======================================== // Property Panel Component // ======================================== -// Dynamic property editor for selected nodes +// Dynamic property editor for unified PromptTemplate nodes -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { useIntl } from 'react-intl'; -import { Settings, X, Terminal, FileText, GitBranch, GitMerge, Trash2 } from 'lucide-react'; +import { Settings, X, MessageSquare, Trash2 } from 'lucide-react'; import { cn } from '@/lib/utils'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; -import { CommandCombobox } from '@/components/ui/CommandCombobox'; -import { MultiNodeSelector, type NodeOption } from '@/components/ui/MultiNodeSelector'; -import { ContextAssembler } from '@/components/ui/ContextAssembler'; import { useFlowStore } from '@/stores'; -import type { - FlowNodeType, - SlashCommandNodeData, - FileOperationNodeData, - ConditionalNodeData, - ParallelNodeData, - CliCommandNodeData, - PromptNodeData, - NodeData, -} from '@/types/flow'; +import type { PromptTemplateNodeData, CliTool, ExecutionMode } from '@/types/flow'; -// ========== Common Form Field Components ========== +// ========== Form Field Components ========== interface LabelInputProps { value: string; @@ -47,483 +35,189 @@ function LabelInput({ value, onChange }: LabelInputProps) { ); } -interface OutputVariableInputProps { - value?: string; - onChange: (value?: string) => void; +// ========== Unified PromptTemplate Property Editor ========== + +interface PromptTemplatePropertiesProps { + data: PromptTemplateNodeData; + onChange: (updates: Partial) => void; } -function OutputVariableInput({ value, onChange }: OutputVariableInputProps) { +function PromptTemplateProperties({ data, onChange }: PromptTemplatePropertiesProps) { const { formatMessage } = useIntl(); - return ( -
- - onChange(e.target.value || undefined)} - placeholder={formatMessage({ id: 'orchestrator.propertyPanel.placeholders.variableName' })} - /> -
+ const nodes = useFlowStore((state) => state.nodes); + const selectedNodeId = useFlowStore((state) => state.selectedNodeId); + + // Build available outputNames from other nodes for contextRefs picker + const availableOutputNames = useMemo(() => { + return nodes + .filter((n) => n.id !== selectedNodeId && n.data?.outputName) + .map((n) => ({ + id: n.data.outputName as string, + label: n.data.label || n.id, + })); + }, [nodes, selectedNodeId]); + + const toggleContextRef = useCallback( + (outputName: string) => { + const currentRefs = data.contextRefs || []; + const newRefs = currentRefs.includes(outputName) + ? currentRefs.filter((ref) => ref !== outputName) + : [...currentRefs, outputName]; + onChange({ contextRefs: newRefs }); + }, + [data.contextRefs, onChange] ); -} - -interface PropertyPanelProps { - className?: string; -} - -// Icon mapping for node types -const nodeIcons: Record> = { - 'slash-command': Terminal, - 'file-operation': FileText, - conditional: GitBranch, - parallel: GitMerge, - 'cli-command': Terminal, - prompt: FileText, -}; - -// Slash Command Property Editor -function SlashCommandProperties({ - data, - onChange, -}: { - data: SlashCommandNodeData; - onChange: (updates: Partial) => void; -}) { - const { formatMessage } = useIntl(); return (
+ {/* Label */} onChange({ label: value })} /> + {/* Instruction - main textarea */}
- - onChange({ command: value })} - placeholder={formatMessage({ id: 'orchestrator.propertyPanel.placeholders.commandName' })} + +