1use std::io::{self, Write};
8
9use super::driver::{LlmDriver, StreamEvent};
10use super::repl::ReplSession;
11use super::result::AgentLoopResult;
12use super::AgentManifest;
13use crate::ansi_colors::Colorize;
14
15pub(super) fn print_stream_event_repl(event: &StreamEvent) {
17 use crate::agent::phase::LoopPhase;
18 match event {
19 StreamEvent::PhaseChange { phase } => match phase {
20 LoopPhase::Perceive => print!("{} ", " [perceive]".dimmed()),
21 LoopPhase::Reason => {}
22 LoopPhase::Act { tool_name } => {
23 println!(" {} {}", " [tool]".bright_yellow(), tool_name.cyan());
24 }
25 LoopPhase::Done => {}
26 LoopPhase::Error { message } => {
27 println!(" {} {}", " [error]".bright_red(), message);
28 }
29 },
30 StreamEvent::TextDelta { text } => {
31 print!("{text}");
32 io::stdout().flush().ok();
33 }
34 StreamEvent::ToolUseStart { name, .. } => {
35 print!(" {} {} ", "⚙".bright_yellow(), name.cyan());
36 io::stdout().flush().ok();
37 }
38 StreamEvent::ToolUseEnd { result, .. } => {
39 let preview =
40 if result.len() > 60 { format!("{}...", &result[..57]) } else { result.clone() };
41 println!("{}", preview.dimmed());
42 }
43 StreamEvent::ContentComplete { .. } => println!(),
44 }
45}
46
47pub(super) fn print_welcome(manifest: &AgentManifest, driver: &dyn LlmDriver) {
49 let tier = driver.privacy_tier();
50 println!();
51 println!(
52 " {} {} ({})",
53 "apr code".bright_cyan().bold(),
54 env!("CARGO_PKG_VERSION"),
55 format!("{tier:?} tier").dimmed()
56 );
57
58 if let Some(ref path) = manifest.model.model_path {
59 let name = path.file_name().map(|f| f.to_string_lossy()).unwrap_or_default();
60 let ext = path.extension().and_then(|e| e.to_str());
61 match ext {
62 Some("apr") => {
63 println!(" {} {} (APR — native format)", "Model:".dimmed(), name.bright_cyan());
64 }
65 Some("gguf") => {
66 println!(" {} {} (GGUF)", "Model:".dimmed(), name.bright_cyan());
67 println!(
68 " {} Convert to APR for faster loading: {}",
69 "tip:".dimmed(),
70 "apr convert --to-apr <model>.gguf".dimmed()
71 );
72 }
73 _ => {
74 println!(" {} {} (model)", "Model:".dimmed(), name.bright_cyan());
75 }
76 }
77 } else {
78 println!(" {} {}", "Model:".dimmed(), "mock (no model loaded)".bright_yellow());
79 }
80
81 if let Some(ref path) = manifest.model.model_path {
83 let name_lower =
84 path.file_name().map(|f| f.to_string_lossy().to_lowercase()).unwrap_or_default();
85 if name_lower.contains("qwen2.5-coder") || name_lower.contains("qwen2_5-coder") {
86 println!(
87 " {} Qwen2.5-Coder cannot do tool-use (PMAT-178). Use Qwen3 1.7B+ instead.",
88 "⚠".bright_yellow()
89 );
90 }
91 }
92
93 let ctx = driver.context_window();
95 if ctx <= 2048 {
96 println!(" {} Small context ({ctx} tokens) — tool-use may not work.", "⚠".bright_yellow());
97 println!(" {} Recommended: 7B+ model with 8K+ context.", " ".dimmed());
98 }
99
100 println!();
101 println!(
102 " Type a message, {} for commands, {} to exit.",
103 "/help".bright_yellow(),
104 "/quit".bright_yellow()
105 );
106 println!(" {}", "─".repeat(56).dimmed());
107}
108
109pub(super) fn print_help() {
111 let cmds = [
112 ("/help", "Show this help"),
113 ("/test", "Run cargo test"),
114 ("/quality", "Run quality gate"),
115 ("/context", "Show context/token usage"),
116 ("/compact", "Compact old messages"),
117 ("/session", "Show session info"),
118 ("/sessions", "List recent sessions"),
119 ("/cost", "Show session cost"),
120 ("/clear", "Clear screen + history"),
121 ("/quit", "Exit apr code"),
122 ];
123 println!(" {}", "Commands:".bold());
124 for (cmd, desc) in cmds {
125 println!(" {:10} {desc}", cmd.bright_yellow());
126 }
127}
128
129pub(super) fn print_turn_footer(
131 result: &AgentLoopResult,
132 cost: f64,
133 session: &ReplSession,
134 _budget: f64,
135) {
136 let cost_str = if cost > 0.0 { format!("${:.4}", cost) } else { "free".into() };
137 println!(
138 "\n{}",
139 format!(
140 " [turn {} | {} | {} tools | {}/{} tok]",
141 session.turn_count,
142 cost_str,
143 result.tool_calls,
144 result.usage.input_tokens,
145 result.usage.output_tokens,
146 )
147 .dimmed()
148 );
149}
150
151pub(super) fn print_session_summary(session: &ReplSession) {
153 if session.turn_count == 0 {
154 return;
155 }
156 println!("\n {}", "Session Summary".bold());
157 println!(" Turns: {}, Tool calls: {}", session.turn_count, session.total_tool_calls);
158 println!(" Tokens: {} in / {} out", session.total_input_tokens, session.total_output_tokens);
159 if let Some(id) = session.session_id() {
160 println!(" Session: {id}");
161 }
162}
163
164pub(super) fn list_recent_sessions() {
166 let sessions_dir = match dirs::home_dir() {
167 Some(h) => h.join(".apr").join("sessions"),
168 None => {
169 println!(" Cannot determine home directory.");
170 return;
171 }
172 };
173 if !sessions_dir.is_dir() {
174 println!(" No sessions found.");
175 return;
176 }
177
178 let mut sessions: Vec<(String, u32, String)> = Vec::new();
179 if let Ok(entries) = std::fs::read_dir(&sessions_dir) {
180 for entry in entries.flatten() {
181 let manifest_path = entry.path().join("manifest.json");
182 if let Ok(json) = std::fs::read_to_string(&manifest_path) {
183 if let Ok(v) = serde_json::from_str::<serde_json::Value>(&json) {
184 let id = v.get("id").and_then(|i| i.as_str()).unwrap_or("?").to_string();
185 let turns = v.get("turns").and_then(|t| t.as_u64()).unwrap_or(0) as u32;
186 let cwd = v.get("cwd").and_then(|c| c.as_str()).unwrap_or("?").to_string();
187 sessions.push((id, turns, cwd));
188 }
189 }
190 }
191 }
192
193 if sessions.is_empty() {
194 println!(" No sessions found.");
195 return;
196 }
197 sessions.sort_by(|a, b| b.0.cmp(&a.0));
198 println!(" Recent sessions:");
199 for (id, turns, cwd) in sessions.iter().take(10) {
200 println!(" {} ({turns} turns) {}", id.cyan(), cwd.dimmed());
201 }
202 println!(" Resume: {} --resume=<id>", "batuta code".bright_yellow());
203}
204
205pub(super) fn run_shell_shortcut(cmd: &str) {
207 match std::process::Command::new("sh").arg("-c").arg(cmd).output() {
208 Ok(output) => {
209 let stdout = String::from_utf8_lossy(&output.stdout);
210 let stderr = String::from_utf8_lossy(&output.stderr);
211 if !stdout.is_empty() {
212 println!("{stdout}");
213 }
214 if !stderr.is_empty() {
215 eprintln!("{stderr}");
216 }
217 if output.status.success() {
218 println!(" {} Done.", "✓".green());
219 } else {
220 println!(" {} Exit code: {}", "✗".bright_red(), output.status);
221 }
222 }
223 Err(e) => println!(" {} Failed: {e}", "✗".bright_red()),
224 }
225}
226
227pub(super) fn compact_history(history: &mut Vec<super::driver::Message>) {
230 use super::driver::Message;
231 if history.len() <= 10 {
232 return;
233 }
234 let compact_boundary = history.len() - 10;
235 let mut compacted = Vec::new();
236 for msg in history.iter().take(compact_boundary) {
237 match msg {
238 Message::User(_) | Message::Assistant(_) => compacted.push(msg.clone()),
239 Message::AssistantToolUse(_) | Message::ToolResult(_) => {}
240 Message::System(_) => compacted.push(msg.clone()),
241 }
242 }
243 compacted.extend_from_slice(&history[compact_boundary..]);
244 *history = compacted;
245}