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 ("/mcp", "List configured MCP servers"),
124 ("/config", "Show active config/manifest path"),
125 ("/review", "Code review mode (stub)"),
126 ("/memory", "Project memory CRUD (stub)"),
127 ("/permissions", "Permission mode (stub)"),
128 ("/hooks", "List registered hooks (stub)"),
129 ("/init", "Scaffold manifest / CLAUDE.md (stub)"),
130 ("/resume", "REPL-scope resume (stub)"),
131 ("/add-dir", "Add directory to tool allowlist (stub)"),
132 ("/agents", "List custom sub-agents (stub)"),
133 ];
134 println!(" {}", "Commands:".bold());
135 for (cmd, desc) in cmds {
136 println!(" {:10} {desc}", cmd.bright_yellow());
137 }
138}
139
140pub(super) fn print_turn_footer(
142 result: &AgentLoopResult,
143 cost: f64,
144 session: &ReplSession,
145 _budget: f64,
146) {
147 let cost_str = if cost > 0.0 { format!("${:.4}", cost) } else { "free".into() };
148 println!(
149 "\n{}",
150 format!(
151 " [turn {} | {} | {} tools | {}/{} tok]",
152 session.turn_count,
153 cost_str,
154 result.tool_calls,
155 result.usage.input_tokens,
156 result.usage.output_tokens,
157 )
158 .dimmed()
159 );
160}
161
162pub(super) fn print_session_summary(session: &ReplSession) {
164 if session.turn_count == 0 {
165 return;
166 }
167 println!("\n {}", "Session Summary".bold());
168 println!(" Turns: {}, Tool calls: {}", session.turn_count, session.total_tool_calls);
169 println!(" Tokens: {} in / {} out", session.total_input_tokens, session.total_output_tokens);
170 if let Some(id) = session.session_id() {
171 println!(" Session: {id}");
172 }
173}
174
175pub(super) fn list_recent_sessions() {
177 let sessions_dir = match dirs::home_dir() {
178 Some(h) => h.join(".apr").join("sessions"),
179 None => {
180 println!(" Cannot determine home directory.");
181 return;
182 }
183 };
184 if !sessions_dir.is_dir() {
185 println!(" No sessions found.");
186 return;
187 }
188
189 let mut sessions: Vec<(String, u32, String)> = Vec::new();
190 if let Ok(entries) = std::fs::read_dir(&sessions_dir) {
191 for entry in entries.flatten() {
192 let manifest_path = entry.path().join("manifest.json");
193 if let Ok(json) = std::fs::read_to_string(&manifest_path) {
194 if let Ok(v) = serde_json::from_str::<serde_json::Value>(&json) {
195 let id = v.get("id").and_then(|i| i.as_str()).unwrap_or("?").to_string();
196 let turns = v.get("turns").and_then(|t| t.as_u64()).unwrap_or(0) as u32;
197 let cwd = v.get("cwd").and_then(|c| c.as_str()).unwrap_or("?").to_string();
198 sessions.push((id, turns, cwd));
199 }
200 }
201 }
202 }
203
204 if sessions.is_empty() {
205 println!(" No sessions found.");
206 return;
207 }
208 sessions.sort_by(|a, b| b.0.cmp(&a.0));
209 println!(" Recent sessions:");
210 for (id, turns, cwd) in sessions.iter().take(10) {
211 println!(" {} ({turns} turns) {}", id.cyan(), cwd.dimmed());
212 }
213 println!(" Resume: {} --resume=<id>", "batuta code".bright_yellow());
214}
215
216pub(super) fn run_shell_shortcut(cmd: &str) {
218 match std::process::Command::new("sh").arg("-c").arg(cmd).output() {
219 Ok(output) => {
220 let stdout = String::from_utf8_lossy(&output.stdout);
221 let stderr = String::from_utf8_lossy(&output.stderr);
222 if !stdout.is_empty() {
223 println!("{stdout}");
224 }
225 if !stderr.is_empty() {
226 eprintln!("{stderr}");
227 }
228 if output.status.success() {
229 println!(" {} Done.", "✓".green());
230 } else {
231 println!(" {} Exit code: {}", "✗".bright_red(), output.status);
232 }
233 }
234 Err(e) => println!(" {} Failed: {e}", "✗".bright_red()),
235 }
236}
237
238pub(super) fn compact_history(history: &mut Vec<super::driver::Message>) {
241 use super::driver::Message;
242 if history.len() <= 10 {
243 return;
244 }
245 let compact_boundary = history.len() - 10;
246 let mut compacted = Vec::new();
247 for msg in history.iter().take(compact_boundary) {
248 match msg {
249 Message::User(_) | Message::Assistant(_) => compacted.push(msg.clone()),
250 Message::AssistantToolUse(_) | Message::ToolResult(_) => {}
251 Message::System(_) => compacted.push(msg.clone()),
252 }
253 }
254 compacted.extend_from_slice(&history[compact_boundary..]);
255 *history = compacted;
256}