From 61eb6639e453dd35c87f748e30650b7702c60c2c Mon Sep 17 00:00:00 2001 From: cyq <15000851237@163.com> Date: Tue, 2 Jun 2026 09:58:22 +0800 Subject: [PATCH] fix(tui): show session timestamps in listings --- README.md | 7 +++++- crates/tui/src/session_manager.rs | 28 +++++++++++++++++++++- crates/tui/src/tui/session_picker.rs | 36 +++++++++++++++++++++++++++- docs/GUIDE.md | 5 ++++ 4 files changed, 73 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 177187d25..01aa14cf9 100644 --- a/README.md +++ b/README.md @@ -387,7 +387,7 @@ codewhale doctor --json # machine-readable diagnostics codewhale setup --status # read-only setup status codewhale setup --tools --plugins # scaffold tool/plugin dirs codewhale models # list live API models -codewhale sessions # list saved sessions +codewhale sessions # list saved sessions with timestamps codewhale resume --last # resume the most recent session in this workspace codewhale resume # resume a specific session by UUID codewhale fork # fork a saved session into a sibling path @@ -409,6 +409,11 @@ id in metadata, and opens that fork so you can explore an alternate direction without polluting the original path. The session picker and `codewhale sessions` mark forked sessions with their parent id. +`codewhale sessions` lists saved sessions across workspaces and includes the +last-updated timestamp. `codewhale resume --last` and `codewhale --continue` +choose the latest session for the current workspace; pass an explicit session id +when resuming work from another directory. + Inside the TUI, Esc-Esc backtrack can rewind the active transcript to a prior user prompt and put that prompt back in the composer for editing. `/restore` and `revert_turn` are separate workspace rollback tools: they restore files diff --git a/crates/tui/src/session_manager.rs b/crates/tui/src/session_manager.rs index 9feb84ee9..cb0282258 100644 --- a/crates/tui/src/session_manager.rs +++ b/crates/tui/src/session_manager.rs @@ -957,6 +957,7 @@ fn truncate_title(s: &str, max_len: usize) -> String { /// Format a session for display in a picker pub fn format_session_line(meta: &SessionMetadata) -> String { let age = format_age(&meta.updated_at); + let updated = format_session_updated_at(&meta.updated_at, &age); let truncated_title = truncate_title(extract_title(&meta.title), 40); let fork_label = meta .parent_session_id @@ -970,10 +971,14 @@ pub fn format_session_line(meta: &SessionMetadata) -> String { truncated_title, meta.message_count, fork_label, - age + updated ) } +pub(crate) fn format_session_updated_at(dt: &DateTime, age: &str) -> String { + format!("{} ({age})", dt.format("%Y-%m-%d %H:%M UTC")) +} + /// Format a datetime as relative age fn format_age(dt: &DateTime) -> String { let now = Utc::now(); @@ -1480,6 +1485,27 @@ mod tests { assert_eq!(format_age(&day_ago), "3d ago"); } + #[test] + fn format_session_line_includes_absolute_updated_timestamp() { + let mut session = create_saved_session( + &[make_test_message("user", "Find Friday work")], + "test-model", + Path::new("/tmp/project"), + 100, + None, + ); + session.metadata.updated_at = DateTime::parse_from_rfc3339("2026-06-01T12:34:00Z") + .expect("timestamp") + .with_timezone(&Utc); + + let line = format_session_line(&session.metadata); + + assert!( + line.contains("2026-06-01 12:34 UTC"), + "session list should include an absolute timestamp, got {line:?}" + ); + } + #[test] fn test_update_session() { let tmp = tempdir().expect("tempdir"); diff --git a/crates/tui/src/tui/session_picker.rs b/crates/tui/src/tui/session_picker.rs index 1cfbad951..2543644ac 100644 --- a/crates/tui/src/tui/session_picker.rs +++ b/crates/tui/src/tui/session_picker.rs @@ -692,7 +692,8 @@ fn build_list_lines( } fn format_session_line(session: &SessionMetadata) -> String { - let updated = format_relative_time(&session.updated_at); + let age = format_relative_time(&session.updated_at); + let updated = crate::session_manager::format_session_updated_at(&session.updated_at, &age); let raw_title = extract_title(&session.title); let title = if raw_title == "Session" { truncate(crate::session_manager::truncate_id(&session.id), 32) @@ -1111,6 +1112,39 @@ mod tests { assert!(span.style.add_modifier.contains(Modifier::BOLD)); } + #[test] + fn build_list_lines_includes_absolute_updated_timestamp() { + let mut session = test_session(1, "last friday thread"); + session.updated_at = DateTime::parse_from_rfc3339("2026-06-01T12:34:00Z") + .expect("timestamp") + .with_timezone(&Utc); + let lines = build_list_lines( + &[session], + 0, + 120, + 0, + 5, + false, + "", + "recent", + false, + false, + "", + None, + ); + + let rendered = lines + .iter() + .flat_map(|line| line.spans.iter()) + .map(|span| span.content.as_ref()) + .collect::>() + .join("\n"); + assert!( + rendered.contains("2026-06-01 12:34 UTC"), + "session picker should include an absolute timestamp, got {rendered:?}" + ); + } + #[test] fn build_list_lines_marks_fork_lineage() { let mut forked = test_session(1, "forked path"); diff --git a/docs/GUIDE.md b/docs/GUIDE.md index fa58b6966..a10bab14a 100644 --- a/docs/GUIDE.md +++ b/docs/GUIDE.md @@ -497,6 +497,11 @@ CodeWhale saves sessions. Use the session picker or resume/continue CLI paths documented in the README and modes guide. For a risky experiment, fork the session before changing direction. +The `/sessions` picker starts scoped to the current workspace so resumes stay +attached to the project you opened. Press `a` in the picker to show sessions +from every workspace, or run `codewhale sessions` to list all saved sessions +with last-updated timestamps before resuming a specific id. + ### What should I do when the model gets confused? Stop and restate the goal, constraints, and current evidence. If the transcript