Fix /resume to show all sessions and use human-readable timestamps

- Change run_autonomous to return Agent instead of () so session
  continuation is properly saved in accumulative mode
- Update format_session_time to show relative times ("2 hours ago",
  "yesterday") for recent sessions and dates for older ones
- Handle Ctrl+C cancellation gracefully with informative message
This commit is contained in:
Dhanji R. Prasanna
2026-01-11 06:13:27 +08:00
parent 8926775acb
commit 3fcef587e8
2 changed files with 101 additions and 22 deletions

View File

@@ -512,6 +512,9 @@ pub async fn run() -> Result<()> {
// Then load README for project context // Then load README for project context
let readme_content = read_project_readme(&workspace_dir); let readme_content = read_project_readme(&workspace_dir);
// Load project memory if available
let memory_content = read_project_memory(&workspace_dir);
// Create project model // Create project model
let project = if cli.autonomous { let project = if cli.autonomous {
if let Some(requirements_text) = &cli.requirements { if let Some(requirements_text) = &cli.requirements {
@@ -587,12 +590,23 @@ pub async fn run() -> Result<()> {
// Initialize agent // Initialize agent
// ui_writer will be created conditionally based on machine mode // ui_writer will be created conditionally based on machine mode
// Combine AGENTS.md and README content if both exist // Combine AGENTS.md, README, and memory content if they exist
let combined_content = match (agents_content.clone(), readme_content.clone()) { let combined_content = {
(Some(agents), Some(readme)) => Some(format!("{}\n\n{}", agents, readme)), let mut parts = Vec::new();
(Some(agents), None) => Some(agents), if let Some(agents) = agents_content.clone() {
(None, Some(readme)) => Some(readme), parts.push(agents);
(None, None) => None, }
if let Some(readme) = readme_content.clone() {
parts.push(readme);
}
if let Some(memory) = memory_content.clone() {
parts.push(memory);
}
if parts.is_empty() {
None
} else {
Some(parts.join("\n\n"))
}
}; };
// Execute task, autonomous mode, or start interactive mode based on machine mode // Execute task, autonomous mode, or start interactive mode based on machine mode
@@ -1168,25 +1182,32 @@ async fn run_accumulative_mode(
// Run autonomous mode with the accumulated requirements // Run autonomous mode with the accumulated requirements
let autonomous_result = tokio::select! { let autonomous_result = tokio::select! {
result = run_autonomous( result = run_autonomous(
agent, agent,
project, project,
cli.show_prompt, cli.show_prompt,
cli.show_code, cli.show_code,
cli.max_turns, cli.max_turns,
cli.quiet, cli.quiet,
cli.codebase_fast_start.clone(), cli.codebase_fast_start.clone(),
) => result, ) => result.map(Some),
_ = tokio::signal::ctrl_c() => { _ = tokio::signal::ctrl_c() => {
output.print("\n⚠️ Autonomous run cancelled by user (Ctrl+C)"); output.print("\n⚠️ Autonomous run cancelled by user (Ctrl+C)");
Ok(()) // Agent was moved into run_autonomous and is now dropped
// We can't save continuation here, but the next iteration will create a new agent
Ok(None)
} }
}; };
match autonomous_result { match autonomous_result {
Ok(_) => { Ok(Some(_returned_agent)) => {
// Session continuation was already saved by run_autonomous
output.print(""); output.print("");
output.print("✅ Autonomous run completed"); output.print("✅ Autonomous run completed");
} }
Ok(None) => {
// Ctrl+C case - agent was dropped, continuation not saved
output.print(" (session continuation not saved due to cancellation)");
}
Err(e) => { Err(e) => {
output.print(""); output.print("");
output.print(&format!("❌ Autonomous run failed: {}", e)); output.print(&format!("❌ Autonomous run failed: {}", e));
@@ -1280,7 +1301,7 @@ async fn run_with_console_mode(
// Execute task, autonomous mode, or start interactive mode // Execute task, autonomous mode, or start interactive mode
if cli.autonomous { if cli.autonomous {
// Autonomous mode with coach-player feedback loop // Autonomous mode with coach-player feedback loop
run_autonomous( let _agent = run_autonomous(
agent, agent,
project, project,
cli.show_prompt, cli.show_prompt,
@@ -1447,6 +1468,30 @@ fn read_project_readme(workspace_dir: &Path) -> Option<String> {
None None
} }
/// Read project memory if available
fn read_project_memory(workspace_dir: &Path) -> Option<String> {
let memory_path = workspace_dir.join(".g3").join("memory.md");
if memory_path.exists() {
match std::fs::read_to_string(&memory_path) {
Ok(content) => {
let size = if content.len() < 1000 {
format!("{} chars", content.len())
} else {
format!("{:.1}k chars", content.len() as f64 / 1000.0)
};
Some(format!(
"🧠 Project Memory ({}):\n\n{}",
size, content
))
}
Err(_) => None,
}
} else {
None
}
}
/// Extract the main heading or title from README content /// Extract the main heading or title from README content
fn extract_readme_heading(readme_content: &str) -> Option<String> { fn extract_readme_heading(readme_content: &str) -> Option<String> {
// Find the README section in the combined content // Find the README section in the combined content
@@ -1594,6 +1639,7 @@ async fn run_interactive<W: UiWriter>(
// Check what was loaded // Check what was loaded
let has_agents = content.contains("Agent Configuration"); let has_agents = content.contains("Agent Configuration");
let has_readme = content.contains("Project README"); let has_readme = content.contains("Project README");
let has_memory = content.contains("Project Memory");
if has_agents { if has_agents {
print!( print!(
@@ -1615,6 +1661,14 @@ async fn run_interactive<W: UiWriter>(
ResetColor ResetColor
); );
} }
if has_memory {
print!(
"{}🧠 Project memory loaded{}\n",
SetForegroundColor(Color::DarkGrey),
ResetColor
);
}
} }
// Display workspace path // Display workspace path
@@ -2354,7 +2408,7 @@ async fn run_autonomous(
max_turns: usize, max_turns: usize,
quiet: bool, quiet: bool,
codebase_fast_start: Option<PathBuf>, codebase_fast_start: Option<PathBuf>,
) -> Result<()> { ) -> Result<Agent<ConsoleUiWriter>> {
let start_time = std::time::Instant::now(); let start_time = std::time::Instant::now();
let output = SimpleOutput::new(); let output = SimpleOutput::new();
let mut turn_metrics: Vec<TurnMetrics> = Vec::new(); let mut turn_metrics: Vec<TurnMetrics> = Vec::new();
@@ -2411,7 +2465,7 @@ async fn run_autonomous(
output.print(&generate_turn_histogram(&turn_metrics)); output.print(&generate_turn_histogram(&turn_metrics));
output.print(&"=".repeat(60)); output.print(&"=".repeat(60));
return Ok(()); return Ok(agent);
} }
// Read requirements // Read requirements
@@ -2453,7 +2507,7 @@ async fn run_autonomous(
output.print(&generate_turn_histogram(&turn_metrics)); output.print(&generate_turn_histogram(&turn_metrics));
output.print(&"=".repeat(60)); output.print(&"=".repeat(60));
return Ok(()); return Ok(agent);
} }
}; };
@@ -3076,5 +3130,5 @@ Remember: Be clear in your review and concise in your feedback. APPROVE iff the
// Save session continuation for resume capability // Save session continuation for resume capability
agent.save_session_continuation(None); agent.save_session_continuation(None);
Ok(()) Ok(agent)
} }

View File

@@ -434,7 +434,32 @@ pub fn format_session_time(created_at: &str) -> String {
match chrono::DateTime::parse_from_rfc3339(created_at) { match chrono::DateTime::parse_from_rfc3339(created_at) {
Ok(dt) => { Ok(dt) => {
let local: chrono::DateTime<chrono::Local> = dt.into(); let local: chrono::DateTime<chrono::Local> = dt.into();
local.format("%Y-%m-%d %H:%M").to_string() let now = chrono::Local::now();
let duration = now.signed_duration_since(local);
// Show relative time for recent sessions, absolute for older ones
if duration.num_minutes() < 1 {
"just now".to_string()
} else if duration.num_minutes() < 60 {
format!("{} min ago", duration.num_minutes())
} else if duration.num_hours() < 24 {
let hours = duration.num_hours();
if hours == 1 {
"1 hour ago".to_string()
} else {
format!("{} hours ago", hours)
}
} else if duration.num_days() < 7 {
let days = duration.num_days();
if days == 1 {
"yesterday".to_string()
} else {
format!("{} days ago", days)
}
} else {
// For older sessions, show the date
local.format("%b %d, %Y").to_string()
}
} }
Err(_) => created_at.to_string(), Err(_) => created_at.to_string(),
} }