Skip to main content

rz_cli/
log.rs

1//! Helpers for extracting and formatting protocol messages from scrollback.
2
3use rz_agent_protocol::{Envelope, MessageKind, SENTINEL};
4
5/// Scan scrollback text for `@@RZ:` lines and parse each into an [`Envelope`].
6///
7/// Handles terminal line-wrapping by joining continuation lines until the
8/// JSON parses successfully (up to 20 lines lookahead).
9pub fn extract_messages(scrollback: &str) -> Vec<Envelope> {
10    let lines: Vec<&str> = scrollback.lines().collect();
11    let mut result = Vec::new();
12    let mut i = 0;
13
14    while i < lines.len() {
15        if let Some(idx) = lines[i].find(SENTINEL) {
16            let mut candidate = lines[i][idx..].to_string();
17            if let Ok(env) = Envelope::decode(&candidate) {
18                result.push(env);
19                i += 1;
20                continue;
21            }
22            // Try joining wrapped continuation lines.
23            for j in 1..20 {
24                if i + j >= lines.len() {
25                    break;
26                }
27                candidate.push_str(lines[i + j]);
28                if let Ok(env) = Envelope::decode(&candidate) {
29                    result.push(env);
30                    i += j;
31                    break;
32                }
33            }
34        }
35        i += 1;
36    }
37
38    result
39}
40
41/// Format an envelope as a human-readable one-liner: `[HH:MM:SS] from_id> text`
42///
43/// If `own_id` is provided and matches `envelope.from`, appends `(me)` to the sender.
44pub fn format_message(envelope: &Envelope, own_id: Option<&str>) -> String {
45    let secs = envelope.ts / 1000;
46    let h = (secs / 3600) % 24;
47    let m = (secs % 3600) / 60;
48    let s = secs % 60;
49
50    let me = if own_id == Some(envelope.from.as_str()) { " (me)" } else { "" };
51
52    let text = match &envelope.kind {
53        MessageKind::Chat { text } => text.as_str(),
54        MessageKind::Ping => "ping",
55        MessageKind::Pong => "pong",
56        MessageKind::Error { message } => {
57            return format!("[{h:02}:{m:02}:{s:02}] {}{me}> error: {message}", envelope.from);
58        }
59        MessageKind::Timer { label } => {
60            return format!("[{h:02}:{m:02}:{s:02}] {}{me}> timer: {label}", envelope.from);
61        }
62        MessageKind::Status { state, detail } => {
63            return format!("[{h:02}:{m:02}:{s:02}] {}{me}> [{state}] {detail}", envelope.from);
64        }
65        MessageKind::ToolCall { name, .. } => {
66            return format!("[{h:02}:{m:02}:{s:02}] {}{me}> (calling tool: {name})", envelope.from);
67        }
68        MessageKind::ToolResult { name, result, is_error } => {
69            let prefix = if *is_error { "tool error" } else { "tool result" };
70            let short = if result.len() > 200 { &result[..200] } else { result.as_str() };
71            return format!("[{h:02}:{m:02}:{s:02}] {}{me}> {prefix} ({name}): {short}", envelope.from);
72        }
73        MessageKind::Delegate { task, .. } => {
74            let short = if task.len() > 200 { &task[..200] } else { task.as_str() };
75            return format!("[{h:02}:{m:02}:{s:02}] {}{me}> (delegating: {short})", envelope.from);
76        }
77        MessageKind::Hello { name } => {
78            return format!("[{h:02}:{m:02}:{s:02}] {}{me}> hello from {name}", envelope.from);
79        }
80    };
81
82    format!("[{h:02}:{m:02}:{s:02}] {}{me}> {text}", envelope.from)
83}