vtcode_exec_events/tracker.rs
1//! Canonical durable contract for task-tracker checklist snapshots.
2//!
3//! Every successful `task_tracker` mutation publishes a [`TrackerSnapshot`]
4//! through [`ThreadEvent::TrackerUpdated`](crate::ThreadEvent::TrackerUpdated)
5//! so timeline consumers (desktop Tasks viewer, session archives) replay typed
6//! checklist state instead of re-parsing rendered tool output. The snapshot
7//! carries the user-facing surface only — title, progress counts, and the
8//! compact item tree — mirroring the CLI transcript/panel split; structured
9//! step metadata (`files`/`outcome`/`verify`) stays in the tool payload and
10//! the persisted tracker artifacts.
11use serde::{Deserialize, Serialize};
12use std::str::FromStr;
13
14/// Lifecycle status of one checklist item, using the canonical `task_tracker`
15/// wire strings (`pending`, `in_progress`, `completed`, `blocked`).
16#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
17#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
18#[serde(rename_all = "snake_case")]
19pub enum TrackerItemStatus {
20 /// Item is queued and not started.
21 #[default]
22 Pending,
23 /// Item is actively being worked on.
24 InProgress,
25 /// Item finished successfully.
26 Completed,
27 /// Item cannot proceed without external input.
28 Blocked,
29 /// Catch-all for statuses added in newer schema versions.
30 #[serde(other)]
31 Unknown,
32}
33
34impl TrackerItemStatus {
35 /// Canonical wire string for this status.
36 #[must_use]
37 pub const fn as_str(self) -> &'static str {
38 match self {
39 Self::Pending => "pending",
40 Self::InProgress => "in_progress",
41 Self::Completed => "completed",
42 Self::Blocked => "blocked",
43 Self::Unknown => "unknown",
44 }
45 }
46}
47
48impl FromStr for TrackerItemStatus {
49 type Err = ();
50
51 /// Parse a canonical status string; unknown shapes fail closed to `Err`
52 /// so producers skip unparseable items instead of mislabeling them.
53 fn from_str(raw: &str) -> Result<Self, Self::Err> {
54 match raw {
55 "pending" => Ok(Self::Pending),
56 "in_progress" => Ok(Self::InProgress),
57 "completed" => Ok(Self::Completed),
58 "blocked" => Ok(Self::Blocked),
59 _ => Err(()),
60 }
61 }
62}
63
64/// One checklist row in a tracker snapshot.
65#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
66#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
67pub struct TrackerItemState {
68 /// Dotted (`1.2`) or flat (`1`) position of the item in the checklist.
69 pub index_path: String,
70 /// Short leading-clause description of the item.
71 pub description: String,
72 /// Lifecycle status of the item.
73 pub status: TrackerItemStatus,
74}
75
76/// Bounded checklist snapshot published after a `task_tracker` mutation.
77///
78/// `completed`/`total` always describe the full checklist even when `items`
79/// is empty (explicit counts win over derived ones, mirroring the CLI
80/// progress line). Derived views replay the newest snapshot per session.
81#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
82#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
83pub struct TrackerSnapshot {
84 /// Raw checklist title; views humanize generated timestamp slugs.
85 pub title: String,
86 /// Finished items in the full checklist.
87 pub completed: usize,
88 /// Renderable items in the full checklist.
89 pub total: usize,
90 /// Item tree in checklist order. Tool-assigned index paths are unique
91 /// within a snapshot, so views may key rows on them.
92 pub items: Vec<TrackerItemState>,
93}