diff --git a/crates/goose-cli/src/cli.rs b/crates/goose-cli/src/cli.rs index 1035b4f01422..5688645e19bb 100644 --- a/crates/goose-cli/src/cli.rs +++ b/crates/goose-cli/src/cli.rs @@ -75,7 +75,7 @@ async fn get_session_id(identifier: Identifier) -> Result { sessions .into_iter() - .find(|s| s.name == name || s.id == name) + .find(|s| s.id == name || s.description.contains(&name)) .map(|s| s.id) .ok_or_else(|| anyhow::anyhow!("No session found with name '{}'", name)) } else if let Some(path) = identifier.path { diff --git a/crates/goose-cli/src/commands/schedule.rs b/crates/goose-cli/src/commands/schedule.rs index 7afa03b93bd7..c2784530c6e6 100644 --- a/crates/goose-cli/src/commands/schedule.rs +++ b/crates/goose-cli/src/commands/schedule.rs @@ -222,7 +222,7 @@ pub async fn handle_schedule_sessions(id: String, limit: Option) -> Resul " - Session ID: {}, Working Dir: {}, Description: \"{}\", Schedule ID: {:?}", session_name, // Display the session_name as Session ID metadata.working_dir.display(), - metadata.name, + metadata.description, metadata.schedule_id.as_deref().unwrap_or("N/A") ); } diff --git a/crates/goose-cli/src/commands/session.rs b/crates/goose-cli/src/commands/session.rs index 008e097a55d8..21d412ae629a 100644 --- a/crates/goose-cli/src/commands/session.rs +++ b/crates/goose-cli/src/commands/session.rs @@ -13,7 +13,7 @@ const TRUNCATED_DESC_LENGTH: usize = 60; pub async fn remove_sessions(sessions: Vec) -> Result<()> { println!("The following sessions will be removed:"); for session in &sessions { - println!("- {} {}", session.id, session.name); + println!("- {} {}", session.id, session.description); } let should_delete = confirm("Are you sure you want to delete these sessions?") @@ -45,10 +45,10 @@ fn prompt_interactive_session_removal(sessions: &[Session]) -> Result = sessions .iter() .map(|s| { - let desc = if s.name.is_empty() { - "(no name)" + let desc = if s.description.is_empty() { + "(no description)" } else { - &s.name + &s.description }; let truncated_desc = safe_truncate(desc, TRUNCATED_DESC_LENGTH); let display_text = format!("{} - {} ({})", s.updated_at, truncated_desc, s.id); @@ -154,7 +154,10 @@ pub async fn handle_session_list( println!("Available sessions:"); for session in sessions { - let output = format!("{} - {} - {}", session.id, session.name, session.updated_at); + let output = format!( + "{} - {} - {}", + session.id, session.description, session.updated_at + ); println!("{}", output); } } @@ -185,7 +188,7 @@ pub async fn handle_session_export( let conversation = session .conversation .ok_or_else(|| anyhow::anyhow!("Session has no messages"))?; - export_session_to_markdown(conversation.messages().to_vec(), &session.name) + export_session_to_markdown(conversation.messages().to_vec(), &session.description) } _ => return Err(anyhow::anyhow!("Unsupported format: {}", format)), }; @@ -290,10 +293,10 @@ pub async fn prompt_interactive_session_selection() -> Result { let display_map: std::collections::HashMap = sessions .iter() .map(|s| { - let desc = if s.name.is_empty() { - "(no name)" + let desc = if s.description.is_empty() { + "(no description)" } else { - &s.name + &s.description }; let truncated_desc = safe_truncate(desc, TRUNCATED_DESC_LENGTH); diff --git a/crates/goose-cli/src/commands/web.rs b/crates/goose-cli/src/commands/web.rs index 636b2590ebbb..7ab0be7516f0 100644 --- a/crates/goose-cli/src/commands/web.rs +++ b/crates/goose-cli/src/commands/web.rs @@ -290,7 +290,7 @@ async fn list_sessions() -> Json { session_info.push(serde_json::json!({ "name": session.id, "path": session.id, - "description": session.name, + "description": session.description, "message_count": session.message_count, "working_dir": session.working_dir })); diff --git a/crates/goose-server/src/openapi.rs b/crates/goose-server/src/openapi.rs index c56a701a2f90..73551c81271b 100644 --- a/crates/goose-server/src/openapi.rs +++ b/crates/goose-server/src/openapi.rs @@ -355,7 +355,7 @@ derive_utoipa!(Icon as IconSchema); super::routes::session::list_sessions, super::routes::session::get_session, super::routes::session::get_session_insights, - super::routes::session::update_session_name, + super::routes::session::update_session_description, super::routes::session::delete_session, super::routes::session::export_session, super::routes::session::import_session, @@ -398,7 +398,7 @@ derive_utoipa!(Icon as IconSchema); super::routes::context::ContextManageResponse, super::routes::session::ImportSessionRequest, super::routes::session::SessionListResponse, - super::routes::session::UpdateSessionNameRequest, + super::routes::session::UpdateSessionDescriptionRequest, super::routes::session::UpdateSessionUserRecipeValuesRequest, Message, MessageContent, diff --git a/crates/goose-server/src/routes/agent.rs b/crates/goose-server/src/routes/agent.rs index 7f9e7a9c5b3b..d488167ab50b 100644 --- a/crates/goose-server/src/routes/agent.rs +++ b/crates/goose-server/src/routes/agent.rs @@ -139,9 +139,9 @@ async fn start_agent( } let counter = state.session_counter.fetch_add(1, Ordering::SeqCst) + 1; - let name = format!("New session {}", counter); + let description = format!("New session {}", counter); - let mut session = SessionManager::create_session(PathBuf::from(&working_dir), name) + let mut session = SessionManager::create_session(PathBuf::from(&working_dir), description) .await .map_err(|err| { error!("Failed to create session: {}", err); diff --git a/crates/goose-server/src/routes/schedule.rs b/crates/goose-server/src/routes/schedule.rs index 518252dc679e..a9ce8f399bec 100644 --- a/crates/goose-server/src/routes/schedule.rs +++ b/crates/goose-server/src/routes/schedule.rs @@ -68,10 +68,10 @@ fn default_limit() -> u32 { #[derive(Serialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] pub struct SessionDisplayInfo { - id: String, - name: String, - created_at: String, - working_dir: String, + id: String, // Derived from session_name (filename) + name: String, // From metadata.description + created_at: String, // Derived from session_name, in ISO 8601 format + working_dir: String, // from metadata.working_dir (as String) schedule_id: Option, message_count: usize, total_tokens: Option, @@ -325,7 +325,7 @@ async fn sessions_handler( for (session_name, session) in session_tuples { display_infos.push(SessionDisplayInfo { id: session_name.clone(), - name: session.name, + name: session.description, created_at: parse_session_name_to_iso(&session_name), working_dir: session.working_dir.to_string_lossy().into_owned(), schedule_id: session.schedule_id, diff --git a/crates/goose-server/src/routes/session.rs b/crates/goose-server/src/routes/session.rs index d32a9e88fef5..3bea6a1251f0 100644 --- a/crates/goose-server/src/routes/session.rs +++ b/crates/goose-server/src/routes/session.rs @@ -22,9 +22,9 @@ pub struct SessionListResponse { #[derive(Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] -pub struct UpdateSessionNameRequest { - /// Updated name for the session (max 200 characters) - name: String, +pub struct UpdateSessionDescriptionRequest { + /// Updated description (name) for the session (max 200 characters) + description: String, } #[derive(Deserialize, ToSchema)] @@ -40,7 +40,7 @@ pub struct ImportSessionRequest { json: String, } -const MAX_NAME_LENGTH: usize = 200; +const MAX_DESCRIPTION_LENGTH: usize = 200; #[utoipa::path( get, @@ -109,14 +109,14 @@ async fn get_session_insights() -> Result, StatusCode> { #[utoipa::path( put, - path = "/sessions/{session_id}/name", - request_body = UpdateSessionNameRequest, + path = "/sessions/{session_id}/description", + request_body = UpdateSessionDescriptionRequest, params( ("session_id" = String, Path, description = "Unique identifier for the session") ), responses( - (status = 200, description = "Session name updated successfully"), - (status = 400, description = "Bad request - Name too long (max 200 characters)"), + (status = 200, description = "Session description updated successfully"), + (status = 400, description = "Bad request - Description too long (max 200 characters)"), (status = 401, description = "Unauthorized - Invalid or missing API key"), (status = 404, description = "Session not found"), (status = 500, description = "Internal server error") @@ -126,20 +126,16 @@ async fn get_session_insights() -> Result, StatusCode> { ), tag = "Session Management" )] -async fn update_session_name( +async fn update_session_description( Path(session_id): Path, - Json(request): Json, + Json(request): Json, ) -> Result { - let name = request.name.trim(); - if name.is_empty() { - return Err(StatusCode::BAD_REQUEST); - } - if name.len() > MAX_NAME_LENGTH { + if request.description.len() > MAX_DESCRIPTION_LENGTH { return Err(StatusCode::BAD_REQUEST); } SessionManager::update_session(&session_id) - .user_provided_name(name.to_string()) + .description(request.description) .apply() .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; @@ -268,7 +264,10 @@ pub fn routes(state: Arc) -> Router { .route("/sessions/{session_id}/export", get(export_session)) .route("/sessions/import", post(import_session)) .route("/sessions/insights", get(get_session_insights)) - .route("/sessions/{session_id}/name", put(update_session_name)) + .route( + "/sessions/{session_id}/description", + put(update_session_description), + ) .route( "/sessions/{session_id}/user_recipe_values", put(update_session_user_recipe_values), diff --git a/crates/goose/src/agents/agent.rs b/crates/goose/src/agents/agent.rs index a9dcc2cb0657..b2ade9ea89fb 100644 --- a/crates/goose/src/agents/agent.rs +++ b/crates/goose/src/agents/agent.rs @@ -1042,7 +1042,9 @@ impl Agent { let provider = self.provider().await?; let session_id = session_config.id.clone(); tokio::spawn(async move { - if let Err(e) = SessionManager::maybe_update_name(&session_id, provider).await { + if let Err(e) = + SessionManager::maybe_update_description(&session_id, provider).await + { warn!("Failed to generate session description: {}", e); } }); diff --git a/crates/goose/src/session/session_manager.rs b/crates/goose/src/session/session_manager.rs index bec7c1607228..0002d4199a1c 100644 --- a/crates/goose/src/session/session_manager.rs +++ b/crates/goose/src/session/session_manager.rs @@ -18,7 +18,7 @@ use tokio::sync::OnceCell; use tracing::{info, warn}; use utoipa::ToSchema; -const CURRENT_SCHEMA_VERSION: i32 = 4; +const CURRENT_SCHEMA_VERSION: i32 = 3; static SESSION_STORAGE: OnceCell> = OnceCell::const_new(); @@ -27,11 +27,7 @@ pub struct Session { pub id: String, #[schema(value_type = String)] pub working_dir: PathBuf, - // Allow importing session exports from before 'description' was renamed to 'name' - #[serde(alias = "description")] - pub name: String, - #[serde(default)] - pub user_set_name: bool, + pub description: String, pub created_at: DateTime, pub updated_at: DateTime, pub extension_data: ExtensionData, @@ -50,8 +46,7 @@ pub struct Session { pub struct SessionUpdateBuilder { session_id: String, - name: Option, - user_set_name: Option, + description: Option, working_dir: Option, extension_data: Option, total_tokens: Option>, @@ -78,8 +73,7 @@ impl SessionUpdateBuilder { fn new(session_id: String) -> Self { Self { session_id, - name: None, - user_set_name: None, + description: None, working_dir: None, extension_data: None, total_tokens: None, @@ -94,21 +88,8 @@ impl SessionUpdateBuilder { } } - pub fn user_provided_name(mut self, name: impl Into) -> Self { - let name = name.into().trim().to_string(); - if !name.is_empty() { - self.name = Some(name); - self.user_set_name = Some(true); - } - self - } - - pub fn system_generated_name(mut self, name: impl Into) -> Self { - let name = name.into().trim().to_string(); - if !name.is_empty() { - self.name = Some(name); - self.user_set_name = Some(false); - } + pub fn description(mut self, description: impl Into) -> Self { + self.description = Some(description.into()); self } @@ -185,10 +166,10 @@ impl SessionManager { .map(Arc::clone) } - pub async fn create_session(working_dir: PathBuf, name: String) -> Result { + pub async fn create_session(working_dir: PathBuf, description: String) -> Result { Self::instance() .await? - .create_session(working_dir, name) + .create_session(working_dir, description) .await } @@ -238,13 +219,8 @@ impl SessionManager { Self::instance().await?.import_session(json).await } - pub async fn maybe_update_name(id: &str, provider: Arc) -> Result<()> { + pub async fn maybe_update_description(id: &str, provider: Arc) -> Result<()> { let session = Self::get_session(id, true).await?; - - if session.user_set_name { - return Ok(()); - } - let conversation = session .conversation .ok_or_else(|| anyhow::anyhow!("No messages found"))?; @@ -256,9 +232,9 @@ impl SessionManager { .count(); if user_message_count <= MSG_COUNT_FOR_SESSION_NAME_GENERATION { - let name = provider.generate_session_name(&conversation).await?; + let description = provider.generate_session_name(&conversation).await?; Self::update_session(id) - .system_generated_name(name) + .description(description) .apply() .await } else { @@ -293,8 +269,7 @@ impl Default for Session { Self { id: String::new(), working_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")), - name: String::new(), - user_set_name: false, + description: String::new(), created_at: Default::default(), updated_at: Default::default(), extension_data: ExtensionData::default(), @@ -331,17 +306,10 @@ impl sqlx::FromRow<'_, sqlx::sqlite::SqliteRow> for Session { let user_recipe_values = user_recipe_values_json.and_then(|json| serde_json::from_str(&json).ok()); - let name: String = row - .try_get("name") - .or_else(|_| row.try_get("description"))?; - - let user_set_name = row.try_get("user_set_name").unwrap_or(false); - Ok(Session { id: row.try_get("id")?, working_dir: PathBuf::from(row.try_get::("working_dir")?), - name, - user_set_name, + description: row.try_get("description")?, created_at: row.try_get("created_at")?, updated_at: row.try_get("updated_at")?, extension_data: serde_json::from_str(&row.try_get::("extension_data")?) @@ -428,8 +396,7 @@ impl SessionStorage { r#" CREATE TABLE sessions ( id TEXT PRIMARY KEY, - name TEXT NOT NULL DEFAULT '', - user_set_name BOOLEAN DEFAULT FALSE, + description TEXT NOT NULL DEFAULT '', working_dir TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, @@ -537,16 +504,15 @@ impl SessionStorage { sqlx::query( r#" INSERT INTO sessions ( - id, name, user_set_name, working_dir, created_at, updated_at, extension_data, + id, description, working_dir, created_at, updated_at, extension_data, total_tokens, input_tokens, output_tokens, accumulated_total_tokens, accumulated_input_tokens, accumulated_output_tokens, schedule_id, recipe_json, user_recipe_values_json - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "#, ) .bind(&session.id) - .bind(&session.name) - .bind(session.user_set_name) + .bind(&session.description) .bind(session.working_dir.to_string_lossy().as_ref()) .bind(session.created_at) .bind(session.updated_at) @@ -654,23 +620,6 @@ impl SessionStorage { .execute(&self.pool) .await?; } - 4 => { - sqlx::query( - r#" - ALTER TABLE sessions RENAME COLUMN description TO name - "#, - ) - .execute(&self.pool) - .await?; - - sqlx::query( - r#" - ALTER TABLE sessions ADD COLUMN user_set_name BOOLEAN DEFAULT FALSE - "#, - ) - .execute(&self.pool) - .await?; - } _ => { anyhow::bail!("Unknown migration version: {}", version); } @@ -679,11 +628,11 @@ impl SessionStorage { Ok(()) } - async fn create_session(&self, working_dir: PathBuf, name: String) -> Result { + async fn create_session(&self, working_dir: PathBuf, description: String) -> Result { let today = chrono::Utc::now().format("%Y%m%d").to_string(); let session_id = sqlx::query_as( r#" - INSERT INTO sessions (id, name, user_set_name, working_dir, extension_data) + INSERT INTO sessions (id, description, working_dir, extension_data) VALUES ( ? || '_' || CAST(COALESCE(( SELECT MAX(CAST(SUBSTR(id, 10) AS INTEGER)) @@ -691,7 +640,6 @@ impl SessionStorage { WHERE id LIKE ? || '_%' ), 0) + 1 AS TEXT), ?, - FALSE, ?, '{}' ) @@ -700,7 +648,7 @@ impl SessionStorage { ) .bind(&today) .bind(&today) - .bind(&name) + .bind(&description) .bind(working_dir.to_string_lossy().as_ref()) .fetch_one(&self.pool) .await?; @@ -715,7 +663,7 @@ impl SessionStorage { async fn get_session(&self, id: &str, include_messages: bool) -> Result { let mut session = sqlx::query_as::<_, Session>( r#" - SELECT id, working_dir, name, user_set_name, created_at, updated_at, extension_data, + SELECT id, working_dir, description, created_at, updated_at, extension_data, total_tokens, input_tokens, output_tokens, accumulated_total_tokens, accumulated_input_tokens, accumulated_output_tokens, schedule_id, recipe_json, user_recipe_values_json @@ -761,8 +709,7 @@ impl SessionStorage { }; } - add_update!(builder.name, "name"); - add_update!(builder.user_set_name, "user_set_name"); + add_update!(builder.description, "description"); add_update!(builder.working_dir, "working_dir"); add_update!(builder.extension_data, "extension_data"); add_update!(builder.total_tokens, "total_tokens"); @@ -782,16 +729,15 @@ impl SessionStorage { return Ok(()); } - query.push_str(", "); + if !updates.is_empty() { + query.push_str(", "); + } query.push_str("updated_at = datetime('now') WHERE id = ?"); let mut q = sqlx::query(&query); - if let Some(name) = builder.name { - q = q.bind(name); - } - if let Some(user_set_name) = builder.user_set_name { - q = q.bind(user_set_name); + if let Some(desc) = builder.description { + q = q.bind(desc); } if let Some(wd) = builder.working_dir { q = q.bind(wd.to_string_lossy().to_string()); @@ -928,7 +874,7 @@ impl SessionStorage { async fn list_sessions(&self) -> Result> { sqlx::query_as::<_, Session>( r#" - SELECT s.id, s.working_dir, s.name, s.user_set_name, s.created_at, s.updated_at, s.extension_data, + SELECT s.id, s.working_dir, s.description, s.created_at, s.updated_at, s.extension_data, s.total_tokens, s.input_tokens, s.output_tokens, s.accumulated_total_tokens, s.accumulated_input_tokens, s.accumulated_output_tokens, s.schedule_id, s.recipe_json, s.user_recipe_values_json, @@ -994,26 +940,23 @@ impl SessionStorage { let import: Session = serde_json::from_str(json)?; let session = self - .create_session(import.working_dir.clone(), import.name.clone()) + .create_session(import.working_dir.clone(), import.description.clone()) .await?; - let mut builder = SessionUpdateBuilder::new(session.id.clone()) - .extension_data(import.extension_data) - .total_tokens(import.total_tokens) - .input_tokens(import.input_tokens) - .output_tokens(import.output_tokens) - .accumulated_total_tokens(import.accumulated_total_tokens) - .accumulated_input_tokens(import.accumulated_input_tokens) - .accumulated_output_tokens(import.accumulated_output_tokens) - .schedule_id(import.schedule_id) - .recipe(import.recipe) - .user_recipe_values(import.user_recipe_values); - - if import.user_set_name { - builder = builder.user_provided_name(import.name.clone()); - } - - self.apply_update(builder).await?; + self.apply_update( + SessionUpdateBuilder::new(session.id.clone()) + .extension_data(import.extension_data) + .total_tokens(import.total_tokens) + .input_tokens(import.input_tokens) + .output_tokens(import.output_tokens) + .accumulated_total_tokens(import.accumulated_total_tokens) + .accumulated_input_tokens(import.accumulated_input_tokens) + .accumulated_output_tokens(import.accumulated_output_tokens) + .schedule_id(import.schedule_id) + .recipe(import.recipe) + .user_recipe_values(import.user_recipe_values), + ) + .await?; if let Some(conversation) = import.conversation { self.replace_conversation(&session.id, &conversation) @@ -1083,7 +1026,7 @@ mod tests { session_storage .apply_update( SessionUpdateBuilder::new(session.id.clone()) - .user_provided_name(format!("Updated session {}", i)) + .description(format!("Updated session {}", i)) .total_tokens(Some(100 * i)), ) .await @@ -1116,7 +1059,7 @@ mod tests { for session in &sessions { assert_eq!(session.message_count, 2); - assert!(session.name.starts_with("Updated session")); + assert!(session.description.starts_with("Updated session")); } let insights = storage.get_insights().await.unwrap(); @@ -1187,7 +1130,7 @@ mod tests { let imported = storage.import_session(&exported).await.unwrap(); assert_ne!(imported.id, original.id); - assert_eq!(imported.name, DESCRIPTION); + assert_eq!(imported.description, DESCRIPTION); assert_eq!(imported.working_dir, PathBuf::from("/tmp/test")); assert_eq!(imported.total_tokens, Some(TOTAL_TOKENS)); assert_eq!(imported.input_tokens, Some(INPUT_TOKENS)); @@ -1200,28 +1143,4 @@ mod tests { assert_eq!(conversation.messages()[0].role, Role::User); assert_eq!(conversation.messages()[1].role, Role::Assistant); } - - #[tokio::test] - async fn test_import_session_with_description_field() { - const OLD_FORMAT_JSON: &str = r#"{ - "id": "20240101_1", - "description": "Old format session", - "user_set_name": true, - "working_dir": "/tmp/test", - "created_at": "2024-01-01T00:00:00Z", - "updated_at": "2024-01-01T00:00:00Z", - "extension_data": {}, - "message_count": 0 - }"#; - - let temp_dir = TempDir::new().unwrap(); - let db_path = temp_dir.path().join("test_import.db"); - let storage = Arc::new(SessionStorage::create(&db_path).await.unwrap()); - - let imported = storage.import_session(OLD_FORMAT_JSON).await.unwrap(); - - assert_eq!(imported.name, "Old format session"); - assert!(imported.user_set_name); - assert_eq!(imported.working_dir, PathBuf::from("/tmp/test")); - } } diff --git a/crates/goose/tests/test_support.rs b/crates/goose/tests/test_support.rs index 6bba89e9198b..d156b8459f29 100644 --- a/crates/goose/tests/test_support.rs +++ b/crates/goose/tests/test_support.rs @@ -381,8 +381,7 @@ pub fn create_test_session_metadata(message_count: usize, working_dir: &str) -> Session { id: "".to_string(), working_dir: PathBuf::from(working_dir), - name: "Test session".to_string(), - user_set_name: false, + description: "Test session".to_string(), created_at: Default::default(), schedule_id: Some("test_job".to_string()), recipe: None, diff --git a/scripts/goose-db-helper.sh b/scripts/goose-db-helper.sh deleted file mode 100755 index 96d3cf824092..000000000000 --- a/scripts/goose-db-helper.sh +++ /dev/null @@ -1,876 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -BACKUP_DIR="${HOME}/.local/share/goose/goose-db-backups" - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -CYAN='\033[0;36m' -NC='\033[0m' - -DRY_RUN=false -SKIP_CONFIRM=false -CLEAN_GENERATE=false - -MIGRATIONS_DIR="${HOME}/.local/share/goose/migrations" -RUST_SESSION_MANAGER="crates/goose/src/session/session_manager.rs" - -get_latest_version() { - if [[ ! -d "${MIGRATIONS_DIR}" ]]; then - echo "0" - return - fi - - local latest=$(find "${MIGRATIONS_DIR}" -mindepth 1 -maxdepth 1 -type d -name "[0-9]*" 2>/dev/null | \ - sed 's/.*\/\([0-9]*\).*/\1/' | \ - sed 's/^0*//' | \ - sort -n | \ - tail -1) - - echo "${latest:-0}" -} - -find_migration_dir() { - local version=$1 - - if [[ ! -d "${MIGRATIONS_DIR}" ]]; then - return - fi - - local version_num=$(echo "${version}" | sed 's/^0*//') - - for dir in "${MIGRATIONS_DIR}"/*; do - if [[ -d "${dir}" ]]; then - local dir_version=$(basename "${dir}" | sed 's/^\([0-9]*\).*/\1/' | sed 's/^0*//') - if [[ "${dir_version}" == "${version_num}" ]]; then - echo "${dir}" - return - fi - fi - done -} - -get_migration_info() { - local version=$1 - - if [[ "${version}" == "0" ]]; then - echo "Initial schema (no schema_version table)" - return - fi - - local migration_dir=$(find_migration_dir "${version}") - if [[ -z "${migration_dir}" ]]; then - echo "Unknown migration" - return - fi - - local metadata_file="${migration_dir}/metadata.txt" - if [[ -f "${metadata_file}" ]]; then - local description=$(grep "^DESCRIPTION=" "${metadata_file}" | cut -d= -f2-) - echo "${description}" - else - echo "Migration ${version}" - fi -} - -list_available_migrations() { - echo -e "${BLUE}=== Available Migrations ===${NC}" - echo "" - echo -e "${CYAN}Version 0:${NC} Initial schema (no schema_version table)" - echo "" - - if [[ ! -d "${MIGRATIONS_DIR}" ]]; then - echo -e "${YELLOW}No migration files found in ${MIGRATIONS_DIR}${NC}" - return - fi - - for migration_dir in $(find "${MIGRATIONS_DIR}" -mindepth 1 -maxdepth 1 -type d -name "[0-9]*" | sort -V); do - local version=$(basename "${migration_dir}" | sed 's/^\([0-9]*\).*/\1/') - local info=$(get_migration_info "${version}") - - echo -e "${CYAN}Version ${version}:${NC} ${info}" - echo "" - done -} - -get_goose_db_path() { - if [[ -n "${GOOSE_PATH_ROOT:-}" ]]; then - echo "${GOOSE_PATH_ROOT}/data/sessions/sessions.db" - else - local possible_paths=( - "${HOME}/.local/share/goose/sessions/sessions.db" - "${HOME}/Library/Application Support/Block/goose/data/sessions/sessions.db" - ) - - for path in "${possible_paths[@]}"; do - if [[ -f "${path}" ]]; then - echo "${path}" - return - fi - done - - echo "${possible_paths[0]}" - fi -} - -DB_PATH=$(get_goose_db_path) - -confirm_action() { - local action="$1" - - if [[ "${SKIP_CONFIRM}" == "true" ]]; then - return 0 - fi - - echo -e "${YELLOW}You are about to: ${action}${NC}" - read -p "Continue? (y/N) " -n 1 -r - echo - if [[ $REPLY =~ ^[Yy]$ ]]; then - return 0 - else - return 1 - fi -} - -check_db_exists() { - if [[ ! -f "${DB_PATH}" ]]; then - echo -e "${RED}ERROR: Database not found at ${DB_PATH}${NC}" >&2 - exit 1 - fi -} - -get_schema_version() { - check_db_exists - local version=$(sqlite3 "${DB_PATH}" "SELECT MAX(version) FROM schema_version;" 2>/dev/null || echo "0") - echo "${version}" -} - -check_column_exists() { - local table=$1 - local column=$2 - check_db_exists - sqlite3 "${DB_PATH}" "PRAGMA table_info(${table});" | grep -q "^[0-9]*|${column}|" -} - -get_table_schema() { - local table=$1 - check_db_exists - sqlite3 "${DB_PATH}" "PRAGMA table_info(${table});" 2>/dev/null || echo "" -} - -create_backup() { - check_db_exists - mkdir -p "${BACKUP_DIR}" - local timestamp=$(date +%Y%m%d_%H%M%S) - local backup_path="${BACKUP_DIR}/sessions_v$(get_schema_version)_${timestamp}.db" - cp "${DB_PATH}" "${backup_path}" - echo -e "${GREEN}✓ Backup created: ${backup_path}${NC}" - echo "${backup_path}" -} - -show_version_history() { - list_available_migrations -} - -show_status() { - echo -e "${BLUE}=== Goose Database Status ===${NC}" - echo "Database path: ${DB_PATH}" - echo "" - - if [[ ! -f "${DB_PATH}" ]]; then - echo -e "${YELLOW}Status: No database found${NC}" - echo "" - echo "This is normal if you haven't run Goose yet." - echo "Once you run Goose, a database will be created automatically." - return - fi - - local version=$(get_schema_version) - local version_info=$(get_migration_info "${version}") - local latest_version=$(get_latest_version) - - echo -e "Current schema version: ${CYAN}${version}${NC}" - echo -e "Version info: ${version_info}" - echo "" - - echo -e "${BLUE}Sessions table schema:${NC}" - get_table_schema "sessions" | while IFS='|' read -r cid name type notnull dflt_value pk; do - echo " - ${name} (${type})" - done - echo "" - - local session_count=$(sqlite3 "${DB_PATH}" "SELECT COUNT(*) FROM sessions;" 2>/dev/null || echo "0") - local message_count=$(sqlite3 "${DB_PATH}" "SELECT COUNT(*) FROM messages;" 2>/dev/null || echo "0") - echo -e "${BLUE}Database contents:${NC}" - echo " Sessions: ${session_count}" - echo " Messages: ${message_count}" - echo "" - - if [[ ${version} -eq ${latest_version} ]]; then - echo -e "${GREEN}✓ Database is at the latest schema version${NC}" - elif [[ ${version} -lt ${latest_version} ]]; then - echo -e "${YELLOW}⚠ Database can be upgraded to v${latest_version}${NC}" - echo " Run: $0 migrate-to ${latest_version}" - fi -} - -apply_migration() { - local target_version=$1 - - if [[ "${target_version}" == "0" ]]; then - echo -e "${RED}ERROR: Cannot migrate forward to version 0${NC}" >&2 - return 1 - fi - - local migration_dir=$(find_migration_dir "${target_version}") - if [[ -z "${migration_dir}" ]]; then - echo -e "${RED}ERROR: Migration files not found for version ${target_version}${NC}" >&2 - echo -e "${YELLOW}Expected to find directory: ${MIGRATIONS_DIR}/${target_version}_*${NC}" - return 1 - fi - - local up_sql="${migration_dir}/up.sql" - if [[ ! -f "${up_sql}" ]]; then - echo -e "${RED}ERROR: Migration file not found: ${up_sql}${NC}" >&2 - return 1 - fi - - if ! sqlite3 "${DB_PATH}" < "${up_sql}"; then - echo -e "${RED}ERROR: Migration to v${target_version} failed${NC}" >&2 - echo -e "${YELLOW}Check the SQL file: ${up_sql}${NC}" - return 1 - fi -} - -rollback_migration() { - local from_version=$1 - - if [[ "${from_version}" == "0" ]]; then - echo -e "${RED}ERROR: Cannot rollback from version 0${NC}" >&2 - return 1 - fi - - local migration_dir=$(find_migration_dir "${from_version}") - if [[ -z "${migration_dir}" ]]; then - echo -e "${RED}ERROR: Migration files not found for version ${from_version}${NC}" >&2 - echo -e "${YELLOW}Expected to find directory: ${MIGRATIONS_DIR}/${from_version}_*${NC}" - return 1 - fi - - local down_sql="${migration_dir}/down.sql" - if [[ ! -f "${down_sql}" ]]; then - echo -e "${RED}ERROR: Rollback file not found: ${down_sql}${NC}" >&2 - return 1 - fi - - if ! sqlite3 "${DB_PATH}" < "${down_sql}"; then - echo -e "${RED}ERROR: Rollback from v${from_version} failed${NC}" >&2 - echo -e "${YELLOW}Check the SQL file: ${down_sql}${NC}" - return 1 - fi -} - -migrate_to_version() { - local target_version=$1 - local latest_version=$(get_latest_version) - - if [[ -z "${target_version}" ]]; then - echo -e "${RED}ERROR: Please specify a target version${NC}" >&2 - echo "Usage: $0 migrate-to " - echo "" - echo "Available versions: 0 to ${latest_version}" - return 1 - fi - - if [[ ! "${target_version}" =~ ^[0-9]+$ ]] || [[ ${target_version} -lt 0 ]] || [[ ${target_version} -gt ${latest_version} ]]; then - echo -e "${RED}ERROR: Invalid version: ${target_version}${NC}" >&2 - echo "Valid versions are: 0 to ${latest_version}" - return 1 - fi - - check_db_exists - local current_version=$(get_schema_version) - - if [[ ${current_version} -eq ${target_version} ]]; then - echo -e "${YELLOW}Already at version ${target_version}${NC}" - return 0 - fi - - echo -e "${BLUE}=== Migrating database from v${current_version} to v${target_version} ===${NC}" - echo "" - - if [[ "${DRY_RUN}" == "true" ]]; then - echo -e "${CYAN}[DRY RUN] Would perform the following actions:${NC}" - echo "" - echo "1. Create backup at: ${BACKUP_DIR}/sessions_v${current_version}_.db" - echo "" - - if [[ ${target_version} -gt ${current_version} ]]; then - echo "2. Apply forward migrations:" - for version in $(seq $((current_version + 1)) ${target_version}); do - local migration_info=$(get_migration_info "${version}") - local migration_dir=$(find_migration_dir "${version}") - echo " - Migrate to v${version}: ${migration_info}" - echo " SQL file: ${migration_dir}/up.sql" - done - else - echo "2. Apply rollback migrations:" - for version in $(seq ${current_version} -1 $((target_version + 1))); do - local migration_info=$(get_migration_info "${version}") - local migration_dir=$(find_migration_dir "${version}") - echo " - Rollback from v${version}: ${migration_info}" - echo " SQL file: ${migration_dir}/down.sql" - done - fi - - echo "" - echo "3. Update schema_version table to ${target_version}" - echo "" - echo -e "${CYAN}[DRY RUN] No changes were made${NC}" - return 0 - fi - - if ! confirm_action "migrate database from v${current_version} to v${target_version}"; then - echo -e "${YELLOW}Migration cancelled${NC}" - return 2 - fi - - local backup_path=$(create_backup) - echo "" - - if [[ ${target_version} -gt ${current_version} ]]; then - for version in $(seq $((current_version + 1)) ${target_version}); do - local migration_info=$(get_migration_info "${version}") - echo -e "Applying migration to v${version}..." - apply_migration ${version} - echo -e "${GREEN}✓ Migrated to v${version}: ${migration_info}${NC}" - done - else - for version in $(seq ${current_version} -1 $((target_version + 1))); do - local migration_info=$(get_migration_info "${version}") - echo -e "Rolling back from v${version}..." - rollback_migration ${version} - echo -e "${GREEN}✓ Rolled back from v${version}${NC}" - done - fi - - echo "" - echo -e "${GREEN}✓ Migration complete!${NC}" - echo -e "Database is now at version ${target_version}" - echo "" - echo "Backup saved at: ${backup_path}" -} - -list_backups() { - if [[ ! -d "${BACKUP_DIR}" ]] || [[ -z "$(ls -A "${BACKUP_DIR}" 2>/dev/null)" ]]; then - echo -e "${YELLOW}No backups found${NC}" - return - fi - - echo -e "${BLUE}=== Available Backups ===${NC}" - echo "" - ls -lh "${BACKUP_DIR}" | tail -n +2 | while read -r line; do - local filename=$(echo "${line}" | awk '{print $NF}') - local size=$(echo "${line}" | awk '{print $5}') - local date=$(echo "${line}" | awk '{print $6, $7, $8}') - - if [[ "${filename}" =~ _v([0-9]+)_ ]]; then - local version="${BASH_REMATCH[1]}" - echo -e "${filename}" - echo -e " Size: ${size}, Date: ${date}, Schema: v${version}" - echo "" - else - echo -e "${filename}" - echo -e " Size: ${size}, Date: ${date}" - echo "" - fi - done -} - -restore_backup() { - local backup_file=$1 - - if [[ -z "${backup_file}" ]]; then - echo -e "${RED}ERROR: Please specify a backup file to restore${NC}" >&2 - echo "Usage: $0 restore " - echo "" - list_backups - exit 1 - fi - - if [[ ! -f "${backup_file}" ]]; then - echo -e "${RED}ERROR: Backup file not found: ${backup_file}${NC}" >&2 - exit 1 - fi - - check_db_exists - - if [[ "${DRY_RUN}" == "true" ]]; then - echo -e "${CYAN}[DRY RUN] Would perform the following actions:${NC}" - echo "" - echo "1. Create backup of current database at: ${BACKUP_DIR}/sessions_v_.db" - echo "2. Restore backup from: ${backup_file}" - echo "3. Replace current database at: ${DB_PATH}" - echo "" - echo -e "${CYAN}[DRY RUN] No changes were made${NC}" - return 0 - fi - - if ! confirm_action "restore backup from ${backup_file} (this will replace your current database)"; then - echo -e "${YELLOW}Restore cancelled${NC}" - return 2 - fi - - local current_backup=$(create_backup) - echo "" - - cp "${backup_file}" "${DB_PATH}" - echo -e "${GREEN}✓ Restored backup from: ${backup_file}${NC}" - echo "Current database backed up to: ${current_backup}" -} - -validate_sql_syntax() { - local sql=$1 - local file_desc=$2 - - if [[ -z "$sql" ]]; then - echo -e "${YELLOW}⚠ WARNING: Empty SQL in $file_desc${NC}" >&2 - return 1 - fi - - if ! echo "$sql" | grep -q ";"; then - echo -e "${YELLOW}⚠ WARNING: No semicolons found in $file_desc${NC}" >&2 - return 1 - fi - - local lines=$(echo "$sql" | grep -v "^--" | grep -v "^BEGIN" | grep -v "^COMMIT" | grep -v "^$") - while IFS= read -r line; do - if [[ -n "$line" ]]; then - if ! echo "$line" | grep -q ";$"; then - local next_line=$(echo "$lines" | grep -A1 "^$line$" | tail -1) - if [[ -n "$next_line" && ! "$next_line" =~ ^(BEGIN|COMMIT|INSERT|DELETE|$) ]]; then - echo -e "${YELLOW}⚠ WARNING: Possible missing semicolon in $file_desc:${NC}" >&2 - echo " $line" >&2 - return 1 - fi - fi - fi - done <<< "$lines" - - return 0 -} - -extract_migration_sql() { - local version=$1 - local rust_file=$2 - - awk -v ver="$version" ' - BEGIN { in_migration=0; sql=""; query_count=0; current_query="" } - /async fn apply_migration/ { found_func=1 } - found_func && $0 ~ ver " =>" { in_migration=1; next } - in_migration && /}$/ && !/=>/ { exit } - in_migration && /sqlx::query/ { - getline - if ($0 ~ /r#"/) { - if (current_query != "") { - if (sql != "") sql = sql ";\n" - sql = sql current_query - current_query = "" - } - getline - while ($0 !~ /"#/) { - if (current_query != "") current_query = current_query "\n" - current_query = current_query $0 - getline - } - query_count++ - } - } - END { - if (current_query != "") { - if (sql != "") sql = sql ";\n" - sql = sql current_query - } - if (sql != "") print sql ";" - } - ' "$rust_file" -} - -generate_rollback_sql() { - local version=$1 - local up_sql=$2 - - echo "BEGIN TRANSACTION;" - echo "" - - local statements=() - mapfile -d $'\0' -t statements < <(echo "$up_sql" | awk 'BEGIN{RS=";"} {gsub(/^[ \t\n]+|[ \t\n]+$/, ""); if (length($0) > 0) {print $0; printf "%c", 0}}') - - local rollback_stmts=() - local has_unsupported=false - - for stmt in "${statements[@]}"; do - if echo "$stmt" | grep -q "CREATE TABLE.*schema_version"; then - rollback_stmts+=("DROP TABLE IF EXISTS schema_version;") - elif echo "$stmt" | grep -q "RENAME COLUMN"; then - local table=$(echo "$stmt" | sed -n 's/.*ALTER TABLE \([^ ]*\).*/\1/p') - local old_col=$(echo "$stmt" | sed -n 's/.*RENAME COLUMN \([^ ]*\) TO.*/\1/p') - local new_col=$(echo "$stmt" | sed -n 's/.*TO \([^ ;]*\).*/\1/p') - rollback_stmts+=("ALTER TABLE $table RENAME COLUMN $new_col TO $old_col;") - elif echo "$stmt" | grep -q "ADD COLUMN"; then - local table=$(echo "$stmt" | sed -n 's/.*ALTER TABLE \([^ ]*\).*/\1/p') - local column=$(echo "$stmt" | sed -n 's/.*ADD COLUMN \([^ ]*\).*/\1/p') - rollback_stmts+=("ALTER TABLE $table DROP COLUMN $column;") - else - rollback_stmts+=("-- TODO: Unable to auto-generate rollback for: $stmt") - has_unsupported=true - fi - done - - for ((i=${#rollback_stmts[@]}-1; i>=0; i--)); do - echo "${rollback_stmts[$i]}" - done - - echo "" - echo "DELETE FROM schema_version WHERE version = $version;" - echo "" - echo "COMMIT;" - - if [[ "$has_unsupported" == "true" ]]; then - return 1 - fi -} - -generate_metadata() { - local version=$1 - local sql=$2 - local author=${USER:-system} - local date=$(date +%Y-%m-%d) - - local description="Migration $version" - if echo "$sql" | grep -q "CREATE TABLE.*schema_version"; then - description="Added schema_version tracking" - elif echo "$sql" | grep -q "ALTER TABLE.*ADD COLUMN"; then - local column=$(echo "$sql" | sed -n 's/.*ADD COLUMN \([^ ]*\).*/\1/p') - description="Added $column column" - elif echo "$sql" | grep -q "RENAME COLUMN"; then - local old_col=$(echo "$sql" | sed -n 's/.*RENAME COLUMN \([^ ]*\) TO.*/\1/p') - local new_col=$(echo "$sql" | sed -n 's/.*TO \([^ ]*\).*/\1/p') - description="Renamed $old_col to $new_col" - fi - - cat <&2 - echo "Make sure you're running this from the goose repository root." - exit 1 - fi - - echo -e "${BLUE}=== Generating Migrations from Rust Source ===${NC}" - echo "" - echo "Reading migrations from: ${RUST_SESSION_MANAGER}" - echo "Output directory: ${MIGRATIONS_DIR}" - echo "" - - if [[ "${CLEAN_GENERATE}" == "true" ]]; then - if [[ -d "${MIGRATIONS_DIR}" ]]; then - local migration_count=$(find "${MIGRATIONS_DIR}" -mindepth 1 -maxdepth 1 -type d -name "[0-9]*" 2>/dev/null | wc -l) - if [[ ${migration_count} -gt 0 ]]; then - echo -e "${YELLOW}⚠ Clean mode: This will remove all ${migration_count} existing migration(s)${NC}" - if ! confirm_action "remove all existing migrations and regenerate from source"; then - echo -e "${YELLOW}Generation cancelled${NC}" - return 2 - fi - echo "Removing existing migrations..." - rm -rf "${MIGRATIONS_DIR}" - fi - fi - fi - - mkdir -p "${MIGRATIONS_DIR}" - - local max_version=$(grep -E '^\s+[0-9]+ =>' "${RUST_SESSION_MANAGER}" | \ - sed 's/[^0-9]//g' | \ - sort -n | \ - tail -1) - - if [[ -z "$max_version" ]]; then - max_version=2 - fi - - local generated_count=0 - local skipped_count=0 - - for version in $(seq 1 $max_version); do - local padded_version=$(printf "%03d" $version) - local sql=$(extract_migration_sql "$version" "${RUST_SESSION_MANAGER}") - - if [[ -z "$sql" ]]; then - echo -e "${YELLOW}⚠ No SQL found for version $version, skipping...${NC}" - skipped_count=$((skipped_count + 1)) - continue - fi - - if ! validate_sql_syntax "$sql" "migration v$version"; then - echo -e "${YELLOW}⚠ Validation warning for version $version, but continuing...${NC}" - fi - - local migration_name - if echo "$sql" | grep -q "CREATE TABLE.*schema_version"; then - migration_name="add_schema_version" - elif echo "$sql" | grep -q "ALTER TABLE.*ADD COLUMN"; then - local column=$(echo "$sql" | sed -n 's/.*ADD COLUMN \([^ ]*\).*/\1/p' | head -1) - migration_name="add_${column}" - elif echo "$sql" | grep -q "RENAME COLUMN"; then - local old_col=$(echo "$sql" | sed -n 's/.*RENAME COLUMN \([^ ]*\) TO.*/\1/p') - local new_col=$(echo "$sql" | sed -n 's/.*TO \([^ ]*\).*/\1/p') - migration_name="rename_${old_col}_to_${new_col}" - else - migration_name="migration_${version}" - fi - - local migration_dir="${MIGRATIONS_DIR}/${padded_version}_${migration_name}" - mkdir -p "$migration_dir" - - echo "BEGIN TRANSACTION;" > "${migration_dir}/up.sql" - echo "" >> "${migration_dir}/up.sql" - echo "$sql" >> "${migration_dir}/up.sql" - echo "" >> "${migration_dir}/up.sql" - echo "INSERT INTO schema_version (version) VALUES ($version);" >> "${migration_dir}/up.sql" - echo "" >> "${migration_dir}/up.sql" - echo "COMMIT;" >> "${migration_dir}/up.sql" - - generate_rollback_sql "$version" "$sql" > "${migration_dir}/down.sql" - - generate_metadata "$version" "$sql" > "${migration_dir}/metadata.txt" - - echo -e "${GREEN}✓ Generated migration $padded_version: ${migration_dir##*/}${NC}" - generated_count=$((generated_count + 1)) - done - - echo "" - echo -e "${GREEN}✓ Generation complete!${NC}" - echo "Generated: $generated_count migrations" - if [[ $skipped_count -gt 0 ]]; then - echo "Skipped: $skipped_count migrations" - fi - echo "" - echo -e "${YELLOW}Note:${NC} Please review generated rollback SQL (down.sql) files." - echo "Some migrations may require manual rollback implementation." -} - -show_help() { - local latest_version=$(get_latest_version) - - echo -e "${BLUE}Goose Database Migration Helper${NC}" - echo "" - echo "This script is a developer utility for manually managing database schema" - echo "versions when switching between branches with different schema requirements." - echo "Migrations are stored in ${MIGRATIONS_DIR}." - echo "" - echo -e "${CYAN}Usage:${NC} $0 [flags] [arguments] [flags]" - echo "" - echo -e "${CYAN}Global Flags (can be placed before or after the command):${NC}" - echo -e " ${GREEN}--dry-run${NC}" - echo " Preview changes without modifying the database" - echo " Works with: migrate-to, restore" - echo "" - echo -e " ${GREEN}--yes, -y${NC}" - echo " Skip confirmation prompts (useful for automation)" - echo " Works with: migrate-to, restore, generate-migrations --clean" - echo "" - echo -e " ${GREEN}--clean${NC}" - echo " Remove all existing migrations before regenerating" - echo " Works with: generate-migrations" - echo " Useful when switching between branches with different migrations" - echo "" - echo -e "${CYAN}Commands:${NC}" - echo -e " ${GREEN}status${NC}" - echo " Show current database schema version, table structure, and statistics" - echo "" - echo -e " ${GREEN}migrate-to ${NC}" - echo " Migrate database to a specific schema version (0-${latest_version})" - echo " Automatically handles forward migrations and rollbacks" - echo "" - echo -e " ${GREEN}history${NC}" - echo " Show all available migrations and their descriptions" - echo "" - echo -e " ${GREEN}generate-migrations${NC}" - echo " Auto-generate migration files from Rust source code (session_manager.rs)" - echo " Creates up.sql, down.sql, and metadata.txt for each migration" - echo "" - echo -e " ${GREEN}backup${NC}" - echo " Create a manual backup of the current database" - echo "" - echo -e " ${GREEN}list-backups${NC}" - echo " Show all available backups with their versions and sizes" - echo "" - echo -e " ${GREEN}restore ${NC}" - echo " Restore database from a backup file" - echo "" - echo -e " ${GREEN}help${NC}" - echo " Show this help message" - echo "" - echo -e "${CYAN}Examples:${NC}" - echo " # Check current status" - echo " $0 status" - echo "" - echo " # View all available migrations" - echo " $0 history" - echo "" - echo " # Preview migration without making changes (dry-run before)" - echo " $0 --dry-run migrate-to 3" - echo "" - echo " # Flags can also be placed after the command and arguments" - echo " $0 migrate-to 3 --dry-run" - echo "" - echo " # Migrate to version 2" - echo " $0 migrate-to 2" - echo "" - echo " # Rollback to version 1 without confirmation prompt" - echo " $0 migrate-to 1 --yes" - echo "" - echo " # Create a backup" - echo " $0 backup" - echo "" - echo " # Clean regenerate migrations (useful when switching branches)" - echo " $0 generate-migrations --clean" - echo "" - echo " # Clean regenerate without confirmation" - echo " $0 generate-migrations --clean --yes" - echo "" - echo -e "${CYAN}Adding New Migrations:${NC}" - echo " After adding a migration to session_manager.rs, run:" - echo "" - echo " $0 generate-migrations" - echo "" - echo " This will automatically extract migrations from the Rust source" - echo " and create the necessary SQL files in ${MIGRATIONS_DIR}." - echo "" - echo -e " ${YELLOW}Note:${NC} Review generated down.sql files, as some rollbacks" - echo -e " may require manual implementation." - echo "" - echo -e "${CYAN}Switching Branches:${NC}" - echo " When switching between branches with different migrations:" - echo "" - echo " # Clean and regenerate to match current branch" - echo " git checkout main" - echo " $0 generate-migrations --clean" - echo "" - echo " # Or manually remove specific migrations" - echo " rm -rf ~/.local/share/goose/migrations/004_*" - echo " $0 generate-migrations" - echo "" - echo -e "${CYAN}Configuration:${NC}" - echo " Database: ${DB_PATH}" - echo " Backups: ${BACKUP_DIR}" - echo " Migrations: ${MIGRATIONS_DIR}" - echo " Latest: v${latest_version}" - echo "" - echo -e "${YELLOW}Note:${NC} All migrations automatically create backups before making changes." -} - -main() { - local non_flag_args=() - - while [[ $# -gt 0 ]]; do - case "$1" in - --dry-run) - DRY_RUN=true - shift - ;; - --yes|-y) - SKIP_CONFIRM=true - shift - ;; - --clean) - CLEAN_GENERATE=true - shift - ;; - --help|-h) - show_help - exit 0 - ;; - -*) - echo -e "${RED}ERROR: Unknown flag: $1${NC}" >&2 - echo "" - show_help - exit 1 - ;; - *) - non_flag_args+=("$1") - shift - ;; - esac - done - - local command=${non_flag_args[0]:-help} - - case "${command}" in - status) - show_status - ;; - migrate-to) - migrate_to_version "${non_flag_args[1]}" - ;; - history) - show_version_history - ;; - generate-migrations) - generate_migrations - ;; - backup) - create_backup - ;; - list-backups) - list_backups - ;; - restore) - restore_backup "${non_flag_args[1]}" - ;; - migrate) - local latest_version=$(get_latest_version) - echo -e "${YELLOW}Note: 'migrate' is deprecated. Use 'migrate-to ${latest_version}' instead.${NC}" - echo "" - migrate_to_version ${latest_version} - ;; - rollback) - echo -e "${YELLOW}Note: 'rollback' is deprecated. Use 'migrate-to ' instead.${NC}" - echo -e "${YELLOW}Use '$0 history' to see available versions.${NC}" - echo "" - show_version_history - ;; - compatible-with) - echo -e "${RED}ERROR: 'compatible-with' command has been removed.${NC}" >&2 - echo "" - echo "The script now uses a generic migration system." - echo "To migrate your database, use: $0 migrate-to " - echo "" - echo "Available migrations:" - show_version_history - exit 1 - ;; - help) - show_help - ;; - *) - echo -e "${RED}ERROR: Unknown command: ${command}${NC}" >&2 - echo "" - show_help - exit 1 - ;; - esac -} - -main "$@" diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index c641a345d7ec..e922c722ddd4 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -1886,12 +1886,12 @@ ] } }, - "/sessions/{session_id}/export": { - "get": { + "/sessions/{session_id}/description": { + "put": { "tags": [ "Session Management" ], - "operationId": "export_session", + "operationId": "update_session_description", "parameters": [ { "name": "session_id", @@ -1903,17 +1903,23 @@ } } ], - "responses": { - "200": { - "description": "Session exported successfully", - "content": { - "text/plain": { - "schema": { - "type": "string" - } + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateSessionDescriptionRequest" } } }, + "required": true + }, + "responses": { + "200": { + "description": "Session description updated successfully" + }, + "400": { + "description": "Bad request - Description too long (max 200 characters)" + }, "401": { "description": "Unauthorized - Invalid or missing API key" }, @@ -1931,12 +1937,12 @@ ] } }, - "/sessions/{session_id}/name": { - "put": { + "/sessions/{session_id}/export": { + "get": { "tags": [ "Session Management" ], - "operationId": "update_session_name", + "operationId": "export_session", "parameters": [ { "name": "session_id", @@ -1948,22 +1954,16 @@ } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateSessionNameRequest" - } - } - }, - "required": true - }, "responses": { "200": { - "description": "Session name updated successfully" - }, - "400": { - "description": "Bad request - Name too long (max 200 characters)" + "description": "Session exported successfully", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } }, "401": { "description": "Unauthorized - Invalid or missing API key" @@ -4077,7 +4077,7 @@ "required": [ "id", "working_dir", - "name", + "description", "created_at", "updated_at", "extension_data", @@ -4111,6 +4111,9 @@ "type": "string", "format": "date-time" }, + "description": { + "type": "string" + }, "extension_data": { "$ref": "#/components/schemas/ExtensionData" }, @@ -4126,9 +4129,6 @@ "type": "integer", "minimum": 0 }, - "name": { - "type": "string" - }, "output_tokens": { "type": "integer", "format": "int32", @@ -4162,9 +4162,6 @@ }, "nullable": true }, - "user_set_name": { - "type": "boolean" - }, "working_dir": { "type": "string" } @@ -4688,15 +4685,15 @@ } } }, - "UpdateSessionNameRequest": { + "UpdateSessionDescriptionRequest": { "type": "object", "required": [ - "name" + "description" ], "properties": { - "name": { + "description": { "type": "string", - "description": "Updated name for the session (max 200 characters)" + "description": "Updated description (name) for the session (max 200 characters)" } } }, diff --git a/ui/desktop/src/App.test.tsx b/ui/desktop/src/App.test.tsx index 1955821280d4..6889f4155883 100644 --- a/ui/desktop/src/App.test.tsx +++ b/ui/desktop/src/App.test.tsx @@ -120,7 +120,7 @@ vi.mock('./contexts/ChatContext', () => ({ useChatContext: () => ({ chat: { id: 'test-id', - name: 'Test Chat', + title: 'Test Chat', messages: [], messageHistoryIndex: 0, recipe: null, diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index 394e8df99b7c..403a21af8bb1 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -302,7 +302,7 @@ export function AppInner() { const [chat, setChat] = useState({ sessionId: '', - name: 'Pair Chat', + title: 'Pair Chat', messages: [], messageHistoryIndex: 0, recipe: null, diff --git a/ui/desktop/src/api/sdk.gen.ts b/ui/desktop/src/api/sdk.gen.ts index 866f8e1a6305..a4a30661dcf8 100644 --- a/ui/desktop/src/api/sdk.gen.ts +++ b/ui/desktop/src/api/sdk.gen.ts @@ -2,7 +2,7 @@ import type { Client, Options as Options2, TDataShape } from './client'; import { client } from './client.gen'; -import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AddSubRecipesData, AddSubRecipesErrors, AddSubRecipesResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, ConfirmPermissionData, ConfirmPermissionErrors, ConfirmPermissionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, ExtendPromptData, ExtendPromptErrors, ExtendPromptResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetToolsData, GetToolsErrors, GetToolsResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, ManageContextData, ManageContextErrors, ManageContextResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StatusData, StatusResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateRouterToolSelectorData, UpdateRouterToolSelectorErrors, UpdateRouterToolSelectorResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionConfigData, UpdateSessionConfigErrors, UpdateSessionConfigResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen'; +import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AddSubRecipesData, AddSubRecipesErrors, AddSubRecipesResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, ConfirmPermissionData, ConfirmPermissionErrors, ConfirmPermissionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, ExtendPromptData, ExtendPromptErrors, ExtendPromptResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetToolsData, GetToolsErrors, GetToolsResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, ManageContextData, ManageContextErrors, ManageContextResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StatusData, StatusResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateRouterToolSelectorData, UpdateRouterToolSelectorErrors, UpdateRouterToolSelectorResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionConfigData, UpdateSessionConfigErrors, UpdateSessionConfigResponses, UpdateSessionDescriptionData, UpdateSessionDescriptionErrors, UpdateSessionDescriptionResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen'; export type Options = Options2 & { /** @@ -504,16 +504,9 @@ export const getSession = (options: Option }); }; -export const exportSession = (options: Options) => { - return (options.client ?? client).get({ - url: '/sessions/{session_id}/export', - ...options - }); -}; - -export const updateSessionName = (options: Options) => { - return (options.client ?? client).put({ - url: '/sessions/{session_id}/name', +export const updateSessionDescription = (options: Options) => { + return (options.client ?? client).put({ + url: '/sessions/{session_id}/description', ...options, headers: { 'Content-Type': 'application/json', @@ -522,6 +515,13 @@ export const updateSessionName = (options: }); }; +export const exportSession = (options: Options) => { + return (options.client ?? client).get({ + url: '/sessions/{session_id}/export', + ...options + }); +}; + export const updateSessionUserRecipeValues = (options: Options) => { return (options.client ?? client).put({ url: '/sessions/{session_id}/user_recipe_values', diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index d112d61d1910..defdc3bc6437 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -693,11 +693,11 @@ export type Session = { accumulated_total_tokens?: number | null; conversation?: Conversation | null; created_at: string; + description: string; extension_data: ExtensionData; id: string; input_tokens?: number | null; message_count: number; - name: string; output_tokens?: number | null; recipe?: Recipe | null; schedule_id?: string | null; @@ -706,7 +706,6 @@ export type Session = { user_recipe_values?: { [key: string]: string; } | null; - user_set_name?: boolean; working_dir: string; }; @@ -889,11 +888,11 @@ export type UpdateScheduleRequest = { cron: string; }; -export type UpdateSessionNameRequest = { +export type UpdateSessionDescriptionRequest = { /** - * Updated name for the session (max 200 characters) + * Updated description (name) for the session (max 200 characters) */ - name: string; + description: string; }; export type UpdateSessionUserRecipeValuesRequest = { @@ -2394,8 +2393,8 @@ export type GetSessionResponses = { export type GetSessionResponse = GetSessionResponses[keyof GetSessionResponses]; -export type ExportSessionData = { - body?: never; +export type UpdateSessionDescriptionData = { + body: UpdateSessionDescriptionRequest; path: { /** * Unique identifier for the session @@ -2403,10 +2402,14 @@ export type ExportSessionData = { session_id: string; }; query?: never; - url: '/sessions/{session_id}/export'; + url: '/sessions/{session_id}/description'; }; -export type ExportSessionErrors = { +export type UpdateSessionDescriptionErrors = { + /** + * Bad request - Description too long (max 200 characters) + */ + 400: unknown; /** * Unauthorized - Invalid or missing API key */ @@ -2421,17 +2424,15 @@ export type ExportSessionErrors = { 500: unknown; }; -export type ExportSessionResponses = { +export type UpdateSessionDescriptionResponses = { /** - * Session exported successfully + * Session description updated successfully */ - 200: string; + 200: unknown; }; -export type ExportSessionResponse = ExportSessionResponses[keyof ExportSessionResponses]; - -export type UpdateSessionNameData = { - body: UpdateSessionNameRequest; +export type ExportSessionData = { + body?: never; path: { /** * Unique identifier for the session @@ -2439,14 +2440,10 @@ export type UpdateSessionNameData = { session_id: string; }; query?: never; - url: '/sessions/{session_id}/name'; + url: '/sessions/{session_id}/export'; }; -export type UpdateSessionNameErrors = { - /** - * Bad request - Name too long (max 200 characters) - */ - 400: unknown; +export type ExportSessionErrors = { /** * Unauthorized - Invalid or missing API key */ @@ -2461,13 +2458,15 @@ export type UpdateSessionNameErrors = { 500: unknown; }; -export type UpdateSessionNameResponses = { +export type ExportSessionResponses = { /** - * Session name updated successfully + * Session exported successfully */ - 200: unknown; + 200: string; }; +export type ExportSessionResponse = ExportSessionResponses[keyof ExportSessionResponses]; + export type UpdateSessionUserRecipeValuesData = { body: UpdateSessionUserRecipeValuesRequest; path: { diff --git a/ui/desktop/src/components/BaseChat2.tsx b/ui/desktop/src/components/BaseChat2.tsx index 68ca077671ce..cdf5292fd089 100644 --- a/ui/desktop/src/components/BaseChat2.tsx +++ b/ui/desktop/src/components/BaseChat2.tsx @@ -94,7 +94,7 @@ function BaseChatContent({ // todo: set to null instead and handle that in other places const emptyChat: ChatType = { sessionId: resumeSessionId, - name: 'Loading...', + title: 'Loading...', messageHistoryIndex: 0, messages: [], recipe: null, @@ -107,7 +107,7 @@ function BaseChatContent({ const conversation = session.conversation || []; const loadedChat: ChatType = { sessionId: session.id, - name: session.name || 'Untitled Chat', + title: session.description || 'Untitled Chat', messageHistoryIndex: 0, messages: conversation, recipe: null, diff --git a/ui/desktop/src/components/GooseSidebar/AppSidebar.tsx b/ui/desktop/src/components/GooseSidebar/AppSidebar.tsx index 67d59bd18e8a..89efec1c5d9c 100644 --- a/ui/desktop/src/components/GooseSidebar/AppSidebar.tsx +++ b/ui/desktop/src/components/GooseSidebar/AppSidebar.tsx @@ -116,16 +116,16 @@ const AppSidebar: React.FC = ({ currentPath }) => { if ( currentPath === '/pair' && - chatContext?.chat?.name && - chatContext.chat.name !== DEFAULT_CHAT_TITLE + chatContext?.chat?.title && + chatContext.chat.title !== DEFAULT_CHAT_TITLE ) { - titleBits.push(chatContext.chat.name); + titleBits.push(chatContext.chat.title); } else if (currentPath !== '/' && currentItem) { titleBits.push(currentItem.label); } document.title = titleBits.join(' - '); - }, [currentPath, chatContext?.chat?.name]); + }, [currentPath, chatContext?.chat?.title]); const isActivePath = (path: string) => { return currentPath === path; diff --git a/ui/desktop/src/components/context_management/__tests__/CompactionMarker.test.tsx b/ui/desktop/src/components/context_management/__tests__/CompactionMarker.test.tsx index 4e09d660d738..756d1cf14960 100644 --- a/ui/desktop/src/components/context_management/__tests__/CompactionMarker.test.tsx +++ b/ui/desktop/src/components/context_management/__tests__/CompactionMarker.test.tsx @@ -10,8 +10,7 @@ const default_message: Message = { }, id: '1', role: 'assistant', - created: 1000, - content: [], + created: 1000,content: [] }; describe('CompactionMarker', () => { diff --git a/ui/desktop/src/components/sessions/SessionHistoryView.tsx b/ui/desktop/src/components/sessions/SessionHistoryView.tsx index ce3cd0844ba1..4d297ae9d1ff 100644 --- a/ui/desktop/src/components/sessions/SessionHistoryView.tsx +++ b/ui/desktop/src/components/sessions/SessionHistoryView.tsx @@ -185,7 +185,7 @@ const SessionHistoryView: React.FC = ({ config.baseUrl, session.working_dir, messages, - session.name || 'Shared Session', + session.description || 'Shared Session', session.total_tokens || 0 ); @@ -270,7 +270,7 @@ const SessionHistoryView: React.FC = ({
diff --git a/ui/desktop/src/components/sessions/SessionItem.tsx b/ui/desktop/src/components/sessions/SessionItem.tsx index e8a010c91b28..05a524b9899d 100644 --- a/ui/desktop/src/components/sessions/SessionItem.tsx +++ b/ui/desktop/src/components/sessions/SessionItem.tsx @@ -12,7 +12,7 @@ const SessionItem: React.FC = ({ session, extraActions }) => { return (
-
{session.name}
+
{session.description || `Session ${session.id}`}
{formatDate(session.updated_at)} • {session.message_count} messages
diff --git a/ui/desktop/src/components/sessions/SessionListView.tsx b/ui/desktop/src/components/sessions/SessionListView.tsx index d137db74f7bc..2f6d8a67eb91 100644 --- a/ui/desktop/src/components/sessions/SessionListView.tsx +++ b/ui/desktop/src/components/sessions/SessionListView.tsx @@ -27,7 +27,7 @@ import { importSession, listSessions, Session, - updateSessionName, + updateSessionDescription, } from '../../api'; interface EditSessionModalProps { @@ -45,7 +45,7 @@ const EditSessionModal = React.memo( useEffect(() => { if (session && isOpen) { - setDescription(session.name); + setDescription(session.description || session.id); } else if (!isOpen) { // Reset state when modal closes setDescription(''); @@ -57,16 +57,16 @@ const EditSessionModal = React.memo( if (!session || disabled) return; const trimmedDescription = description.trim(); - if (trimmedDescription === session.name) { + if (trimmedDescription === session.description) { onClose(); return; } setIsUpdating(true); try { - await updateSessionName({ + await updateSessionDescription({ path: { session_id: session.id }, - body: { name: trimmedDescription }, + body: { description: trimmedDescription }, throwOnError: true, }); await onSave(session.id, trimmedDescription); @@ -80,7 +80,7 @@ const EditSessionModal = React.memo( const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; console.error('Failed to update session description:', errorMessage); toast.error(`Failed to update session description: ${errorMessage}`); - setDescription(session.name); + setDescription(session.description || session.id); } finally { setIsUpdating(false); } @@ -333,7 +333,7 @@ const SessionListView: React.FC = React.memo( startTransition(() => { const searchTerm = caseSensitive ? debouncedSearchTerm : debouncedSearchTerm.toLowerCase(); const filtered = sessions.filter((session) => { - const description = session.name; + const description = session.description || session.id; const workingDir = session.working_dir; const sessionId = session.id; @@ -397,7 +397,7 @@ const SessionListView: React.FC = React.memo( const handleModalSave = useCallback(async (sessionId: string, newDescription: string) => { // Update state immediately for optimistic UI setSessions((prevSessions) => - prevSessions.map((s) => (s.id === sessionId ? { ...s, name: newDescription } : s)) + prevSessions.map((s) => (s.id === sessionId ? { ...s, description: newDescription } : s)) ); }, []); @@ -416,7 +416,7 @@ const SessionListView: React.FC = React.memo( setShowDeleteConfirmation(false); const sessionToDeleteId = sessionToDelete.id; - const sessionName = sessionToDelete.name; + const sessionName = sessionToDelete.description || sessionToDelete.id; setSessionToDelete(null); try { @@ -451,7 +451,7 @@ const SessionListView: React.FC = React.memo( const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; - a.download = `${session.name}.json`; + a.download = `${session.description || session.id}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); @@ -557,7 +557,9 @@ const SessionListView: React.FC = React.memo(
-

{session.name}

+

+ {session.description || session.id} +

@@ -804,7 +806,7 @@ const SessionListView: React.FC = React.memo(
- {session.name} + + {session.description || session.id} +
{formatDateOnly(session.updated_at)} diff --git a/ui/desktop/src/components/sessions/SessionsView.tsx b/ui/desktop/src/components/sessions/SessionsView.tsx index a6d6cd27202f..dc822e31b273 100644 --- a/ui/desktop/src/components/sessions/SessionsView.tsx +++ b/ui/desktop/src/components/sessions/SessionsView.tsx @@ -70,14 +70,13 @@ const SessionsView: React.FC = () => { selectedSession || { id: initialSessionId || '', conversation: [], - name: 'Loading...', + description: 'Loading...', working_dir: '', message_count: 0, total_tokens: 0, created_at: '', updated_at: '', extension_data: {}, - user_set_name: false, } } isLoading={isLoadingSession} diff --git a/ui/desktop/src/contexts/ChatContext.tsx b/ui/desktop/src/contexts/ChatContext.tsx index a23dc66ecd60..7e4cf90faffe 100644 --- a/ui/desktop/src/contexts/ChatContext.tsx +++ b/ui/desktop/src/contexts/ChatContext.tsx @@ -55,7 +55,7 @@ export const ChatProvider: React.FC = ({ const resetChat = () => { setChat({ sessionId: '', - name: DEFAULT_CHAT_TITLE, + title: DEFAULT_CHAT_TITLE, messages: [], messageHistoryIndex: 0, recipe: null, diff --git a/ui/desktop/src/hooks/useAgent.ts b/ui/desktop/src/hooks/useAgent.ts index df55e17e7abe..b5907ec9cebd 100644 --- a/ui/desktop/src/hooks/useAgent.ts +++ b/ui/desktop/src/hooks/useAgent.ts @@ -81,7 +81,7 @@ export function useAgent(): UseAgentReturn { const messages = agentSession.conversation || []; return { sessionId: agentSession.id, - name: agentSession.recipe?.title || agentSession.name, + title: agentSession.recipe?.title || agentSession.description, messageHistoryIndex: 0, messages, recipe: agentSession.recipe, @@ -182,7 +182,7 @@ export function useAgent(): UseAgentReturn { const messages = initContext.recipe && !initContext.resumeSessionId ? [] : conversation; let initChat: ChatType = { sessionId: agentSession.id, - name: agentSession.recipe?.title || agentSession.name, + title: agentSession.recipe?.title || agentSession.description, messageHistoryIndex: 0, messages: messages, recipe: recipe, diff --git a/ui/desktop/src/hooks/useChatEngine.test.ts b/ui/desktop/src/hooks/useChatEngine.test.ts index d62befeb9dc1..f1241a1ff2b2 100644 --- a/ui/desktop/src/hooks/useChatEngine.test.ts +++ b/ui/desktop/src/hooks/useChatEngine.test.ts @@ -131,7 +131,7 @@ describe('useChatEngine', () => { const mockChat: ChatType = { sessionId: 'test-chat', messages: initialMessages, - name: 'Test Chat', + title: 'Test Chat', messageHistoryIndex: 0, }; diff --git a/ui/desktop/src/types/chat.ts b/ui/desktop/src/types/chat.ts index 5a9f994eacbc..70130ebcf935 100644 --- a/ui/desktop/src/types/chat.ts +++ b/ui/desktop/src/types/chat.ts @@ -3,7 +3,7 @@ import { Message } from '../api'; export interface ChatType { sessionId: string; - name: string; + title: string; messageHistoryIndex: number; messages: Message[]; recipe?: Recipe | null; // Add recipe configuration to chat state