skill_web/store/
executions.rs

1//! Executions state store
2//!
3//! Manages execution history and active executions.
4
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use yewdux::prelude::*;
8
9/// Execution status
10#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
11pub enum ExecutionStatus {
12    #[default]
13    Pending,
14    Running,
15    Success,
16    Failed,
17    Timeout,
18    Cancelled,
19}
20
21impl ExecutionStatus {
22    pub fn is_terminal(&self) -> bool {
23        matches!(self, Self::Success | Self::Failed | Self::Timeout | Self::Cancelled)
24    }
25
26    pub fn is_success(&self) -> bool {
27        matches!(self, Self::Success)
28    }
29
30    pub fn as_str(&self) -> &'static str {
31        match self {
32            Self::Pending => "pending",
33            Self::Running => "running",
34            Self::Success => "success",
35            Self::Failed => "failed",
36            Self::Timeout => "timeout",
37            Self::Cancelled => "cancelled",
38        }
39    }
40}
41
42/// Execution history entry
43#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
44pub struct ExecutionEntry {
45    /// Unique execution ID
46    pub id: String,
47    /// Skill name
48    pub skill: String,
49    /// Tool name
50    pub tool: String,
51    /// Instance used
52    pub instance: String,
53    /// Execution status
54    pub status: ExecutionStatus,
55    /// Input arguments
56    pub args: HashMap<String, String>,
57    /// Output content (if completed)
58    pub output: Option<String>,
59    /// Error message (if failed)
60    pub error: Option<String>,
61    /// Duration in milliseconds
62    pub duration_ms: u64,
63    /// When the execution started
64    pub started_at: String,
65    /// Additional metadata
66    pub metadata: HashMap<String, String>,
67}
68
69/// Active execution state (for real-time updates)
70#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
71pub struct ActiveExecution {
72    /// Execution entry
73    pub entry: ExecutionEntry,
74    /// Streaming output chunks
75    pub output_chunks: Vec<String>,
76    /// Progress percentage (0-100)
77    pub progress: Option<u8>,
78}
79
80/// Executions store state
81#[derive(Clone, Debug, PartialEq, Store)]
82pub struct ExecutionsStore {
83    /// Execution history (most recent first)
84    pub history: Vec<ExecutionEntry>,
85    /// Currently active execution (if any)
86    pub active: Option<ActiveExecution>,
87    /// Whether history is being loaded
88    pub loading: bool,
89    /// Error message
90    pub error: Option<String>,
91    /// Filter by skill
92    pub skill_filter: Option<String>,
93    /// Filter by status
94    pub status_filter: Option<ExecutionStatus>,
95    /// Maximum history entries to keep
96    pub max_history: usize,
97}
98
99impl Default for ExecutionsStore {
100    fn default() -> Self {
101        Self {
102            history: Vec::new(),
103            active: None,
104            loading: false,
105            error: None,
106            skill_filter: None,
107            status_filter: None,
108            max_history: 1000,
109        }
110    }
111}
112
113impl ExecutionsStore {
114    /// Get filtered history
115    pub fn filtered_history(&self) -> Vec<&ExecutionEntry> {
116        self.history
117            .iter()
118            .filter(|entry| {
119                // Skill filter
120                if let Some(ref skill) = self.skill_filter {
121                    if &entry.skill != skill {
122                        return false;
123                    }
124                }
125
126                // Status filter
127                if let Some(ref status) = self.status_filter {
128                    if &entry.status != status {
129                        return false;
130                    }
131                }
132
133                true
134            })
135            .collect()
136    }
137
138    /// Get execution by ID
139    pub fn get_execution(&self, id: &str) -> Option<&ExecutionEntry> {
140        self.history.iter().find(|e| e.id == id)
141    }
142
143    /// Get recent executions for a skill
144    pub fn recent_for_skill(&self, skill: &str, limit: usize) -> Vec<&ExecutionEntry> {
145        self.history
146            .iter()
147            .filter(|e| e.skill == skill)
148            .take(limit)
149            .collect()
150    }
151
152    /// Get success rate
153    pub fn success_rate(&self) -> f32 {
154        if self.history.is_empty() {
155            return 0.0;
156        }
157        let successes = self.history.iter().filter(|e| e.status.is_success()).count();
158        successes as f32 / self.history.len() as f32
159    }
160
161    /// Get average duration
162    pub fn average_duration_ms(&self) -> u64 {
163        if self.history.is_empty() {
164            return 0;
165        }
166        let total: u64 = self.history.iter().map(|e| e.duration_ms).sum();
167        total / self.history.len() as u64
168    }
169
170    /// Check if there's an active execution
171    pub fn has_active(&self) -> bool {
172        self.active.is_some()
173    }
174}
175
176/// Executions store actions
177pub enum ExecutionsAction {
178    /// Set entire history
179    SetHistory(Vec<ExecutionEntry>),
180    /// Add execution to history
181    AddExecution(ExecutionEntry),
182    /// Update an execution
183    UpdateExecution(ExecutionEntry),
184    /// Remove execution by ID
185    RemoveExecution(String),
186    /// Clear all history
187    ClearHistory,
188    /// Start a new execution
189    StartExecution(ExecutionEntry),
190    /// Update active execution output
191    AppendOutput(String),
192    /// Update active execution progress
193    SetProgress(u8),
194    /// Complete active execution
195    CompleteExecution(ExecutionEntry),
196    /// Cancel active execution
197    CancelExecution,
198    /// Set loading state
199    SetLoading(bool),
200    /// Set error
201    SetError(Option<String>),
202    /// Set skill filter
203    SetSkillFilter(Option<String>),
204    /// Set status filter
205    SetStatusFilter(Option<ExecutionStatus>),
206    /// Clear filters
207    ClearFilters,
208}
209
210impl Reducer<ExecutionsStore> for ExecutionsAction {
211    fn apply(self, mut store: std::rc::Rc<ExecutionsStore>) -> std::rc::Rc<ExecutionsStore> {
212        let state = std::rc::Rc::make_mut(&mut store);
213
214        match self {
215            ExecutionsAction::SetHistory(history) => {
216                state.history = history;
217                state.loading = false;
218                state.error = None;
219            }
220            ExecutionsAction::AddExecution(entry) => {
221                // Add to front (most recent first)
222                state.history.insert(0, entry);
223                // Trim to max
224                if state.history.len() > state.max_history {
225                    state.history.truncate(state.max_history);
226                }
227            }
228            ExecutionsAction::UpdateExecution(entry) => {
229                if let Some(existing) = state.history.iter_mut().find(|e| e.id == entry.id) {
230                    *existing = entry;
231                }
232            }
233            ExecutionsAction::RemoveExecution(id) => {
234                state.history.retain(|e| e.id != id);
235            }
236            ExecutionsAction::ClearHistory => {
237                state.history.clear();
238            }
239            ExecutionsAction::StartExecution(entry) => {
240                state.active = Some(ActiveExecution {
241                    entry,
242                    output_chunks: Vec::new(),
243                    progress: None,
244                });
245            }
246            ExecutionsAction::AppendOutput(chunk) => {
247                if let Some(ref mut active) = state.active {
248                    active.output_chunks.push(chunk);
249                }
250            }
251            ExecutionsAction::SetProgress(progress) => {
252                if let Some(ref mut active) = state.active {
253                    active.progress = Some(progress.min(100));
254                }
255            }
256            ExecutionsAction::CompleteExecution(entry) => {
257                // Add to history
258                state.history.insert(0, entry);
259                if state.history.len() > state.max_history {
260                    state.history.truncate(state.max_history);
261                }
262                // Clear active
263                state.active = None;
264            }
265            ExecutionsAction::CancelExecution => {
266                if let Some(active) = state.active.take() {
267                    // Add cancelled execution to history
268                    let mut entry = active.entry;
269                    entry.status = ExecutionStatus::Cancelled;
270                    state.history.insert(0, entry);
271                }
272            }
273            ExecutionsAction::SetLoading(loading) => {
274                state.loading = loading;
275            }
276            ExecutionsAction::SetError(error) => {
277                state.error = error;
278                state.loading = false;
279            }
280            ExecutionsAction::SetSkillFilter(filter) => {
281                state.skill_filter = filter;
282            }
283            ExecutionsAction::SetStatusFilter(filter) => {
284                state.status_filter = filter;
285            }
286            ExecutionsAction::ClearFilters => {
287                state.skill_filter = None;
288                state.status_filter = None;
289            }
290        }
291
292        store
293    }
294}