Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <SESSION_ID> # resume a specific session by UUID
codewhale fork <SESSION_ID> # fork a saved session into a sibling path
Expand All @@ -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
Expand Down
28 changes: 27 additions & 1 deletion crates/tui/src/session_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<Utc>, age: &str) -> String {
format!("{} ({age})", dt.format("%Y-%m-%d %H:%M UTC"))
}

/// Format a datetime as relative age
fn format_age(dt: &DateTime<Utc>) -> String {
let now = Utc::now();
Expand Down Expand Up @@ -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");
Expand Down
36 changes: 35 additions & 1 deletion crates/tui/src/tui/session_picker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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::<Vec<_>>()
.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");
Expand Down
5 changes: 5 additions & 0 deletions docs/GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading