1use crate::domain::{ContextBundle, TargetTool};
2
3pub fn render(bundle: &ContextBundle, max_chars: usize) -> String {
4 let mut output = String::new();
5 output.push_str(match bundle.input.target {
6 TargetTool::Claude => "以下是给 Claude 使用的精简上下文。\n\n",
7 TargetTool::Codex => "以下是给 Codex 使用的精简上下文。\n\n",
8 TargetTool::Opencode => "以下是给 OpenCode 使用的精简上下文。\n\n",
9 });
10
11 if let Some(project) = &bundle.route.project {
12 output.push_str(&format!("项目:{} ({})\n", project.name, project.id));
13 output.push_str(&format!("项目命中原因:{}\n\n", project.reason));
14 }
15
16 if !bundle.route.modules.is_empty() {
17 output.push_str("模块:\n");
18 for module in &bundle.route.modules {
19 output.push_str(&format!("- {}\n", module.id));
20 }
21 output.push('\n');
22 }
23
24 if !bundle.route.scenes.is_empty() {
25 output.push_str("场景:\n");
26 for scene in &bundle.route.scenes {
27 output.push_str(&format!("- {}\n", scene.id));
28 }
29 output.push('\n');
30 }
31
32 if !bundle.route.lifecycle_candidates.is_empty() {
33 output.push_str("记忆(accepted / canonical):\n");
34 for candidate in &bundle.route.lifecycle_candidates {
35 let summary = truncate_chars(&candidate.summary, 120);
36 output.push_str(&format!(
37 "- [{}] [{}] {} — {}\n",
38 candidate.score, candidate.memory_type, candidate.title, summary
39 ));
40 }
41 output.push('\n');
42 }
43
44 output.push_str("候选笔记:\n");
45 for candidate in &bundle.route.candidates {
46 output.push_str(&format!(
47 "- [{}] {}\n {}\n",
48 candidate.score, candidate.relative_path, candidate.excerpt
49 ));
50 }
51
52 if output.chars().count() > max_chars {
53 output.chars().take(max_chars).collect()
54 } else {
55 output
56 }
57}
58
59fn truncate_chars(value: &str, max_chars: usize) -> String {
60 let chars: Vec<char> = value.chars().collect();
61 if chars.len() <= max_chars {
62 return value.to_string();
63 }
64 let mut out: String = chars.iter().take(max_chars).collect();
65 out.push('…');
66 out
67}