tmai_core/transcript/
renderer.rs1use super::types::TranscriptRecord;
4
5fn truncate_chars(s: &str, max_chars: usize) -> String {
7 let truncated: String = s.chars().take(max_chars).collect();
8 if truncated.len() < s.len() {
9 format!("{}...", truncated)
10 } else {
11 truncated
12 }
13}
14
15pub fn render_preview(records: &[TranscriptRecord], max_lines: usize) -> String {
17 let mut lines = Vec::new();
18
19 for record in records {
20 match record {
21 TranscriptRecord::User { text } => {
22 let first_line = text.lines().next().unwrap_or(text);
23 lines.push(format!("▶ User: {}", truncate_chars(first_line, 120)));
24 }
25 TranscriptRecord::AssistantText { text } => {
26 for (i, line) in text.lines().enumerate() {
28 if i >= 5 {
29 lines.push(" ...".to_string());
30 break;
31 }
32 let truncated = truncate_chars(line, 120);
33 if i == 0 {
34 lines.push(format!("◀ {}", truncated));
35 } else {
36 lines.push(format!(" {}", truncated));
37 }
38 }
39 }
40 TranscriptRecord::ToolUse {
41 tool_name,
42 input_summary,
43 } => {
44 if input_summary.is_empty() {
45 lines.push(format!(" ⚙ {}", tool_name));
46 } else {
47 lines.push(format!(" ⚙ {}: {}", tool_name, input_summary));
48 }
49 }
50 TranscriptRecord::ToolResult { output_summary } => {
51 let first_line = output_summary.lines().next().unwrap_or(output_summary);
52 lines.push(format!(" ✓ {}", truncate_chars(first_line, 100)));
53 }
54 }
55 }
56
57 if lines.len() > max_lines {
59 let start = lines.len() - max_lines;
60 lines = lines[start..].to_vec();
61 }
62
63 lines.join("\n")
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69
70 #[test]
71 fn test_render_preview_user_and_assistant() {
72 let records = vec![
73 TranscriptRecord::User {
74 text: "Fix the bug in main.rs".to_string(),
75 },
76 TranscriptRecord::AssistantText {
77 text: "I'll look at main.rs and fix the issue.".to_string(),
78 },
79 ];
80
81 let result = render_preview(&records, 100);
82 assert!(result.contains("▶ User: Fix the bug"));
83 assert!(result.contains("◀ I'll look at main.rs"));
84 }
85
86 #[test]
87 fn test_render_preview_with_tools() {
88 let records = vec![
89 TranscriptRecord::ToolUse {
90 tool_name: "Bash".to_string(),
91 input_summary: "cargo test".to_string(),
92 },
93 TranscriptRecord::ToolResult {
94 output_summary: "test result: ok".to_string(),
95 },
96 ];
97
98 let result = render_preview(&records, 100);
99 assert!(result.contains("⚙ Bash: cargo test"));
100 assert!(result.contains("✓ test result: ok"));
101 }
102
103 #[test]
104 fn test_render_preview_max_lines() {
105 let records: Vec<TranscriptRecord> = (0..20)
106 .map(|i| TranscriptRecord::User {
107 text: format!("Message {}", i),
108 })
109 .collect();
110
111 let result = render_preview(&records, 5);
112 let line_count = result.lines().count();
113 assert_eq!(line_count, 5);
114 }
115
116 #[test]
117 fn test_render_preview_empty() {
118 let result = render_preview(&[], 100);
119 assert!(result.is_empty());
120 }
121
122 #[test]
123 fn test_truncate_chars_multibyte() {
124 let text = "これは日本語のテストです。とても長い文章を書いています。";
126 let result = truncate_chars(text, 10);
127 assert!(result.ends_with("..."));
128 }
130
131 #[test]
132 fn test_truncate_chars_ascii() {
133 let text = "short";
134 let result = truncate_chars(text, 120);
135 assert_eq!(result, "short");
136 }
137}