Skip to main content

yarli_cli/stream/
events.rs

1//! Stream event types — the renderer's input protocol.
2//!
3//! The stream renderer receives `StreamEvent`s from the scheduler/executor
4//! and renders them either to the inline viewport (transient) or pushes
5//! completed transitions to scrollback (permanent).
6
7use std::time::Duration;
8
9use chrono::{DateTime, Utc};
10use uuid::Uuid;
11
12use crate::yarli_core::domain::TaskId;
13use crate::yarli_core::entities::ContinuationPayload;
14use crate::yarli_core::fsm::run::RunState;
15use crate::yarli_core::fsm::task::TaskState;
16
17/// Events the stream renderer can consume.
18#[derive(Debug, Clone)]
19pub enum StreamEvent {
20    /// A task is known/planned for this run before transitions are emitted.
21    TaskDiscovered {
22        task_id: TaskId,
23        task_name: String,
24        depends_on: Vec<String>,
25    },
26
27    /// A task changed state (Section 33: single structured line per transition).
28    TaskTransition {
29        task_id: TaskId,
30        task_name: String,
31        from: TaskState,
32        to: TaskState,
33        elapsed: Option<Duration>,
34        exit_code: Option<i32>,
35        detail: Option<String>,
36        at: DateTime<Utc>,
37    },
38
39    /// A run changed state.
40    RunTransition {
41        run_id: Uuid,
42        from: RunState,
43        to: RunState,
44        reason: Option<String>,
45        at: DateTime<Utc>,
46    },
47
48    /// Live command output from an executing task.
49    CommandOutput {
50        task_id: TaskId,
51        task_name: String,
52        line: String,
53    },
54
55    /// A transient status message (e.g. "connecting to postgres...").
56    /// Shows only in inline viewport, never pushed to scrollback.
57    TransientStatus { message: String },
58
59    /// A "Why Not Done?" summary update.
60    ExplainUpdate { summary: String },
61
62    /// A task's worker assignment for display.
63    TaskWorker { task_id: TaskId, worker_id: String },
64
65    /// Run started — emitted once at the beginning so operators know the run ID.
66    RunStarted {
67        run_id: Uuid,
68        objective: String,
69        at: DateTime<Utc>,
70    },
71
72    /// Run exited — structured continuation payload for downstream tooling.
73    RunExited { payload: ContinuationPayload },
74
75    /// Tick event — advance spinners and refresh viewport.
76    Tick,
77}
78
79/// Snapshot of a task's current state for viewport rendering.
80#[derive(Debug, Clone)]
81pub struct TaskView {
82    pub task_id: TaskId,
83    pub name: String,
84    pub state: TaskState,
85    pub elapsed: Option<Duration>,
86    pub last_output_line: Option<String>,
87    pub blocked_by: Option<String>,
88    pub worker_id: Option<String>,
89}
90
91impl TaskView {
92    /// Is this task currently active (executing or waiting)?
93    pub fn is_active(&self) -> bool {
94        matches!(
95            self.state,
96            TaskState::TaskExecuting | TaskState::TaskWaiting
97        )
98    }
99
100    /// Is this task terminal (complete, failed, cancelled)?
101    pub fn is_terminal(&self) -> bool {
102        self.state.is_terminal()
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn task_view_active_states() {
112        let mut v = TaskView {
113            task_id: Uuid::new_v4(),
114            name: "test".into(),
115            state: TaskState::TaskExecuting,
116            elapsed: None,
117            last_output_line: None,
118            blocked_by: None,
119            worker_id: None,
120        };
121        assert!(v.is_active());
122
123        v.state = TaskState::TaskWaiting;
124        assert!(v.is_active());
125
126        v.state = TaskState::TaskReady;
127        assert!(!v.is_active());
128    }
129
130    #[test]
131    fn task_view_terminal_states() {
132        let mut v = TaskView {
133            task_id: Uuid::new_v4(),
134            name: "test".into(),
135            state: TaskState::TaskComplete,
136            elapsed: None,
137            last_output_line: None,
138            blocked_by: None,
139            worker_id: None,
140        };
141        assert!(v.is_terminal());
142
143        v.state = TaskState::TaskFailed;
144        assert!(v.is_terminal());
145
146        v.state = TaskState::TaskExecuting;
147        assert!(!v.is_terminal());
148    }
149}