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