1use anyhow::Context;
5use serde_json::Value;
6
7use super::{ClientCmd, LockCmd, NoteCmd, TaskCmd};
8
9pub(super) fn field<'v>(v: &'v Value, key: &str) -> &'v str {
10 v.get(key).and_then(Value::as_str).unwrap_or("")
11}
12
13fn render_messages(value: &Value) {
14 let messages = value["messages"].as_array().cloned().unwrap_or_default();
15 if messages.is_empty() {
16 println!("(no messages)");
17 return;
18 }
19 for m in &messages {
20 let target = m["channel"]
21 .as_str()
22 .map(|c| format!("#{c}"))
23 .or_else(|| m["to"].as_str().map(|t| format!("@{t}")))
24 .unwrap_or_default();
25 println!(
26 "[{}] {} {} → {}: {}",
27 m["id"],
28 field(m, "created_at"),
29 field(m, "from"),
30 target,
31 field(m, "body"),
32 );
33 }
34 if value["truncated"].as_bool() == Some(true) {
35 println!("(truncated at limit; raise --limit to see more)");
36 }
37}
38
39fn render_task_line(t: &Value) {
40 let holder = t["claimed_by"]
41 .as_str()
42 .map(|h| format!(" ({h})"))
43 .unwrap_or_default();
44 let expired = if t["lease_expired"].as_bool() == Some(true) {
45 " [lease expired]"
46 } else {
47 ""
48 };
49 println!(
50 "{:<24} {:<8}{}{} {}",
51 field(t, "key"),
52 field(t, "status"),
53 holder,
54 expired,
55 field(t, "title"),
56 );
57}
58
59pub(super) fn render(cmd: &ClientCmd, value: &Value) -> anyhow::Result<()> {
60 match cmd {
61 ClientCmd::Whoami => {
62 println!(
63 "{} @ {} — {} unread DM(s), {} claimed task(s)",
64 field(value, "agent"),
65 field(value, "team"),
66 value["unread_direct_messages"],
67 value["open_claimed_tasks"],
68 );
69 }
70 ClientCmd::Read { .. } | ClientCmd::Search { .. } => render_messages(value),
71 ClientCmd::Send { .. } => {
72 let m = &value["message"];
73 println!("sent [{}] to {}", m["id"], value["delivered_to"]);
74 }
75 ClientCmd::Ask { .. } => {
76 if value["answered"].as_bool() == Some(true) {
77 let a = &value["answer"];
78 println!("{}: {}", field(a, "from"), field(a, "body"));
79 } else {
80 println!("(no answer yet)");
81 println!("{}", field(value, "suggestion"));
82 }
83 }
84 ClientCmd::Attach { .. } => {
85 println!(
86 "attached [{}] {} ({} bytes)",
87 value["id"],
88 field(value, "filename"),
89 value["size_bytes"]
90 );
91 }
92 ClientCmd::Download { out, .. } => {
93 use base64::Engine;
94 let data = base64::engine::general_purpose::STANDARD
95 .decode(field(value, "data_base64"))
96 .context("server returned invalid base64")?;
97 let path = out
98 .clone()
99 .unwrap_or_else(|| std::path::PathBuf::from(field(value, "filename")));
100 std::fs::write(&path, data)
101 .with_context(|| format!("cannot write {}", path.display()))?;
102 println!(
103 "wrote {} ({} bytes, {})",
104 path.display(),
105 value["size_bytes"],
106 field(value, "content_type")
107 );
108 }
109 ClientCmd::ChannelCreate { .. } => {
110 println!("channel #{} ready", field(value, "name"));
111 }
112 ClientCmd::Channels => {
113 let channels = value["channels"].as_array().cloned().unwrap_or_default();
114 if channels.is_empty() {
115 println!("(no channels yet)");
116 }
117 for c in &channels {
118 println!(
119 "#{:<20} {:>5} msg(s) {}",
120 field(c, "name"),
121 c["message_count"],
122 field(c, "topic"),
123 );
124 }
125 }
126 ClientCmd::Agents { .. } => {
127 for a in value["agents"].as_array().cloned().unwrap_or_default() {
128 let place = match (a["repo"].as_str(), a["branch"].as_str()) {
129 (Some(r), Some(b)) => format!(" {r}@{b}"),
130 (Some(r), None) => format!(" {r}"),
131 _ => String::new(),
132 };
133 println!(
134 "{:<20} {:<8}{} {}",
135 field(&a, "name"),
136 field(&a, "status"),
137 place,
138 field(&a, "activity"),
139 );
140 }
141 println!("({} online)", value["online_count"]);
142 }
143 ClientCmd::Tasks { .. } => {
144 let tasks = value["tasks"].as_array().cloned().unwrap_or_default();
145 if tasks.is_empty() {
146 println!("(no tasks)");
147 }
148 for t in &tasks {
149 render_task_line(t);
150 }
151 println!("({} open, {} claimed)", value["open"], value["claimed"]);
152 }
153 ClientCmd::Task(TaskCmd::Show { .. }) => {
154 render_task_line(&value["task"]);
155 if let Some(desc) = value["task"]["description"].as_str() {
156 println!(" {desc}");
157 }
158 if let Some(result) = value["task"]["result"].as_str() {
159 println!(" result: {result}");
160 }
161 for e in value["history"].as_array().cloned().unwrap_or_default() {
162 println!(
163 " {} {} {} {}",
164 field(&e, "created_at"),
165 field(&e, "event"),
166 field(&e, "agent"),
167 field(&e, "detail"),
168 );
169 }
170 }
171 ClientCmd::Task(TaskCmd::Claim { .. } | TaskCmd::Next { .. }) => {
172 if value["claimed"].as_bool() == Some(true) {
173 print!("claimed: ");
174 render_task_line(&value["task"]);
175 } else {
176 println!(
177 "not claimed: {}",
178 value["reason"].as_str().unwrap_or("unknown reason")
179 );
180 }
181 }
182 ClientCmd::Task(_) => {
183 render_task_line(value);
184 }
185 ClientCmd::Notes { .. } | ClientCmd::Note(NoteCmd::Search { .. }) => {
186 let notes = value["notes"].as_array().cloned().unwrap_or_default();
187 if notes.is_empty() {
188 println!("(no notes)");
189 }
190 for n in ¬es {
191 let first_line = field(n, "value").lines().next().unwrap_or("").to_owned();
192 println!("{}/{}: {}", field(n, "scope"), field(n, "key"), first_line);
193 }
194 }
195 ClientCmd::Note(NoteCmd::Get { .. }) => {
196 if value["found"].as_bool() == Some(true) {
197 let n = &value["note"];
198 println!(
199 "{}/{} (by {}, {})",
200 field(n, "scope"),
201 field(n, "key"),
202 field(n, "updated_by"),
203 field(n, "updated_at"),
204 );
205 println!("{}", field(n, "value"));
206 } else {
207 println!("not found");
208 }
209 }
210 ClientCmd::Note(NoteCmd::Set { .. }) => {
211 println!("saved {}/{}", field(value, "scope"), field(value, "key"));
212 }
213 ClientCmd::Note(NoteCmd::Rm { .. }) => {
214 println!("{}", field(value, "detail"));
215 }
216 ClientCmd::Beat { .. } => {
217 println!(
218 "presence updated: {} {}",
219 field(value, "status"),
220 field(value, "activity"),
221 );
222 }
223 ClientCmd::Wait { .. } => {
224 if value["woke"].as_bool() == Some(true) {
225 for e in value["events"].as_array().cloned().unwrap_or_default() {
226 println!("[{}] {}", field(&e, "kind"), field(&e, "summary"));
227 }
228 } else {
229 println!("(nothing happened)");
230 }
231 println!("{}", field(value, "suggestion"));
232 }
233 ClientCmd::Lock(LockCmd::List) => {
234 let locks = value["locks"].as_array().cloned().unwrap_or_default();
235 if locks.is_empty() {
236 println!("(no locks held)");
237 }
238 for l in &locks {
239 println!(
240 "{:<28} {} until {} {}",
241 field(l, "name"),
242 field(l, "holder"),
243 field(l, "expires_at"),
244 field(l, "purpose"),
245 );
246 }
247 }
248 ClientCmd::Lock(LockCmd::Acquire { .. }) => {
249 if value["acquired"].as_bool() == Some(true) {
250 let l = &value["lock"];
251 println!(
252 "acquired {} until {}",
253 field(l, "name"),
254 field(l, "expires_at")
255 );
256 } else {
257 println!("not acquired: {}", field(value, "reason"));
258 }
259 }
260 ClientCmd::Lock(LockCmd::Release { .. }) => {
261 println!("{}", field(value, "detail"));
262 }
263 ClientCmd::Digest { .. } => {
264 println!("— last {}h —", value["hours"]);
265 for c in value["channels"].as_array().cloned().unwrap_or_default() {
266 println!("#{}: {} msg(s)", field(&c, "name"), c["message_count"]);
267 for m in c["last_messages"].as_array().cloned().unwrap_or_default() {
268 println!(" {}: {}", field(&m, "from"), field(&m, "body"));
269 }
270 }
271 let moved = value["tasks_moved"].as_array().cloned().unwrap_or_default();
272 if !moved.is_empty() {
273 println!("tasks:");
274 for t in &moved {
275 println!(
276 " {} → {} {}",
277 field(t, "key"),
278 field(t, "status"),
279 t["claimed_by"]
280 .as_str()
281 .map(|s| format!("({s})"))
282 .unwrap_or_default(),
283 );
284 }
285 }
286 for n in value["notes_updated"]
287 .as_array()
288 .cloned()
289 .unwrap_or_default()
290 {
291 println!(
292 "note {}/{} by {}",
293 field(&n, "scope"),
294 field(&n, "key"),
295 field(&n, "updated_by")
296 );
297 }
298 println!(
299 "({} open, {} claimed, {} lock(s))",
300 value["open_tasks"],
301 value["claimed_tasks"],
302 value["active_locks"].as_array().map(Vec::len).unwrap_or(0),
303 );
304 }
305 _ => {
306 println!("{}", serde_json::to_string_pretty(value).unwrap());
307 }
308 }
309 Ok(())
310}