Skip to content
Merged
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
1 change: 1 addition & 0 deletions crates/goose-cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ pub mod bench;
pub mod configure;
pub mod info;
pub mod mcp;
pub mod session;
pub mod update;
47 changes: 47 additions & 0 deletions crates/goose-cli/src/commands/session.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use anyhow::Result;
use goose::session::info::{get_session_info, SessionInfo};

pub fn handle_session_list(verbose: bool, format: String) -> Result<()> {
let sessions = match get_session_info() {
Ok(sessions) => sessions,
Err(e) => {
tracing::error!("Failed to list sessions: {:?}", e);
return Err(anyhow::anyhow!("Failed to list sessions"));
}
};

match format.as_str() {
"json" => {
println!("{}", serde_json::to_string(&sessions)?);
}
_ => {
if sessions.is_empty() {
println!("No sessions found");
return Ok(());
} else {
println!("Available sessions:");
for SessionInfo {
id,
path,
metadata,
modified,
} in sessions
{
let description = if metadata.description.is_empty() {
"(none)"
} else {
&metadata.description
};
let output = format!("{} - {} - {}", id, description, modified);
if verbose {
println!(" {}", output);
println!(" Path: {}", path);
} else {
println!("{}", output);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

currently the output looks like this:

❯ goose session list
Available sessions:
  20250307_121402
  20250307_122909
  20250307_125332
  20250307_125441
  20250307_112710
  20250307_121343
  20250310_112933
  20250307_112732
  20250307_124641

i think it'd be nicer to do sth similar to what we have in goose-server routes:

let sessions = match session::list_sessions() {
Ok(sessions) => sessions,
Err(e) => {
tracing::error!("Failed to list sessions: {:?}", e);
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
};
let session_infos = sessions
.into_iter()
.map(|(id, path)| {
// Get last modified time as string
let modified = path
.metadata()
.and_then(|m| m.modified())
.map(|time| {
chrono::DateTime::<chrono::Utc>::from(time)
.format("%Y-%m-%d %H:%M:%S UTC")
.to_string()
})
.unwrap_or_else(|_| "Unknown".to_string());
// Get session description
let metadata = session::read_metadata(&path).expect("Failed to read session metadata");
SessionInfo {
id,
path: path.to_string_lossy().to_string(),
modified,
metadata,
}
})
.collect();

then we can print sth like this:

[session_name] - [description]  - [modified date]

}
}
}
Ok(())
}
59 changes: 44 additions & 15 deletions crates/goose-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use goose_cli::commands::bench::{list_suites, run_benchmark};
use goose_cli::commands::configure::handle_configure;
use goose_cli::commands::info::handle_info;
use goose_cli::commands::mcp::run_server;
use goose_cli::commands::session::handle_session_list;
use goose_cli::logging::setup_logging;
use goose_cli::session;
use goose_cli::session::build_session;
Expand Down Expand Up @@ -53,6 +54,23 @@ fn extract_identifier(identifier: Identifier) -> session::Identifier {
}
}

#[derive(Subcommand)]
enum SessionCommand {
#[command(about = "List all available sessions")]
List {
#[arg(short, long, help = "List all available sessions")]
verbose: bool,

#[arg(
short,
long,
help = "Output format (text, json)",
default_value = "text"
)]
format: String,
},
}

