返回 CodeWhale
digest.rs
根目录 / crates / tui / src / work_graph / digest.rs
1 //! Plain-text projection of canonical Work Graph and To-do state.
2
3 use crate::tools::todo::{TodoItem, TodoListSnapshot, TodoStatus};
4
5 use super::{NodeKind, NodeState, WorkGraphSnapshot, WorkRuntimeSnapshot};
6
7 #[must_use]
8 pub fn format_operation_digest(snapshot: Option<&WorkRuntimeSnapshot>) -> String {
9 let Some(snapshot) = snapshot else {
10 return "No active operations or to-do items.".to_string();
11 };
12 format_operation_digest_parts(&snapshot.graph, &snapshot.todos)
13 }
14
15 #[must_use]
16 pub fn format_operation_digest_parts(
17 graph: &WorkGraphSnapshot,
18 todos: &TodoListSnapshot,
19 ) -> String {
20 let mut operations = graph
21 .nodes
22 .iter()
23 .filter(|node| node.kind == NodeKind::Operation)
24 .collect::<Vec<_>>();
25 operations.sort_by_key(|node| (state_rank(node.state), node.updated_at));
26
27 let mut todo_items = todos.items.iter().collect::<Vec<_>>();
28 todo_items.sort_by_key(|item| (todo_rank(item), item.id));
29
30 if operations.is_empty() && todo_items.is_empty() {
31 return "No active operations or to-do items.".to_string();
32 }
33
34 let mut out = String::from("Operation digest\n");
35 if !operations.is_empty() {
36 out.push_str("\nOperations\n");
37 for node in operations {
38 let owner = node
39 .binding
40 .as_ref()
41 .map_or("unbound", |binding| binding.external.as_str());
42 out.push_str(&format!(
43 " {:<10} {} · {}\n",
44 state_label(node.state),
45 owner,
46 one_line(&node.title)
47 ));
48 }
49 }
50 if !todo_items.is_empty() {
51 out.push_str("\nTo-do\n");
52 for item in todo_items {
53 out.push_str(&format!(
54 " {:<10} #{} · {}\n",
55 todo_label(item.status),
56 item.id,
57 one_line(&item.content)
58 ));
59 }
60 }
61 out.trim_end().to_string()
62 }
63
64 const fn state_rank(state: NodeState) -> u8 {
65 match state {
66 NodeState::Active | NodeState::Initializing => 0,
67 NodeState::Waiting => 1,
68 NodeState::Blocked | NodeState::Stale => 2,
69 NodeState::Ready => 3,
70 NodeState::Failed => 4,
71 NodeState::Completed => 5,
72 NodeState::Verified => 6,
73 NodeState::Cancelled | NodeState::Superseded => 7,
74 }
75 }
76
77 const fn state_label(state: NodeState) -> &'static str {
78 match state {
79 NodeState::Ready => "ready",
80 NodeState::Initializing => "starting",
81 NodeState::Active => "running",
82 NodeState::Waiting => "waiting",
83 NodeState::Blocked => "blocked",
84 NodeState::Completed => "ended",
85 NodeState::Verified => "verified",
86 NodeState::Stale => "stale",
87 NodeState::Superseded => "superseded",
88 NodeState::Cancelled => "cancelled",
89 NodeState::Failed => "failed",
90 }
91 }
92
93 const fn todo_rank(item: &TodoItem) -> u8 {
94 match item.status {
95 TodoStatus::InProgress => 0,
96 TodoStatus::Pending => 1,
97 TodoStatus::Completed => 2,
98 TodoStatus::Cancelled => 3,
99 }
100 }
101
102 const fn todo_label(status: TodoStatus) -> &'static str {
103 match status {
104 TodoStatus::Pending => "pending",
105 TodoStatus::InProgress => "running",
106 TodoStatus::Completed => "completed",
107 TodoStatus::Cancelled => "cancelled",
108 }
109 }
110
111 fn one_line(value: &str) -> String {
112 value.split_whitespace().collect::<Vec<_>>().join(" ")
113 }
114
115 #[cfg(test)]
116 mod tests {
117 use super::*;
118 use crate::tools::todo::TodoItem;
119
120 #[test]
121 fn digest_orders_running_work_first_and_distinguishes_cancelled() {
122 let todos = TodoListSnapshot {
123 items: vec![
124 TodoItem {
125 id: 1,
126 content: "later".into(),
127 status: TodoStatus::Pending,
128 },
129 TodoItem {
130 id: 2,
131 content: "now".into(),
132 status: TodoStatus::InProgress,
133 },
134 TodoItem {
135 id: 3,
136 content: "dropped".into(),
137 status: TodoStatus::Cancelled,
138 },
139 ],
140 completion_pct: 0,
141 in_progress_id: Some(2),
142 };
143 let text = format_operation_digest_parts(&WorkGraphSnapshot::new(), &todos);
144 assert!(text.find("#2 · now").unwrap() < text.find("#1 · later").unwrap());
145 assert!(text.contains("cancelled #3 · dropped"));
146 assert!(!text.contains('\u{1b}'));
147 }
148 }
149
149 lines RUST