use crate::domain::message::{ContentBlock, Message};
use colored::*;
use regex::Regex;
use serde_json::Value;
pub fn format_message(msg: &Message, json_mode: bool, prefix: Option<&str>) -> String {
if json_mode {
if let Some(p) = prefix {
format!(" [{}] Message: {:?}", p, msg)
} else {
format!(" Message: {:?}", msg)
}
} else {
format_message_interactive(msg, prefix)
}
}
fn format_message_interactive(msg: &Message, prefix: Option<&str>) -> String {
let prefix_str = if let Some(p) = prefix {
format!("[{}] ", p).yellow().bold().to_string()
} else {
String::new()
};
match msg {
Message::Assistant(assistant_msg) => format_assistant_message(assistant_msg, &prefix_str),
Message::User(user_msg) => {
format!(
"{}{} {}",
prefix_str,
"User:".cyan().bold(),
truncate_and_format(&format!("{:?}", user_msg.message.content), 80)
)
}
Message::System(system_msg) => {
format!(
"{}{} {}: {}",
prefix_str,
"System".blue().bold(),
system_msg.subtype.dimmed(),
format_json_value(&system_msg.data, 60)
)
}
Message::Result(result_msg) => format_result_message(result_msg, &prefix_str),
Message::StreamEvent(stream_msg) => {
format!(
"{}{} {}",
prefix_str,
"Stream".magenta().bold(),
format_json_value(&stream_msg.event, 80)
)
}
}
}
fn format_assistant_message(
msg: &crate::domain::message::AssistantMessage,
prefix: &str,
) -> String {
let mut output = String::new();
for (idx, block) in msg.message.content.iter().enumerate() {
if idx > 0 {
output.push('\n');
}
let block_str = match block {
ContentBlock::Text { text } => {
format!(
"{}{} {}",
prefix,
"Assistant:".green().bold(),
truncate_and_format(text, 100)
)
}
ContentBlock::Thinking { thinking, .. } => {
format!(
"{}{} {}",
prefix,
"Thinking:".yellow().bold(),
truncate_and_format(thinking, 100).dimmed()
)
}
ContentBlock::ToolUse { name, input, .. } => {
format!(
"{}{} {} {}",
prefix,
"Tool:".cyan().bold(),
name.bright_white(),
format_json_value(input, 70).dimmed()
)
}
ContentBlock::ToolResult {
tool_use_id,
content,
is_error,
} => {
let status = if is_error.unwrap_or(false) {
"Error".red()
} else {
"Result".green()
};
let content_str = if let Some(c) = content {
format_json_value(c, 70)
} else {
"(empty)".dimmed().to_string()
};
format!(
"{}{} [{}] {}",
prefix,
status.bold(),
tool_use_id.chars().take(8).collect::<String>().dimmed(),
content_str
)
}
};
output.push_str(&block_str);
}
let lines: Vec<&str> = output.lines().collect();
if lines.len() > 2 {
format!(
"{}\n{} {}",
lines[0],
lines[1],
format!("(+{} more blocks)", lines.len() - 2).dimmed()
)
} else {
output
}
}
fn format_result_message(msg: &crate::domain::message::ResultMessage, prefix: &str) -> String {
let status = if msg.is_error {
"Error".red()
} else {
"Complete".green()
};
let duration = format!("{:.2}s", msg.duration_ms as f64 / 1000.0);
let turns = format!("{} turns", msg.num_turns);
let cost = if let Some(c) = msg.total_cost_usd {
format!(" ${:.4}", c).yellow().to_string()
} else {
String::new()
};
let result_preview = if let Some(ref r) = msg.result {
format!(" - {}", truncate_and_format(r, 50).dimmed())
} else {
String::new()
};
format!(
"{}{} {} | {} | {}{}{}",
prefix,
status.bold(),
duration.bright_white(),
turns.dimmed(),
cost,
result_preview,
format_json_value(&msg.usage.clone().unwrap_or(Value::Null), 40).dimmed()
)
}
fn format_json_value(value: &Value, max_len: usize) -> String {
match value {
Value::Null => String::new(),
Value::Bool(b) => b.to_string(),
Value::Number(n) => n.to_string(),
Value::String(s) => {
if let Ok(parsed) = serde_json::from_str::<Value>(s) {
format_json_value(&parsed, max_len)
} else {
truncate_and_format(s, max_len)
}
}
Value::Array(arr) => {
if arr.is_empty() {
"[]".to_string()
} else if arr.len() == 1 {
format_json_value(&arr[0], max_len)
} else {
format!("[{} items]", arr.len())
}
}
Value::Object(obj) => {
let relevant_keys = extract_relevant_fields(obj);
if relevant_keys.is_empty() {
if obj.is_empty() {
"{}".to_string()
} else {
format!("{{{} fields}}", obj.len())
}
} else {
let parts: Vec<String> = relevant_keys
.iter()
.take(2) .filter_map(|key| {
obj.get(key).map(|val| {
let val_str = match val {
Value::String(s) => truncate_and_format(s, 30),
Value::Number(n) => n.to_string(),
Value::Bool(b) => b.to_string(),
Value::Array(a) => format!("[{}]", a.len()),
Value::Object(o) => format!("{{{}}}", o.len()),
Value::Null => "null".to_string(),
};
format!("{}: {}", key, val_str)
})
})
.collect();
let result = parts.join(", ");
if result.len() > max_len {
let truncated: String =
result.chars().take(max_len.saturating_sub(3)).collect();
format!("{}...", truncated)
} else {
result
}
}
}
}
}
fn extract_relevant_fields(obj: &serde_json::Map<String, Value>) -> Vec<String> {
let priority_keys = [
"error", "message", "result", "status", "name", "type", "data", "content", "value", "text",
"output", "response", "id",
];
let mut found = Vec::new();
for key in &priority_keys {
if obj.contains_key(*key) {
found.push(key.to_string());
}
}
if !found.is_empty() {
return found.into_iter().take(3).collect();
}
obj.keys().take(3).cloned().collect()
}
fn truncate_and_format(text: &str, max_len: usize) -> String {
let re = Regex::new(r"\s+").unwrap();
let cleaned = re.replace_all(text.trim(), " ");
if cleaned.len() > max_len {
let truncated: String = cleaned.chars().take(max_len.saturating_sub(3)).collect();
format!("{}...", truncated)
} else {
cleaned.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::message::*;
#[test]
fn test_format_json_value_object() {
let json = serde_json::json!({
"error": "Test error",
"status": "failed",
"other": "data"
});
let formatted = format_json_value(&json, 100);
assert!(formatted.contains("error"));
assert!(formatted.contains("Test error"));
}
#[test]
fn test_format_json_value_nested_string() {
let inner = serde_json::json!({"message": "Hello"});
let json = Value::String(serde_json::to_string(&inner).unwrap());
let formatted = format_json_value(&json, 100);
assert!(formatted.contains("message"));
}
#[test]
fn test_truncate_and_format() {
let long_text = "This is a very long text that should be truncated to fit within the maximum length limit";
let formatted = truncate_and_format(long_text, 30);
assert!(formatted.len() <= 30);
assert!(formatted.ends_with("..."));
}
#[test]
fn test_truncate_and_format_whitespace() {
let text = "Text with\n\nmultiple\t\twhitespace";
let formatted = truncate_and_format(text, 100);
assert!(!formatted.contains("\n"));
assert!(!formatted.contains("\t"));
}
#[test]
fn test_extract_relevant_fields() {
let mut obj = serde_json::Map::new();
obj.insert("error".to_string(), Value::String("test".to_string()));
obj.insert("random".to_string(), Value::String("data".to_string()));
obj.insert("message".to_string(), Value::String("msg".to_string()));
let fields = extract_relevant_fields(&obj);
assert!(fields.contains(&"error".to_string()));
assert!(fields.contains(&"message".to_string()));
}
#[test]
fn test_format_message_json_mode() {
let msg = Message::User(UserMessage {
message: UserMessageContent {
role: "user".to_string(),
content: ContentValue::Text("Hello".to_string()),
},
parent_tool_use_id: None,
});
let formatted = format_message(&msg, true, None);
assert!(formatted.starts_with(" Message:"));
}
#[test]
fn test_format_message_interactive_mode() {
let msg = Message::User(UserMessage {
message: UserMessageContent {
role: "user".to_string(),
content: ContentValue::Text("Hello".to_string()),
},
parent_tool_use_id: None,
});
let formatted = format_message(&msg, false, None);
assert!(!formatted.contains("UserMessage"));
}
#[test]
fn test_truncate_with_multibyte_utf8_chars() {
let text = "╔══════════════════════════════════════════════════════════════╗ ║ LOOP TYPES TESTING ║ ╚";
let formatted = truncate_and_format(text, 30);
assert!(formatted.chars().count() <= 33); assert!(formatted.ends_with("..."));
assert!(!formatted.is_empty());
}
#[test]
fn test_format_json_value_with_multibyte_chars() {
let json = serde_json::json!({
"message": "╔══════════════════════════════════════════════════════════════╗ Test"
});
let formatted = format_json_value(&json, 30);
assert!(!formatted.is_empty());
}
}