Skip to main content

matrixcode_tui/workflow/
types.rs

1//! Workflow Visualization Types
2//!
3//! Defines visual state for workflow DAG rendering
4
5use matrixcode_core::workflow::{WorkflowDef, WorkflowContext, NodeStatus, NodeType, WorkflowPersistence, WorkflowRegistry};
6use std::collections::HashMap;
7use std::path::PathBuf;
8
9/// Workflow visualization state
10pub struct WorkflowViewState {
11    /// Current workflow definition (for DAG structure)
12    pub workflow_def: Option<WorkflowDef>,
13    /// Execution context (for status)
14    pub context: Option<WorkflowContext>,
15    /// Visual layout cache
16    pub layout: DagLayout,
17    /// Panel visibility
18    pub visible: bool,
19    /// Selected node (for inspection)
20    pub selected_node: Option<String>,
21    /// View mode
22    pub view_mode: WorkflowViewMode,
23    /// Spinner frame for running nodes
24    pub spinner_frame: usize,
25}
26
27impl Default for WorkflowViewState {
28    fn default() -> Self {
29        Self {
30            workflow_def: None,
31            context: None,
32            layout: DagLayout::default(),
33            visible: false,
34            selected_node: None,
35            view_mode: WorkflowViewMode::Dag,
36            spinner_frame: 0,
37        }
38    }
39}
40
41/// DAG layout cache (computed from WorkflowDef)
42#[derive(Default)]
43pub struct DagLayout {
44    /// Node positions (row, col) in grid coordinates
45    pub node_positions: HashMap<String, (usize, usize)>,
46    /// Edge connections (from, to)
47    pub edges: Vec<EdgeInfo>,
48    /// Layer nodes (nodes grouped by layer)
49    pub layers: Vec<Vec<String>>,
50    /// Layout dimensions (rows, cols)
51    pub height: usize,
52    pub width: usize,
53}
54
55
56/// Edge connection info
57pub struct EdgeInfo {
58    pub from: String,
59    pub to: String,
60    pub condition: Option<String>,
61}
62
63/// View modes
64pub enum WorkflowViewMode {
65    /// Full DAG visualization
66    Dag,
67    /// Compact progress bar + node list
68    Progress,
69    /// Node detail panel
70    Detail,
71}
72
73/// Visual node representation
74pub struct VisualNode {
75    pub id: String,
76    pub name: String,
77    pub node_type: NodeType,
78    pub status: NodeVisualStatus,
79    pub position: (usize, usize),
80    pub elapsed_ms: Option<u64>,
81}
82
83/// Visual node status
84pub enum NodeVisualStatus {
85    Pending,
86    Running,
87    Completed,
88    Failed { error: String },
89    Skipped,
90}
91
92impl NodeVisualStatus {
93    /// Get status icon
94    pub fn icon(&self) -> &'static str {
95        match self {
96            Self::Pending => "○",
97            Self::Running => "⟳",
98            Self::Completed => "✓",
99            Self::Failed { .. } => "✗",
100            Self::Skipped => "→",
101        }
102    }
103
104    /// Get status color name (for ratatui styling)
105    pub fn color(&self) -> &'static str {
106        match self {
107            Self::Pending => "gray",
108            Self::Running => "yellow",
109            Self::Completed => "green",
110            Self::Failed { .. } => "red",
111            Self::Skipped => "blue",
112        }
113    }
114}
115
116/// Get node type icon
117pub fn node_type_icon(node_type: &NodeType) -> &'static str {
118    match node_type {
119        NodeType::Start => "▶",
120        NodeType::End => "■",
121        NodeType::Task => "⚙",
122        NodeType::Condition => "◇",
123        NodeType::Parallel => "║",
124        NodeType::Approval => "?",
125        NodeType::Wait => "⏳",
126        NodeType::SubWorkflow => "↳",
127    }
128}
129
130/// Convert WorkflowContext node status to visual status
131pub fn to_visual_status(status: &NodeStatus, error: Option<&String>) -> NodeVisualStatus {
132    match status {
133        NodeStatus::Pending => NodeVisualStatus::Pending,
134        NodeStatus::Running => NodeVisualStatus::Running,
135        NodeStatus::Completed => NodeVisualStatus::Completed,
136        NodeStatus::Failed => NodeVisualStatus::Failed {
137            error: error.cloned().unwrap_or_default(),
138        },
139        NodeStatus::Skipped => NodeVisualStatus::Skipped,
140    }
141}
142
143impl WorkflowViewState {
144    /// Update from workflow definition
145    pub fn set_workflow(&mut self, def: WorkflowDef) {
146        self.workflow_def = Some(def.clone());
147        self.layout = compute_layout(&def);
148        self.visible = true;
149    }
150
151    /// Update from execution context
152    pub fn update_context(&mut self, context: WorkflowContext) {
153        self.context = Some(context);
154    }
155
156    /// Load workflow instances from persistence
157    /// Returns a list of recent workflow contexts (most recent first)
158    pub fn load_recent_instances(project_dir: Option<&PathBuf>) -> Vec<WorkflowContext> {
159        let persistence = WorkflowPersistence::new(project_dir);
160        if let Ok(contexts) = persistence.list() {
161            // Sort by updated_at descending (most recent first)
162            let mut sorted = contexts;
163            sorted.sort_by(|a, b| {
164                b.updated_at.cmp(&a.updated_at)
165            });
166            // Return only running/paused workflows, or the 5 most recent
167            let running: Vec<_> = sorted.iter()
168                .filter(|c| matches!(c.status, matrixcode_core::workflow::WorkflowStatus::Running | matrixcode_core::workflow::WorkflowStatus::Paused))
169                .cloned()
170                .collect();
171            if running.is_empty() {
172                sorted.into_iter().take(5).collect()
173            } else {
174                running
175            }
176        } else {
177            Vec::new()
178        }
179    }
180
181    /// Load workflow definition from registry
182    pub fn load_workflow_def(project_dir: Option<&PathBuf>, workflow_id: &str) -> Option<WorkflowDef> {
183        let registry = WorkflowRegistry::new(project_dir);
184        if let Some(info) = registry.get(workflow_id) {
185            matrixcode_core::workflow::parse_workflow_from_file(&info.path).ok()
186        } else {
187            None
188        }
189    }
190
191    /// Load and display the most recent workflow instance
192    pub fn load_most_recent(&mut self, project_dir: Option<&PathBuf>) {
193        let instances = Self::load_recent_instances(project_dir);
194        if let Some(ctx) = instances.first() {
195            // Load the workflow definition
196            if let Some(def) = Self::load_workflow_def(project_dir, &ctx.workflow_id) {
197                self.set_workflow(def);
198                self.update_context(ctx.clone());
199            }
200        }
201    }
202
203    /// Get node visual status
204    pub fn get_node_status(&self, node_id: &str) -> NodeVisualStatus {
205        if let Some(ctx) = &self.context
206            && let Some(exec) = ctx.node_executions.get(node_id) {
207                return to_visual_status(&exec.status, exec.error.as_ref());
208            }
209        NodeVisualStatus::Pending
210    }
211
212    /// Get progress percentage
213    pub fn progress(&self) -> (usize, usize) {
214        if let Some(ctx) = &self.context {
215            let total = self.layout.node_positions.len();
216            let completed = ctx.execution_path.len();
217            return (completed, total);
218        }
219        (0, self.layout.node_positions.len())
220    }
221
222    /// Advance spinner frame
223    pub fn advance_spinner(&mut self) {
224        self.spinner_frame = (self.spinner_frame + 1) % 10;
225    }
226
227    /// Get spinner char for current frame
228    pub fn spinner_char(&self) -> char {
229        const SPINNER: &[char] = &['⠁', '⠃', '⠇', '⡇', '⣇', '⣧', '⣷', '⣿', '⢟', '⠟'];
230        SPINNER[self.spinner_frame % SPINNER.len()]
231    }
232}
233
234/// Compute DAG layout from workflow definition
235fn compute_layout(def: &WorkflowDef) -> DagLayout {
236    let mut layout = DagLayout::default();
237
238    // Build layers using topological sort
239    let mut visited: HashMap<String, bool> = HashMap::new();
240    let mut layers: Vec<Vec<String>> = Vec::new();
241
242    // Find start node
243    let start_node = def.nodes.iter()
244        .find(|n| n.node_type == NodeType::Start)
245        .map(|n| n.id.clone());
246
247    if let Some(start) = start_node {
248        // BFS to assign layers
249        let mut current_layer = vec![start.clone()];
250        visited.insert(start.clone(), true);
251
252        while !current_layer.is_empty() {
253            layers.push(current_layer.clone());
254            let mut next_layer = Vec::new();
255
256            for node_id in &current_layer {
257                // Find edges from this node
258                for edge in &def.edges {
259                    if &edge.from == node_id && !visited.contains_key(&edge.to) {
260                        visited.insert(edge.to.clone(), true);
261                        next_layer.push(edge.to.clone());
262                    }
263                }
264            }
265
266            current_layer = next_layer;
267        }
268    }
269
270    // Assign positions based on layers
271    for (row, layer) in layers.iter().enumerate() {
272        for (col, node_id) in layer.iter().enumerate() {
273            layout.node_positions.insert(node_id.clone(), (row, col));
274        }
275    }
276
277    layout.layers = layers.clone();
278    layout.height = layers.len();
279    layout.width = layers.iter().map(|l| l.len()).max().unwrap_or(0);
280
281    // Build edge info
282    for edge in &def.edges {
283        layout.edges.push(EdgeInfo {
284            from: edge.from.clone(),
285            to: edge.to.clone(),
286            condition: edge.condition.clone(),
287        });
288    }
289
290    layout
291}