#[derive(Subcommand)]
enum Command {
/// Configure Goose settings
Expand All @@ -77,6 +95,8 @@ enum Command {
visible_alias = "s"
)]
Session {
#[command(subcommand)]
command: Option<SessionCommand>,
/// Identifier for the chat session
#[command(flatten)]
identifier: Option<Identifier>,
Expand Down Expand Up @@ -299,27 +319,36 @@ async fn main() -> Result<()> {
let _ = run_server(&name).await;
}
Some(Command::Session {
command,
identifier,
resume,
debug,
extension,
builtin,
}) => {
let mut session = build_session(
identifier.map(extract_identifier),
resume,
extension,
builtin,
debug,
)
.await;

setup_logging(
session.session_file().file_stem().and_then(|s| s.to_str()),
None,
)?;
let _ = session.interactive(None).await;
return Ok(());
match command {
Some(SessionCommand::List { verbose, format }) => {
handle_session_list(verbose, format)?;
return Ok(());
}
None => {
// Run session command by default
let mut session = build_session(
identifier.map(extract_identifier),
resume,
extension,
builtin,
debug,
)
.await;
setup_logging(
session.session_file().file_stem().and_then(|s| s.to_str()),
None,
)?;
let _ = session.interactive(None).await;
return Ok(());
}
}
}
Some(Command::Run {
instructions,
Expand Down
47 changes: 3 additions & 44 deletions crates/goose-server/src/routes/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,9 @@ use axum::{
};
use goose::message::Message;
use goose::session;
use goose::session::info::{get_session_info, SessionInfo};
use serde::Serialize;

#[derive(Serialize)]
struct SessionInfo {
id: String,
path: String,
modified: String,
metadata: session::SessionMetadata,
}

#[derive(Serialize)]
struct SessionListResponse {
sessions: Vec<SessionInfo>,
Expand Down Expand Up @@ -44,43 +37,9 @@ async fn list_sessions(
return Err(StatusCode::UNAUTHORIZED);
}

let sessions = match session::list_sessions() {
Ok(sessions) => sessions,
Err(e) => {
tracing::error!("Failed to list sessions: {:?}", e);
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
};

let session_infos = sessions
.into_iter()
.map(|(id, path)| {
// Get last modified time as string
let modified = path
.metadata()
.and_then(|m| m.modified())
.map(|time| {
chrono::DateTime::<chrono::Utc>::from(time)
.format("%Y-%m-%d %H:%M:%S UTC")
.to_string()
})
.unwrap_or_else(|_| "Unknown".to_string());

// Get session description
let metadata = session::read_metadata(&path).expect("Failed to read session metadata");
let sessions = get_session_info().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

SessionInfo {
id,
path: path.to_string_lossy().to_string(),
modified,
metadata,
}
})
.collect();

Ok(Json(SessionListResponse {
sessions: session_infos,
}))
Ok(Json(SessionListResponse { sessions }))
}

// Get a specific session's history
Expand Down
49 changes: 49 additions & 0 deletions crates/goose/src/session/info.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
use anyhow::Result;
use serde::Serialize;

use crate::session::{self, SessionMetadata};

#[derive(Serialize)]
pub struct SessionInfo {
pub id: String,
pub path: String,
pub modified: String,
pub metadata: SessionMetadata,
}

pub fn get_session_info() -> Result<Vec<SessionInfo>> {
let sessions = match session::list_sessions() {
Ok(sessions) => sessions,
Err(e) => {
tracing::error!("Failed to list sessions: {:?}", e);
return Err(anyhow::anyhow!("Failed to list sessions"));
}
};
let session_infos = sessions
.into_iter()
.map(|(id, path)| {
// Get last modified time as string
let modified = path
.metadata()
.and_then(|m| m.modified())
.map(|time| {
chrono::DateTime::<chrono::Utc>::from(time)
.format("%Y-%m-%d %H:%M:%S UTC")
.to_string()
})
.unwrap_or_else(|_| "Unknown".to_string());

// Get session description
let metadata = session::read_metadata(&path).expect("Failed to read session metadata");

SessionInfo {
id,
path: path.to_string_lossy().to_string(),
modified,
metadata,
}
})
.collect();

Ok(session_infos)
}
3 changes: 3 additions & 0 deletions crates/goose/src/session/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod info;
pub mod storage;

// Re-export common session types and functions
Expand All @@ -6,3 +7,5 @@ pub use storage::{
get_path, list_sessions, persist_messages, read_messages, read_metadata, update_metadata,
Identifier, SessionMetadata,
};

pub use info::{get_session_info, SessionInfo};
Loading