From 4d18f4e173003b2c29f456af760e000e3908ecd1 Mon Sep 17 00:00:00 2001 From: Jeff Garzik Date: Mon, 22 Dec 2025 03:00:57 -0500 Subject: [PATCH 1/3] Tools provide numeric summaries upon completion --- src/tools/grep.rs | 4 ++++ src/tools/read.rs | 13 ++++++++++--- src/tools/write.rs | 4 ++++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/tools/grep.rs b/src/tools/grep.rs index 8622eb2..de39082 100644 --- a/src/tools/grep.rs +++ b/src/tools/grep.rs @@ -91,8 +91,12 @@ pub fn execute(args: Value, root: &Path) -> anyhow::Result { } } + let match_count = matches.len(); + eprintln!("Found {} lines", match_count); + Ok(json!({ "matches": matches, + "matches_found": match_count, "truncated": truncated })) } diff --git a/src/tools/read.rs b/src/tools/read.rs index 10e37ab..1334d2e 100644 --- a/src/tools/read.rs +++ b/src/tools/read.rs @@ -42,20 +42,27 @@ pub fn execute(args: Value, root: &Path) -> anyhow::Result { let slice = &data[offset.min(data.len())..end]; let truncated = end < data.len(); - let (content, encoding) = match std::str::from_utf8(slice) { - Ok(s) => (s.to_string(), None), + let (content, encoding, lines_read) = match std::str::from_utf8(slice) { + Ok(s) => { + let line_count = s.lines().count(); + (s.to_string(), None, line_count) + } Err(_) => ( base64::Engine::encode(&base64::engine::general_purpose::STANDARD, slice), Some("base64"), + 0, ), }; + eprintln!("Read {} lines", lines_read); + let mut result = json!({ "path": path, "offset": offset, "truncated": truncated, "content": content, - "sha256": sha256(&data) + "sha256": sha256(&data), + "lines": lines_read }); if let Some(enc) = encoding { diff --git a/src/tools/write.rs b/src/tools/write.rs index 41026b6..8e0a86a 100644 --- a/src/tools/write.rs +++ b/src/tools/write.rs @@ -46,9 +46,13 @@ pub fn execute(args: Value, root: &Path) -> anyhow::Result { return Ok(json!({ "error": { "code": "write_error", "message": e.to_string() } })); } + let lines_written = content.lines().count(); + eprintln!("Wrote {} lines", lines_written); + Ok(json!({ "path": path, "bytes_written": bytes.len(), + "lines": lines_written, "sha256": sha256(bytes) })) } From 2aba7f2ed7785aa9ea3500526fe27c5ffcb8c8a7 Mon Sep 17 00:00:00 2001 From: Jeff Garzik Date: Mon, 22 Dec 2025 03:16:22 -0500 Subject: [PATCH 2/3] Per-command metrics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All three metrics are working correctly: - Duration: 41.4s - Tokens: 3.6k - Tools: 1 (Glob was used) ⏺ Implementation complete. Here's a summary of the changes: Files Modified | File | Changes | |-------------------|--------------------------------------------------------------------------| | src/llm.rs | Added Usage struct to parse token counts from API responses | | src/agent.rs | Added CommandStats struct, modified run_turn() to track and return stats | | src/subagent.rs | Modified run_subagent() to track and return stats | | src/tools/task.rs | Updated to propagate stats from subagents | | src/cli.rs | Added print_stats() helper, timing around commands | Output Format [Duration: 0.7s | Tokens: 1.6k | Tools: 0] - Duration: Total wall-clock time for the command - Tokens: Total tokens used (input + output), formatted with 'k' suffix when ≥1000 - Tools: Count of all tool calls (including nested subagent calls) Key Design Decisions 1. Stats struct returned from functions - run_turn() and run_subagent() return CommandStats so callers can aggregate 2. Subagent stats merged - When a Task tool runs a subagent, its stats are merged into the parent's 3. Token parsing from API - The Usage struct parses the standard OpenAI-compatible usage field 4. Output to stderr - Stats print to stderr to avoid mixing with command output --- src/agent.rs | 40 +++++++++++++++++++++++++--- src/cli.rs | 37 ++++++++++++++++++++++---- src/llm.rs | 15 +++++++++++ src/subagent.rs | 35 ++++++++++++++++++------- src/tools/task.rs | 66 ++++++++++++++++++++++++++++------------------- 5 files changed, 149 insertions(+), 44 deletions(-) diff --git a/src/agent.rs b/src/agent.rs index 5521dc0..e407d62 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -6,6 +6,28 @@ use serde_json::{json, Value}; const MAX_ITERATIONS: usize = 12; +/// Statistics collected during command execution +#[derive(Debug, Default, Clone)] +pub struct CommandStats { + pub input_tokens: u64, + pub output_tokens: u64, + pub tool_uses: u64, +} + +impl CommandStats { + /// Total tokens used (input + output) + pub fn total_tokens(&self) -> u64 { + self.input_tokens + self.output_tokens + } + + /// Merge stats from another source (e.g., subagent) + pub fn merge(&mut self, other: &CommandStats) { + self.input_tokens += other.input_tokens; + self.output_tokens += other.output_tokens; + self.tool_uses += other.tool_uses; + } +} + const SYSTEM_PROMPT: &str = r#"You are an agentic coding assistant running locally. You can only access files via tools. All paths are relative to the project root. Use Glob/Grep to find files before Read. Before Edit/Write, explain what you will change. @@ -25,7 +47,8 @@ fn verbose(ctx: &Context, message: &str) { } } -pub fn run_turn(ctx: &Context, user_input: &str, messages: &mut Vec) -> Result<()> { +pub fn run_turn(ctx: &Context, user_input: &str, messages: &mut Vec) -> Result { + let mut stats = CommandStats::default(); let _ = ctx.transcript.borrow_mut().user_message(user_input); messages.push(json!({ @@ -159,6 +182,12 @@ pub fn run_turn(ctx: &Context, user_input: &str, messages: &mut Vec) -> R client.chat(&request)? }; + // Track token usage from this LLM call + if let Some(usage) = &response.usage { + stats.input_tokens += usage.prompt_tokens; + stats.output_tokens += usage.completion_tokens; + } + if response.choices.is_empty() { println!("No response from model"); break; @@ -204,6 +233,9 @@ pub fn run_turn(ctx: &Context, user_input: &str, messages: &mut Vec) -> R let name = &tc.function.name; let args: Value = serde_json::from_str(&tc.function.arguments).unwrap_or(json!({})); + // Count this tool use + stats.tool_uses += 1; + trace( ctx, "CALL", @@ -287,7 +319,9 @@ pub fn run_turn(ctx: &Context, user_input: &str, messages: &mut Vec) -> R } } else if name == "Task" { // Execute Task tool (subagent delegation) - tools::task::execute(args.clone(), ctx)? + let (result, sub_stats) = tools::task::execute(args.clone(), ctx)?; + stats.merge(&sub_stats); + result } else if name.starts_with("mcp.") { // Execute MCP tool let start = std::time::Instant::now(); @@ -376,5 +410,5 @@ pub fn run_turn(ctx: &Context, user_input: &str, messages: &mut Vec) -> R } } - Ok(()) + Ok(stats) } diff --git a/src/cli.rs b/src/cli.rs index e7d06f6..2b03432 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,5 +1,5 @@ use crate::{ - agent, + agent::{self, CommandStats}, backend::BackendRegistry, config::Config, config::PermissionMode, @@ -16,6 +16,7 @@ use rustyline::error::ReadlineError; use rustyline::DefaultEditor; use std::cell::RefCell; use std::path::PathBuf; +use std::time::{Duration, Instant}; pub struct Context { pub args: Args, @@ -33,9 +34,27 @@ pub struct Context { pub model_router: RefCell, } +/// Print command stats to stderr +fn print_stats(duration: Duration, stats: &CommandStats) { + let tokens = stats.total_tokens(); + let token_display = if tokens >= 1000 { + format!("{:.1}k", tokens as f64 / 1000.0) + } else { + tokens.to_string() + }; + eprintln!( + "[Duration: {:.1}s | Tokens: {} | Tools: {}]", + duration.as_secs_f64(), + token_display, + stats.tool_uses + ); +} + pub fn run_once(ctx: &Context, prompt: &str) -> Result<()> { + let start = Instant::now(); let mut messages = Vec::new(); - agent::run_turn(ctx, prompt, &mut messages)?; + let stats = agent::run_turn(ctx, prompt, &mut messages)?; + print_stats(start.elapsed(), &stats); Ok(()) } @@ -61,8 +80,14 @@ pub fn run_repl(ctx: Context) -> Result<()> { continue; } - if let Err(e) = agent::run_turn(&ctx, line, &mut messages) { - eprintln!("Error: {}", e); + let start = Instant::now(); + match agent::run_turn(&ctx, line, &mut messages) { + Ok(stats) => { + print_stats(start.elapsed(), &stats); + } + Err(e) => { + eprintln!("Error: {}", e); + } } } Err(ReadlineError::Interrupted | ReadlineError::Eof) => break, @@ -447,8 +472,9 @@ fn handle_task_command(ctx: &Context, args: &str) { println!("Running subagent '{}'...", agent_name); // Run the subagent + let start = Instant::now(); match crate::subagent::run_subagent(ctx, &spec, prompt, None) { - Ok(result) => { + Ok((result, stats)) => { if result.ok { println!("\n--- Subagent Output ---"); println!("{}", result.output.text); @@ -461,6 +487,7 @@ fn handle_task_command(ctx: &Context, args: &str) { } else if let Some(error) = &result.error { println!("Subagent error: {} - {}", error.code, error.message); } + print_stats(start.elapsed(), &stats); } Err(e) => { eprintln!("Failed to run subagent: {}", e); diff --git a/src/llm.rs b/src/llm.rs index 6da2e36..67f3a40 100644 --- a/src/llm.rs +++ b/src/llm.rs @@ -12,9 +12,24 @@ pub struct ChatRequest { pub tool_choice: Option, } +/// Token usage statistics from the API response +#[derive(Debug, Deserialize, Default, Clone)] +pub struct Usage { + #[serde(default)] + pub prompt_tokens: u64, + #[serde(default)] + pub completion_tokens: u64, + /// Total tokens from API (may be redundant with prompt_tokens + completion_tokens) + #[serde(default)] + #[allow(dead_code)] + pub total_tokens: u64, +} + #[derive(Debug, Deserialize)] pub struct ChatResponse { pub choices: Vec, + #[serde(default)] + pub usage: Option, } #[derive(Debug, Deserialize)] diff --git a/src/subagent.rs b/src/subagent.rs index 36f26fe..22351a6 100644 --- a/src/subagent.rs +++ b/src/subagent.rs @@ -1,5 +1,6 @@ //! Subagent runtime for executing specialized, restricted agent tasks. +use crate::agent::CommandStats; use crate::config::{AgentSpec, PermissionMode}; use crate::policy::{Decision, PolicyEngine}; use crate::{cli::Context, llm, tools}; @@ -133,13 +134,15 @@ fn trace(ctx: &Context, agent: &str, label: &str, content: &str) { } /// Run a subagent with the given specification and prompt +/// Returns both the result and stats collected during execution pub fn run_subagent( ctx: &Context, spec: &AgentSpec, prompt: &str, input_context: Option, -) -> Result { +) -> Result<(SubagentResult, CommandStats)> { let start_time = Instant::now(); + let mut stats = CommandStats::default(); let agent_name = &spec.name; // Get parent permission mode @@ -297,6 +300,12 @@ pub fn run_subagent( client.chat(&request)? }; + // Track token usage from this LLM call + if let Some(usage) = &response.usage { + stats.input_tokens += usage.prompt_tokens; + stats.output_tokens += usage.completion_tokens; + } + if response.choices.is_empty() { break; } @@ -338,6 +347,9 @@ pub fn run_subagent( let name = &tc.function.name; let args: Value = serde_json::from_str(&tc.function.arguments).unwrap_or(json!({})); + // Count this tool use + stats.tool_uses += 1; + trace( ctx, agent_name, @@ -487,14 +499,17 @@ pub fn run_subagent( &format!("duration={}ms", duration_ms), ); - Ok(SubagentResult { - agent: agent_name.clone(), - ok: !had_errors, - output: SubagentOutput { - text: collected_text, - files_referenced, - proposed_edits, + Ok(( + SubagentResult { + agent: agent_name.clone(), + ok: !had_errors, + output: SubagentOutput { + text: collected_text, + files_referenced, + proposed_edits, + }, + error: last_error, }, - error: last_error, - }) + stats, + )) } diff --git a/src/tools/task.rs b/src/tools/task.rs index cd373fd..ad34620 100644 --- a/src/tools/task.rs +++ b/src/tools/task.rs @@ -1,5 +1,6 @@ //! Task tool for delegating work to subagents. +use crate::agent::CommandStats; use crate::cli::Context; use crate::subagent::{self, InputContext, SubagentResult}; use serde_json::{json, Value}; @@ -50,28 +51,35 @@ pub fn schema() -> Value { } /// Execute the Task tool - delegates to a subagent -pub fn execute(args: Value, ctx: &Context) -> anyhow::Result { +/// Returns both the result JSON and collected stats +pub fn execute(args: Value, ctx: &Context) -> anyhow::Result<(Value, CommandStats)> { let agent_name = match args["agent"].as_str() { Some(name) => name, None => { - return Ok(json!({ - "error": { - "code": "missing_agent", - "message": "Missing required 'agent' parameter" - } - })); + return Ok(( + json!({ + "error": { + "code": "missing_agent", + "message": "Missing required 'agent' parameter" + } + }), + CommandStats::default(), + )); } }; let prompt = match args["prompt"].as_str() { Some(p) => p, None => { - return Ok(json!({ - "error": { - "code": "missing_prompt", - "message": "Missing required 'prompt' parameter" - } - })); + return Ok(( + json!({ + "error": { + "code": "missing_prompt", + "message": "Missing required 'prompt' parameter" + } + }), + CommandStats::default(), + )); } }; @@ -81,12 +89,15 @@ pub fn execute(args: Value, ctx: &Context) -> anyhow::Result { Some(s) => s.clone(), None => { let available: Vec<&String> = config.agents.keys().collect(); - return Ok(json!({ - "error": { - "code": "agent_not_found", - "message": format!("Agent '{}' not found. Available agents: {:?}", agent_name, available) - } - })); + return Ok(( + json!({ + "error": { + "code": "agent_not_found", + "message": format!("Agent '{}' not found. Available agents: {:?}", agent_name, available) + } + }), + CommandStats::default(), + )); } }; drop(config); @@ -98,13 +109,16 @@ pub fn execute(args: Value, ctx: &Context) -> anyhow::Result { // Run the subagent match subagent::run_subagent(ctx, &spec, prompt, input_context) { - Ok(result) => Ok(subagent_result_to_json(&result)), - Err(e) => Ok(json!({ - "error": { - "code": "subagent_error", - "message": e.to_string() - } - })), + Ok((result, sub_stats)) => Ok((subagent_result_to_json(&result), sub_stats)), + Err(e) => Ok(( + json!({ + "error": { + "code": "subagent_error", + "message": e.to_string() + } + }), + CommandStats::default(), + )), } } From 6b3de5cd655c8ce62586e29881760da335ea69f8 Mon Sep 17 00:00:00 2001 From: Jeff Garzik Date: Mon, 22 Dec 2025 03:28:03 -0500 Subject: [PATCH 3/3] persist command history --- Cargo.lock | 94 ++++++++++++++++++++++++++++++++++++++++++++++------ Cargo.toml | 2 +- src/agent.rs | 6 +++- src/cli.rs | 18 ++++++++++ 4 files changed, 108 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 88eda00..b252580 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -133,9 +133,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" -version = "0.1.1" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" @@ -641,9 +641,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.28.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ "bitflags", "cfg-if", @@ -837,9 +837,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rustyline" -version = "14.0.0" +version = "17.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7803e8936da37efd9b6d4478277f4b2b9bb5cdb37a113e8d63222e58da647e63" +checksum = "e902948a25149d50edc1a8e0141aad50f54e22ba83ff988cf8f7c9ef07f50564" dependencies = [ "bitflags", "cfg-if", @@ -854,7 +854,7 @@ dependencies = [ "unicode-segmentation", "unicode-width", "utf8parse", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -1105,9 +1105,9 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" -version = "0.1.14" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "untrusted" @@ -1366,6 +1366,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -1399,13 +1408,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -1418,6 +1444,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -1430,6 +1462,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -1442,12 +1480,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -1460,6 +1510,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -1472,6 +1528,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -1484,6 +1546,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -1496,6 +1564,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "0.7.14" diff --git a/Cargo.toml b/Cargo.toml index 9357404..330c7a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ dirs = "5" dotenvy = "0.15" glob = "0.3" regex = "1" -rustyline = "14" +rustyline = { version = "17", features = ["with-file-history"] } serde = { version = "1", features = ["derive"] } serde_json = "1" serde_yaml = { package = "serde_yml", version = "0.0.12" } diff --git a/src/agent.rs b/src/agent.rs index e407d62..5cfce1d 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -47,7 +47,11 @@ fn verbose(ctx: &Context, message: &str) { } } -pub fn run_turn(ctx: &Context, user_input: &str, messages: &mut Vec) -> Result { +pub fn run_turn( + ctx: &Context, + user_input: &str, + messages: &mut Vec, +) -> Result { let mut stats = CommandStats::default(); let _ = ctx.transcript.borrow_mut().user_message(user_input); diff --git a/src/cli.rs b/src/cli.rs index 2b03432..28e7ba9 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -18,6 +18,14 @@ use std::cell::RefCell; use std::path::PathBuf; use std::time::{Duration, Instant}; +/// Get the path to the history file +fn history_path() -> PathBuf { + dirs::home_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join(".yo") + .join("history") +} + pub struct Context { pub args: Args, pub root: PathBuf, @@ -62,6 +70,10 @@ pub fn run_repl(ctx: Context) -> Result<()> { let mut rl = DefaultEditor::new()?; let mut messages = Vec::new(); + // Load command history + let history_file = history_path(); + let _ = rl.load_history(&history_file); + println!("yo - type /help for commands, /exit to quit"); loop { @@ -98,6 +110,12 @@ pub fn run_repl(ctx: Context) -> Result<()> { } } + // Save command history (create parent directory if needed) + if let Some(parent) = history_file.parent() { + let _ = std::fs::create_dir_all(parent); + } + let _ = rl.save_history(&history_file); + Ok(()) }