1use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use yewdux::prelude::*;
8
9#[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#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
44pub struct ExecutionEntry {
45 pub id: String,
47 pub skill: String,
49 pub tool: String,
51 pub instance: String,
53 pub status: ExecutionStatus,
55 pub args: HashMap<String, String>,
57 pub output: Option<String>,
59 pub error: Option<String>,
61 pub duration_ms: u64,
63 pub started_at: String,
65 pub metadata: HashMap<String, String>,
67}
68
69#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
71pub struct ActiveExecution {
72 pub entry: ExecutionEntry,
74 pub output_chunks: Vec<String>,
76 pub progress: Option<u8>,
78}
79
80#[derive(Clone, Debug, PartialEq, Store)]
82pub struct ExecutionsStore {
83 pub history: Vec<ExecutionEntry>,
85 pub active: Option<ActiveExecution>,
87 pub loading: bool,
89 pub error: Option<String>,
91 pub skill_filter: Option<String>,
93 pub status_filter: Option<ExecutionStatus>,
95 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 pub fn filtered_history(&self) -> Vec<&ExecutionEntry> {
116 self.history
117 .iter()
118 .filter(|entry| {
119 if let Some(ref skill) = self.skill_filter {
121 if &entry.skill != skill {
122 return false;
123 }
124 }
125
126 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 pub fn get_execution(&self, id: &str) -> Option<&ExecutionEntry> {
140 self.history.iter().find(|e| e.id == id)
141 }
142
143 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 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 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 pub fn has_active(&self) -> bool {
172 self.active.is_some()
173 }
174}
175
176pub enum ExecutionsAction {
178 SetHistory(Vec<ExecutionEntry>),
180 AddExecution(ExecutionEntry),
182 UpdateExecution(ExecutionEntry),
184 RemoveExecution(String),
186 ClearHistory,
188 StartExecution(ExecutionEntry),
190 AppendOutput(String),
192 SetProgress(u8),
194 CompleteExecution(ExecutionEntry),
196 CancelExecution,
198 SetLoading(bool),
200 SetError(Option<String>),
202 SetSkillFilter(Option<String>),
204 SetStatusFilter(Option<ExecutionStatus>),
206 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 state.history.insert(0, entry);
223 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 state.history.insert(0, entry);
259 if state.history.len() > state.max_history {
260 state.history.truncate(state.max_history);
261 }
262 state.active = None;
264 }
265 ExecutionsAction::CancelExecution => {
266 if let Some(active) = state.active.take() {
267 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}