Refine planner mode UI, logging, and history tracking
- Display coach feedback content (up to 25 lines) instead of just length - Write GIT COMMIT entry to history before actual commit for better a... - Implement single-line status updates during LLM processing with too... - Display non-tool LLM text responses in planner UI - Redirect all logs to <workspace>/logs directory instead of codepath - Preserve TODO file in planner mode for history (prevent deletion) Completed files: - completed_requirements_2025-12-09_16-16-51.md - completed_todo_2025-12-09_16-16-51.md
This commit is contained in:
@@ -129,26 +129,27 @@ impl ErrorContext {
|
||||
return;
|
||||
}
|
||||
|
||||
let logs_dir = std::path::Path::new("logs/errors");
|
||||
let base_logs_dir = crate::logs_dir();
|
||||
let logs_dir = base_logs_dir.join("errors");
|
||||
if !logs_dir.exists() {
|
||||
if let Err(e) = std::fs::create_dir_all(logs_dir) {
|
||||
if let Err(e) = std::fs::create_dir_all(&logs_dir) {
|
||||
error!("Failed to create error logs directory: {}", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let filename = format!(
|
||||
"logs/errors/error_{}_{}.json",
|
||||
let filename = logs_dir.join(format!(
|
||||
"error_{}_{}.json",
|
||||
self.timestamp,
|
||||
self.session_id.as_deref().unwrap_or("unknown")
|
||||
);
|
||||
));
|
||||
|
||||
match serde_json::to_string_pretty(self) {
|
||||
Ok(json_content) => {
|
||||
if let Err(e) = std::fs::write(&filename, json_content) {
|
||||
error!("Failed to save error context to {}: {}", filename, e);
|
||||
error!("Failed to save error context to {:?}: {}", &filename, e);
|
||||
} else {
|
||||
info!("Error details saved to: {}", filename);
|
||||
info!("Error details saved to: {:?}", &filename);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
|
||||
@@ -52,6 +52,27 @@ fn get_todo_path() -> std::path::PathBuf {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the path to the logs directory.
|
||||
///
|
||||
/// Checks for G3_WORKSPACE_PATH environment variable first (used by planning mode),
|
||||
/// then falls back to "logs" in the current directory.
|
||||
fn get_logs_dir() -> std::path::PathBuf {
|
||||
if let Ok(workspace_path) = std::env::var("G3_WORKSPACE_PATH") {
|
||||
std::path::PathBuf::from(workspace_path).join("logs")
|
||||
} else {
|
||||
std::env::current_dir().unwrap_or_default().join("logs")
|
||||
}
|
||||
}
|
||||
|
||||
/// Public accessor for the logs directory path (for use by submodules)
|
||||
pub fn logs_dir() -> std::path::PathBuf {
|
||||
get_logs_dir()
|
||||
}
|
||||
|
||||
/// Environment variable name for workspace path
|
||||
/// Used to direct all logs to the workspace directory
|
||||
pub const G3_WORKSPACE_PATH_ENV: &str = "G3_WORKSPACE_PATH";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ToolCall {
|
||||
pub tool: String,
|
||||
@@ -1832,18 +1853,19 @@ impl<W: UiWriter> Agent<W> {
|
||||
|
||||
TOOL_LOG
|
||||
.get_or_init(|| {
|
||||
if let Err(e) = std::fs::create_dir_all("logs") {
|
||||
let logs_dir = get_logs_dir();
|
||||
if let Err(e) = std::fs::create_dir_all(&logs_dir) {
|
||||
error!("Failed to create logs directory for tool log: {}", e);
|
||||
return None;
|
||||
}
|
||||
|
||||
let ts = Local::now().format("%Y%m%d_%H%M%S").to_string();
|
||||
let path = format!("logs/tool_calls_{}.log", ts);
|
||||
let path = logs_dir.join(format!("tool_calls_{}.log", ts));
|
||||
|
||||
match OpenOptions::new().create(true).append(true).open(&path) {
|
||||
Ok(file) => Some(Mutex::new(file)),
|
||||
Err(e) => {
|
||||
error!("Failed to open tool log file {}: {}", path, e);
|
||||
error!("Failed to open tool log file {:?}: {}", path, e);
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -2202,9 +2224,9 @@ impl<W: UiWriter> Agent<W> {
|
||||
.as_secs();
|
||||
|
||||
// Create logs directory if it doesn't exist
|
||||
let logs_dir = std::path::Path::new("logs");
|
||||
let logs_dir = get_logs_dir();
|
||||
if !logs_dir.exists() {
|
||||
if let Err(e) = std::fs::create_dir_all(logs_dir) {
|
||||
if let Err(e) = std::fs::create_dir_all(&logs_dir) {
|
||||
error!("Failed to create logs directory: {}", e);
|
||||
return;
|
||||
}
|
||||
@@ -2212,9 +2234,9 @@ impl<W: UiWriter> Agent<W> {
|
||||
|
||||
// Use session-based filename if we have a session ID, otherwise fall back to timestamp
|
||||
let filename = if let Some(ref session_id) = self.session_id {
|
||||
format!("logs/g3_session_{}.json", session_id)
|
||||
logs_dir.join(format!("g3_session_{}.json", session_id))
|
||||
} else {
|
||||
format!("logs/g3_context_{}.json", timestamp)
|
||||
logs_dir.join(format!("g3_context_{}.json", timestamp))
|
||||
};
|
||||
|
||||
let context_data = serde_json::json!({
|
||||
@@ -2231,8 +2253,8 @@ impl<W: UiWriter> Agent<W> {
|
||||
|
||||
match serde_json::to_string_pretty(&context_data) {
|
||||
Ok(json_content) => {
|
||||
if let Err(e) = std::fs::write(&filename, json_content) {
|
||||
error!("Failed to save context window to {}: {}", filename, e);
|
||||
if let Err(e) = std::fs::write(&filename, &json_content) {
|
||||
error!("Failed to save context window to {:?}: {}", &filename, e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -2290,17 +2312,17 @@ impl<W: UiWriter> Agent<W> {
|
||||
};
|
||||
|
||||
// Create logs directory if it doesn't exist
|
||||
let logs_dir = std::path::Path::new("logs");
|
||||
let logs_dir = get_logs_dir();
|
||||
if !logs_dir.exists() {
|
||||
if let Err(e) = std::fs::create_dir_all(logs_dir) {
|
||||
if let Err(e) = std::fs::create_dir_all(&logs_dir) {
|
||||
error!("Failed to create logs directory: {}", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate filename using same pattern as save_context_window
|
||||
let filename = format!("logs/context_window_{}.txt", session_id);
|
||||
let symlink_path = "logs/current_context_window";
|
||||
let filename = logs_dir.join(format!("context_window_{}.txt", session_id));
|
||||
let symlink_path = logs_dir.join("current_context_window");
|
||||
|
||||
// Build the summary content
|
||||
let mut summary_lines = Vec::new();
|
||||
@@ -2354,23 +2376,23 @@ impl<W: UiWriter> Agent<W> {
|
||||
let summary_content = summary_lines.join("");
|
||||
if let Err(e) = std::fs::write(&filename, summary_content) {
|
||||
error!(
|
||||
"Failed to write context window summary to {}: {}",
|
||||
filename, e
|
||||
"Failed to write context window summary to {:?}: {}",
|
||||
&filename, e
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update symlink
|
||||
// Remove old symlink if it exists
|
||||
let _ = std::fs::remove_file(symlink_path);
|
||||
let _ = std::fs::remove_file(&symlink_path);
|
||||
|
||||
// Create new symlink
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::symlink;
|
||||
let target = format!("context_window_{}.txt", session_id);
|
||||
if let Err(e) = symlink(&target, symlink_path) {
|
||||
error!("Failed to create symlink {}: {}", symlink_path, e);
|
||||
if let Err(e) = symlink(&target, &symlink_path) {
|
||||
error!("Failed to create symlink {:?}: {}", &symlink_path, e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2378,13 +2400,13 @@ impl<W: UiWriter> Agent<W> {
|
||||
{
|
||||
use std::os::windows::fs::symlink_file;
|
||||
let target = format!("context_window_{}.txt", session_id);
|
||||
if let Err(e) = symlink_file(&target, symlink_path) {
|
||||
error!("Failed to create symlink {}: {}", symlink_path, e);
|
||||
if let Err(e) = symlink_file(&target, &symlink_path) {
|
||||
error!("Failed to create symlink {:?}: {}", &symlink_path, e);
|
||||
}
|
||||
}
|
||||
|
||||
debug!(
|
||||
"Context window summary written to {} ({} messages)",
|
||||
"Context window summary written to {:?} ({} messages)",
|
||||
filename,
|
||||
self.context_window.conversation_history.len()
|
||||
);
|
||||
@@ -2443,7 +2465,8 @@ impl<W: UiWriter> Agent<W> {
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
|
||||
let filename = format!("logs/g3_session_{}.json", session_id);
|
||||
let logs_dir = get_logs_dir();
|
||||
let filename = logs_dir.join(format!("g3_session_{}.json", session_id));
|
||||
|
||||
// Read existing session log
|
||||
let mut session_data: serde_json::Value = if std::path::Path::new(&filename).exists() {
|
||||
@@ -5293,8 +5316,11 @@ impl<W: UiWriter> Agent<W> {
|
||||
});
|
||||
|
||||
// If all todos are complete, delete the file instead of writing
|
||||
if !has_incomplete && (content_str.contains("- [x]") || content_str.contains("- [X]")) {
|
||||
let todo_path = get_todo_path();
|
||||
// EXCEPT in planner mode (G3_TODO_PATH is set) - preserve for rename to completed_todo_*.md
|
||||
let in_planner_mode = std::env::var("G3_TODO_PATH").is_ok();
|
||||
let todo_path = get_todo_path();
|
||||
|
||||
if !in_planner_mode && !has_incomplete && (content_str.contains("- [x]") || content_str.contains("- [X]")) {
|
||||
if todo_path.exists() {
|
||||
match std::fs::remove_file(&todo_path) {
|
||||
Ok(_) => {
|
||||
@@ -5315,7 +5341,6 @@ impl<W: UiWriter> Agent<W> {
|
||||
}
|
||||
|
||||
// Write to todo.g3.md file (uses G3_TODO_PATH env var if set, else current dir)
|
||||
let todo_path = get_todo_path();
|
||||
|
||||
match std::fs::write(&todo_path, content_str) {
|
||||
Ok(_) => {
|
||||
|
||||
@@ -11,8 +11,6 @@ use std::fs::{self, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::prompts;
|
||||
|
||||
/// Format a timestamp for planner_history.txt entries
|
||||
/// Format: YYYY-MM-DD HH:MM:SS (ISO 8601 for readability)
|
||||
pub fn format_timestamp() -> String {
|
||||
|
||||
@@ -196,12 +196,19 @@ pub fn extract_summary(response: &str) -> Option<String> {
|
||||
|
||||
/// Write the codebase report to logs directory
|
||||
fn write_code_report(report: &str) -> Result<()> {
|
||||
// Ensure logs directory exists
|
||||
fs::create_dir_all("logs")?;
|
||||
// Get logs directory from workspace path or current dir
|
||||
let logs_dir = if let Ok(workspace_path) = std::env::var("G3_WORKSPACE_PATH") {
|
||||
std::path::PathBuf::from(workspace_path).join("logs")
|
||||
} else {
|
||||
std::env::current_dir().unwrap_or_default().join("logs")
|
||||
};
|
||||
|
||||
// Ensure logs directory exists
|
||||
fs::create_dir_all(&logs_dir)?;
|
||||
|
||||
// Generate timestamp in same format as tool_calls log
|
||||
let timestamp = Local::now().format("%Y%m%d_%H%M%S").to_string();
|
||||
let filename = format!("logs/code_report_{}.log", timestamp);
|
||||
let filename = logs_dir.join(format!("code_report_{}.log", timestamp));
|
||||
|
||||
// Write the report to file
|
||||
let mut file = OpenOptions::new()
|
||||
@@ -218,12 +225,19 @@ fn write_code_report(report: &str) -> Result<()> {
|
||||
|
||||
/// Write the discovery commands to logs directory
|
||||
fn write_discovery_commands(commands: &[String]) -> Result<()> {
|
||||
// Get logs directory from workspace path or current dir
|
||||
let logs_dir = if let Ok(workspace_path) = std::env::var("G3_WORKSPACE_PATH") {
|
||||
std::path::PathBuf::from(workspace_path).join("logs")
|
||||
} else {
|
||||
std::env::current_dir().unwrap_or_default().join("logs")
|
||||
};
|
||||
|
||||
// Ensure logs directory exists
|
||||
fs::create_dir_all("logs")?;
|
||||
fs::create_dir_all(&logs_dir)?;
|
||||
|
||||
// Generate timestamp in same format as tool_calls log
|
||||
let timestamp = Local::now().format("%Y%m%d_%H%M%S").to_string();
|
||||
let filename = format!("logs/discovery_commands_{}.log", timestamp);
|
||||
let filename = logs_dir.join(format!("discovery_commands_{}.log", timestamp));
|
||||
|
||||
// Write the commands to file
|
||||
let mut file = OpenOptions::new()
|
||||
|
||||
@@ -182,8 +182,33 @@ pub async fn generate_commit_message(
|
||||
}
|
||||
|
||||
/// A simple UiWriter implementation for planner output
|
||||
/// Uses single-line status updates during LLM processing
|
||||
#[derive(Clone)]
|
||||
pub struct PlannerUiWriter;
|
||||
pub struct PlannerUiWriter {
|
||||
tool_count: std::sync::Arc<std::sync::atomic::AtomicUsize>,
|
||||
}
|
||||
|
||||
impl Default for PlannerUiWriter {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl PlannerUiWriter {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
tool_count: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear the current line and print a status message
|
||||
fn print_status_line(&self, message: &str) {
|
||||
use std::io::Write;
|
||||
// Use carriage return to overwrite previous line, pad to 80 chars to clear old content
|
||||
print!("\r{:<80}", message);
|
||||
std::io::stdout().flush().ok();
|
||||
}
|
||||
}
|
||||
|
||||
impl g3_core::ui_writer::UiWriter for PlannerUiWriter {
|
||||
fn print(&self, message: &str) {
|
||||
@@ -209,7 +234,11 @@ impl g3_core::ui_writer::UiWriter for PlannerUiWriter {
|
||||
}
|
||||
|
||||
fn print_tool_header(&self, tool_name: &str) {
|
||||
println!("🔧 {}", tool_name);
|
||||
// Increment tool count and show on single line
|
||||
let count = self.tool_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst) + 1;
|
||||
// Clear the "Thinking..." line and print tool header on new line
|
||||
print!("\r{:<80}\n", ""); // Clear status line
|
||||
println!("🔧 [{}] {}", count, tool_name);
|
||||
}
|
||||
|
||||
fn print_tool_arg(&self, _key: &str, _value: &str) {}
|
||||
@@ -218,9 +247,25 @@ impl g3_core::ui_writer::UiWriter for PlannerUiWriter {
|
||||
fn print_tool_output_line(&self, _line: &str) {}
|
||||
fn print_tool_output_summary(&self, _hidden_count: usize) {}
|
||||
fn print_tool_timing(&self, _duration_str: &str) {}
|
||||
fn print_agent_prompt(&self) {}
|
||||
fn print_agent_response(&self, _content: &str) {}
|
||||
fn notify_sse_received(&self) {}
|
||||
|
||||
fn print_agent_prompt(&self) {
|
||||
// Clear any status line before agent response
|
||||
print!("\r{:<80}\n", "");
|
||||
}
|
||||
|
||||
fn print_agent_response(&self, content: &str) {
|
||||
// Display non-tool text messages from LLM
|
||||
if !content.trim().is_empty() {
|
||||
print!("{}", content);
|
||||
use std::io::Write;
|
||||
std::io::stdout().flush().ok();
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_sse_received(&self) {
|
||||
// Show "Thinking..." status on single line
|
||||
self.print_status_line("💭 Thinking...");
|
||||
}
|
||||
|
||||
fn flush(&self) {
|
||||
use std::io::Write;
|
||||
@@ -254,7 +299,7 @@ pub async fn call_refinement_llm_with_tools(
|
||||
|
||||
// Create agent with planner config
|
||||
let planner_config = config.for_planner()?;
|
||||
let ui_writer = PlannerUiWriter;
|
||||
let ui_writer = PlannerUiWriter::new();
|
||||
|
||||
// Create project pointing to codepath as workspace
|
||||
let workspace = std::path::PathBuf::from(codepath);
|
||||
|
||||
@@ -11,7 +11,6 @@ use std::path::{Path, PathBuf};
|
||||
use crate::git;
|
||||
use crate::history;
|
||||
use crate::llm;
|
||||
use crate::prompts;
|
||||
use crate::state::{
|
||||
ApprovalChoice, BranchConfirmChoice, CompletionChoice, DirtyFilesChoice,
|
||||
PlannerState, RecoveryChoice, RecoveryInfo,
|
||||
@@ -482,14 +481,14 @@ pub fn stage_and_commit(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Log commit to history BEFORE making the commit (provides audit trail even if commit fails)
|
||||
history::write_git_commit(&config.plan_dir(), summary)?;
|
||||
|
||||
// Make commit
|
||||
print_msg("📝 Making git commit...");
|
||||
let _commit_sha = git::commit(&config.codepath, summary, description)?;
|
||||
print_msg("✅ Commit successful");
|
||||
|
||||
// Log commit to history
|
||||
history::write_git_commit(&config.plan_dir(), summary)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -588,6 +587,9 @@ pub async fn run_coach_player_loop(
|
||||
// Set environment variable for custom todo path
|
||||
std::env::set_var("G3_TODO_PATH", planner_config.todo_path().display().to_string());
|
||||
|
||||
// Set environment variable for workspace path (used for logs)
|
||||
std::env::set_var("G3_WORKSPACE_PATH", planner_config.codepath.display().to_string());
|
||||
|
||||
let mut turn = 1;
|
||||
let mut coach_feedback = String::new();
|
||||
|
||||
@@ -598,7 +600,7 @@ pub async fn run_coach_player_loop(
|
||||
print_msg("🎯 Player: Implementing requirements...");
|
||||
|
||||
let player_config = g3_config.for_player()?;
|
||||
let ui_writer = llm::PlannerUiWriter;
|
||||
let ui_writer = llm::PlannerUiWriter::new();
|
||||
let mut player_agent = Agent::new_autonomous_with_readme_and_quiet(
|
||||
player_config,
|
||||
ui_writer,
|
||||
@@ -633,7 +635,7 @@ pub async fn run_coach_player_loop(
|
||||
print_msg("🎓 Coach: Reviewing implementation...");
|
||||
|
||||
let coach_config = g3_config.for_coach()?;
|
||||
let coach_ui_writer = llm::PlannerUiWriter;
|
||||
let coach_ui_writer = llm::PlannerUiWriter::new();
|
||||
let mut coach_agent = Agent::new_autonomous_with_readme_and_quiet(
|
||||
coach_config,
|
||||
coach_ui_writer,
|
||||
@@ -657,7 +659,19 @@ pub async fn run_coach_player_loop(
|
||||
return Ok(());
|
||||
}
|
||||
coach_feedback = result.response;
|
||||
print_msg(&format!("📝 Coach feedback: {} chars", coach_feedback.len()));
|
||||
// Display first 25 lines of coach feedback
|
||||
let lines: Vec<&str> = coach_feedback.lines().collect();
|
||||
let display_lines = if lines.len() > 25 {
|
||||
let mut truncated: Vec<&str> = lines[..25].to_vec();
|
||||
truncated.push("...");
|
||||
truncated
|
||||
} else {
|
||||
lines
|
||||
};
|
||||
print_msg(&format!("📝 Coach feedback ({} chars):", coach_feedback.len()));
|
||||
for line in display_lines {
|
||||
print_msg(&format!(" {}", line));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
print_msg(&format!("⚠️ Coach error: {}", e));
|
||||
|
||||
Reference in New Issue
Block a user