use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use yewdux::prelude::*;
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub enum ExecutionStatus {
#[default]
Pending,
Running,
Success,
Failed,
Timeout,
Cancelled,
}
impl ExecutionStatus {
pub fn is_terminal(&self) -> bool {
matches!(self, Self::Success | Self::Failed | Self::Timeout | Self::Cancelled)
}
pub fn is_success(&self) -> bool {
matches!(self, Self::Success)
}
pub fn as_str(&self) -> &'static str {
match self {
Self::Pending => "pending",
Self::Running => "running",
Self::Success => "success",
Self::Failed => "failed",
Self::Timeout => "timeout",
Self::Cancelled => "cancelled",
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ExecutionEntry {
pub id: String,
pub skill: String,
pub tool: String,
pub instance: String,
pub status: ExecutionStatus,
pub args: HashMap<String, String>,
pub output: Option<String>,
pub error: Option<String>,
pub duration_ms: u64,
pub started_at: String,
pub metadata: HashMap<String, String>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ActiveExecution {
pub entry: ExecutionEntry,
pub output_chunks: Vec<String>,
pub progress: Option<u8>,
}
#[derive(Clone, Debug, PartialEq, Store)]
pub struct ExecutionsStore {
pub history: Vec<ExecutionEntry>,
pub active: Option<ActiveExecution>,
pub loading: bool,
pub error: Option<String>,
pub skill_filter: Option<String>,
pub status_filter: Option<ExecutionStatus>,
pub max_history: usize,
}
impl Default for ExecutionsStore {
fn default() -> Self {
Self {
history: Vec::new(),
active: None,
loading: false,
error: None,
skill_filter: None,
status_filter: None,
max_history: 1000,
}
}
}
impl ExecutionsStore {
pub fn filtered_history(&self) -> Vec<&ExecutionEntry> {
self.history
.iter()
.filter(|entry| {
if let Some(ref skill) = self.skill_filter {
if &entry.skill != skill {
return false;
}
}
if let Some(ref status) = self.status_filter {
if &entry.status != status {
return false;
}
}
true
})
.collect()
}
pub fn get_execution(&self, id: &str) -> Option<&ExecutionEntry> {
self.history.iter().find(|e| e.id == id)
}
pub fn recent_for_skill(&self, skill: &str, limit: usize) -> Vec<&ExecutionEntry> {
self.history
.iter()
.filter(|e| e.skill == skill)
.take(limit)
.collect()
}
pub fn success_rate(&self) -> f32 {
if self.history.is_empty() {
return 0.0;
}
let successes = self.history.iter().filter(|e| e.status.is_success()).count();
successes as f32 / self.history.len() as f32
}
pub fn average_duration_ms(&self) -> u64 {
if self.history.is_empty() {
return 0;
}
let total: u64 = self.history.iter().map(|e| e.duration_ms).sum();
total / self.history.len() as u64
}
pub fn has_active(&self) -> bool {
self.active.is_some()
}
}
pub enum ExecutionsAction {
SetHistory(Vec<ExecutionEntry>),
AddExecution(ExecutionEntry),
UpdateExecution(ExecutionEntry),
RemoveExecution(String),
ClearHistory,
StartExecution(ExecutionEntry),
AppendOutput(String),
SetProgress(u8),
CompleteExecution(ExecutionEntry),
CancelExecution,
SetLoading(bool),
SetError(Option<String>),
SetSkillFilter(Option<String>),
SetStatusFilter(Option<ExecutionStatus>),
ClearFilters,
}
impl Reducer<ExecutionsStore> for ExecutionsAction {
fn apply(self, mut store: std::rc::Rc<ExecutionsStore>) -> std::rc::Rc<ExecutionsStore> {
let state = std::rc::Rc::make_mut(&mut store);
match self {
ExecutionsAction::SetHistory(history) => {
state.history = history;
state.loading = false;
state.error = None;
}
ExecutionsAction::AddExecution(entry) => {
state.history.insert(0, entry);
if state.history.len() > state.max_history {
state.history.truncate(state.max_history);
}
}
ExecutionsAction::UpdateExecution(entry) => {
if let Some(existing) = state.history.iter_mut().find(|e| e.id == entry.id) {
*existing = entry;
}
}
ExecutionsAction::RemoveExecution(id) => {
state.history.retain(|e| e.id != id);
}
ExecutionsAction::ClearHistory => {
state.history.clear();
}
ExecutionsAction::StartExecution(entry) => {
state.active = Some(ActiveExecution {
entry,
output_chunks: Vec::new(),
progress: None,
});
}
ExecutionsAction::AppendOutput(chunk) => {
if let Some(ref mut active) = state.active {
active.output_chunks.push(chunk);
}
}
ExecutionsAction::SetProgress(progress) => {
if let Some(ref mut active) = state.active {
active.progress = Some(progress.min(100));
}
}
ExecutionsAction::CompleteExecution(entry) => {
state.history.insert(0, entry);
if state.history.len() > state.max_history {
state.history.truncate(state.max_history);
}
state.active = None;
}
ExecutionsAction::CancelExecution => {
if let Some(active) = state.active.take() {
let mut entry = active.entry;
entry.status = ExecutionStatus::Cancelled;
state.history.insert(0, entry);
}
}
ExecutionsAction::SetLoading(loading) => {
state.loading = loading;
}
ExecutionsAction::SetError(error) => {
state.error = error;
state.loading = false;
}
ExecutionsAction::SetSkillFilter(filter) => {
state.skill_filter = filter;
}
ExecutionsAction::SetStatusFilter(filter) => {
state.status_filter = filter;
}
ExecutionsAction::ClearFilters => {
state.skill_filter = None;
state.status_filter = None;
}
}
store
}
}