1use serde::{Deserialize, Serialize};
4use std::time::{Duration, SystemTime};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
8pub struct TaskId(pub u64);
9
10impl TaskId {
11 pub fn new() -> Self {
13 use std::sync::atomic::{AtomicU64, Ordering};
14 static COUNTER: AtomicU64 = AtomicU64::new(1);
15 Self(COUNTER.fetch_add(1, Ordering::SeqCst))
16 }
17}
18
19impl Default for TaskId {
20 fn default() -> Self {
21 Self::new()
22 }
23}
24
25impl std::fmt::Display for TaskId {
26 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27 write!(f, "#{}", self.0)
28 }
29}
30
31#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
33pub enum TaskStatus {
34 Pending,
36
37 Running {
39 progress: Option<f32>,
41 message: Option<String>,
43 },
44
45 Background {
47 progress: Option<f32>,
48 message: Option<String>,
49 },
50
51 Paused { reason: Option<String> },
53
54 Completed {
56 summary: Option<String>,
58 output: Option<String>,
60 },
61
62 Failed {
64 error: String,
65 retryable: bool,
67 },
68
69 Cancelled,
71
72 WaitingForInput { prompt: String },
74}
75
76impl TaskStatus {
77 pub fn is_terminal(&self) -> bool {
79 matches!(
80 self,
81 Self::Completed { .. } | Self::Failed { .. } | Self::Cancelled
82 )
83 }
84
85 pub fn is_running(&self) -> bool {
87 matches!(self, Self::Running { .. } | Self::Background { .. })
88 }
89
90 pub fn is_foreground(&self) -> bool {
92 matches!(self, Self::Running { .. } | Self::WaitingForInput { .. })
93 }
94
95 pub fn progress(&self) -> Option<f32> {
97 match self {
98 Self::Running { progress, .. } | Self::Background { progress, .. } => *progress,
99 Self::Completed { .. } => Some(1.0),
100 _ => None,
101 }
102 }
103
104 pub fn message(&self) -> Option<&str> {
106 match self {
107 Self::Running { message, .. } | Self::Background { message, .. } => message.as_deref(),
108 Self::Paused { reason } => reason.as_deref(),
109 Self::Completed { summary, .. } => summary.as_deref(),
110 Self::Failed { error, .. } => Some(error.as_str()),
111 Self::WaitingForInput { prompt } => Some(prompt.as_str()),
112 _ => None,
113 }
114 }
115}
116
117#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
119pub enum TaskKind {
120 Command { command: String, pid: Option<u32> },
122
123 SubAgent {
125 session_key: String,
126 label: Option<String>,
127 },
128
129 CronJob {
131 job_id: String,
132 job_name: Option<String>,
133 },
134
135 McpTool { server: String, tool: String },
137
138 Browser { action: String, url: Option<String> },
140
141 FileOp { operation: String, path: String },
143
144 WebRequest { url: String, method: String },
146
147 Custom {
149 name: String,
150 details: Option<String>,
151 },
152}
153
154impl TaskKind {
155 pub fn display_name(&self) -> &str {
157 match self {
158 Self::Command { .. } => "Command",
159 Self::SubAgent { .. } => "Sub-agent",
160 Self::CronJob { .. } => "Cron job",
161 Self::McpTool { .. } => "MCP",
162 Self::Browser { .. } => "Browser",
163 Self::FileOp { .. } => "File",
164 Self::WebRequest { .. } => "Web",
165 Self::Custom { name, .. } => name.as_str(),
166 }
167 }
168
169 pub fn description(&self) -> String {
171 match self {
172 Self::Command { command, pid } => {
173 if let Some(p) = pid {
174 format!("{} (pid {})", command, p)
175 } else {
176 command.clone()
177 }
178 }
179 Self::SubAgent { label, session_key } => {
180 label.clone().unwrap_or_else(|| session_key.clone())
181 }
182 Self::CronJob { job_name, job_id } => {
183 job_name.clone().unwrap_or_else(|| job_id.clone())
184 }
185 Self::McpTool { server, tool } => format!("{}:{}", server, tool),
186 Self::Browser { action, url } => {
187 if let Some(u) = url {
188 format!("{} {}", action, u)
189 } else {
190 action.clone()
191 }
192 }
193 Self::FileOp { operation, path } => format!("{} {}", operation, path),
194 Self::WebRequest { method, url } => format!("{} {}", method, url),
195 Self::Custom { name, details } => {
196 if let Some(d) = details {
197 format!("{}: {}", name, d)
198 } else {
199 name.clone()
200 }
201 }
202 }
203 }
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct TaskProgress {
209 pub fraction: Option<f32>,
211
212 pub steps: Option<(u32, u32)>,
214
215 pub bytes: Option<(u64, u64)>,
217
218 pub items: Option<(u32, u32)>,
220
221 pub eta_secs: Option<u64>,
223
224 pub message: Option<String>,
226}
227
228impl TaskProgress {
229 pub fn indeterminate() -> Self {
231 Self {
232 fraction: None,
233 steps: None,
234 bytes: None,
235 items: None,
236 eta_secs: None,
237 message: None,
238 }
239 }
240
241 pub fn fraction(f: f32) -> Self {
243 Self {
244 fraction: Some(f.clamp(0.0, 1.0)),
245 ..Self::indeterminate()
246 }
247 }
248
249 pub fn steps(current: u32, total: u32) -> Self {
251 let frac = if total > 0 {
252 Some(current as f32 / total as f32)
253 } else {
254 None
255 };
256 Self {
257 fraction: frac,
258 steps: Some((current, total)),
259 ..Self::indeterminate()
260 }
261 }
262
263 pub fn with_message(mut self, msg: impl Into<String>) -> Self {
265 self.message = Some(msg.into());
266 self
267 }
268
269 pub fn with_eta(mut self, secs: u64) -> Self {
271 self.eta_secs = Some(secs);
272 self
273 }
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct Task {
279 pub id: TaskId,
281
282 pub kind: TaskKind,
284
285 pub status: TaskStatus,
287
288 #[serde(with = "system_time_serde")]
290 pub created_at: SystemTime,
291
292 #[serde(with = "option_system_time_serde")]
294 pub started_at: Option<SystemTime>,
295
296 #[serde(with = "option_system_time_serde")]
298 pub finished_at: Option<SystemTime>,
299
300 pub session_key: Option<String>,
302
303 pub label: Option<String>,
305
306 pub description: Option<String>,
308
309 pub stream_output: bool,
311
312 #[serde(skip)]
314 pub output_buffer: String,
315}
316
317impl Task {
318 pub fn new(kind: TaskKind) -> Self {
320 Self {
321 id: TaskId::new(),
322 kind,
323 status: TaskStatus::Pending,
324 created_at: SystemTime::now(),
325 started_at: None,
326 finished_at: None,
327 session_key: None,
328 label: None,
329 description: None,
330 stream_output: false,
331 output_buffer: String::new(),
332 }
333 }
334
335 pub fn with_session(mut self, key: impl Into<String>) -> Self {
337 self.session_key = Some(key.into());
338 self
339 }
340
341 pub fn with_label(mut self, label: impl Into<String>) -> Self {
343 self.label = Some(label.into());
344 self
345 }
346
347 pub fn with_description(mut self, desc: impl Into<String>) -> Self {
349 self.description = Some(desc.into());
350 self
351 }
352
353 pub fn with_streaming(mut self) -> Self {
355 self.stream_output = true;
356 self
357 }
358
359 pub fn start(&mut self) {
361 self.started_at = Some(SystemTime::now());
362 self.status = TaskStatus::Running {
363 progress: None,
364 message: None,
365 };
366 }
367
368 pub fn background(&mut self) {
370 if let TaskStatus::Running { progress, message } = &self.status {
371 self.status = TaskStatus::Background {
372 progress: *progress,
373 message: message.clone(),
374 };
375 }
376 }
377
378 pub fn foreground(&mut self) {
380 if let TaskStatus::Background { progress, message } = &self.status {
381 self.status = TaskStatus::Running {
382 progress: *progress,
383 message: message.clone(),
384 };
385 }
386 }
387
388 pub fn update_progress(&mut self, progress: TaskProgress) {
390 match &mut self.status {
391 TaskStatus::Running {
392 progress: p,
393 message: m,
394 }
395 | TaskStatus::Background {
396 progress: p,
397 message: m,
398 } => {
399 *p = progress.fraction;
400 if progress.message.is_some() {
401 *m = progress.message;
402 }
403 }
404 _ => {}
405 }
406 }
407
408 pub fn complete(&mut self, summary: Option<String>) {
410 self.finished_at = Some(SystemTime::now());
411 let output = if self.output_buffer.is_empty() {
412 None
413 } else {
414 Some(std::mem::take(&mut self.output_buffer))
415 };
416 self.status = TaskStatus::Completed { summary, output };
417 }
418
419 pub fn fail(&mut self, error: impl Into<String>, retryable: bool) {
421 self.finished_at = Some(SystemTime::now());
422 self.status = TaskStatus::Failed {
423 error: error.into(),
424 retryable,
425 };
426 }
427
428 pub fn cancel(&mut self) {
430 self.finished_at = Some(SystemTime::now());
431 self.status = TaskStatus::Cancelled;
432 }
433
434 pub fn elapsed(&self) -> Option<Duration> {
436 self.started_at.map(|start| {
437 let end = self.finished_at.unwrap_or_else(SystemTime::now);
438 end.duration_since(start).unwrap_or_default()
439 })
440 }
441
442 pub fn display_label(&self) -> String {
444 self.label
445 .clone()
446 .unwrap_or_else(|| self.kind.description())
447 }
448
449 pub fn display_description(&self) -> String {
451 self.description
452 .clone()
453 .unwrap_or_else(|| self.display_label())
454 }
455}
456
457mod system_time_serde {
459 use serde::{Deserialize, Deserializer, Serialize, Serializer};
460 use std::time::{SystemTime, UNIX_EPOCH};
461
462 pub fn serialize<S: Serializer>(time: &SystemTime, ser: S) -> Result<S::Ok, S::Error> {
463 let millis = time
464 .duration_since(UNIX_EPOCH)
465 .unwrap_or_default()
466 .as_millis() as u64;
467 millis.serialize(ser)
468 }
469
470 pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<SystemTime, D::Error> {
471 let millis = u64::deserialize(de)?;
472 Ok(UNIX_EPOCH + std::time::Duration::from_millis(millis))
473 }
474}
475
476mod option_system_time_serde {
477 use serde::{Deserialize, Deserializer, Serialize, Serializer};
478 use std::time::{SystemTime, UNIX_EPOCH};
479
480 pub fn serialize<S: Serializer>(time: &Option<SystemTime>, ser: S) -> Result<S::Ok, S::Error> {
481 match time {
482 Some(t) => {
483 let millis = t.duration_since(UNIX_EPOCH).unwrap_or_default().as_millis() as u64;
484 Some(millis).serialize(ser)
485 }
486 None => None::<u64>.serialize(ser),
487 }
488 }
489
490 pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<Option<SystemTime>, D::Error> {
491 let millis: Option<u64> = Option::deserialize(de)?;
492 Ok(millis.map(|m| UNIX_EPOCH + std::time::Duration::from_millis(m)))
493 }
494}