mirror of
https://github.com/block/goose.git
synced 2026-06-01 22:11:07 +02:00
(re)Standardize Session Name Attribute (#5279)
This commit is contained in:
@@ -75,7 +75,7 @@ async fn get_session_id(identifier: Identifier) -> Result<String> {
|
||||
|
||||
sessions
|
||||
.into_iter()
|
||||
.find(|s| s.id == name || s.description.contains(&name))
|
||||
.find(|s| s.name == name || s.id == name)
|
||||
.map(|s| s.id)
|
||||
.ok_or_else(|| anyhow::anyhow!("No session found with name '{}'", name))
|
||||
} else if let Some(path) = identifier.path {
|
||||
|
||||
@@ -222,7 +222,7 @@ pub async fn handle_schedule_sessions(id: String, limit: Option<usize>) -> Resul
|
||||
" - Session ID: {}, Working Dir: {}, Description: \"{}\", Schedule ID: {:?}",
|
||||
session_name, // Display the session_name as Session ID
|
||||
metadata.working_dir.display(),
|
||||
metadata.description,
|
||||
metadata.name,
|
||||
metadata.schedule_id.as_deref().unwrap_or("N/A")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ const TRUNCATED_DESC_LENGTH: usize = 60;
|
||||
pub async fn remove_sessions(sessions: Vec<Session>) -> Result<()> {
|
||||
println!("The following sessions will be removed:");
|
||||
for session in &sessions {
|
||||
println!("- {} {}", session.id, session.description);
|
||||
println!("- {} {}", session.id, session.name);
|
||||
}
|
||||
|
||||
let should_delete = confirm("Are you sure you want to delete these sessions?")
|
||||
@@ -46,10 +46,10 @@ fn prompt_interactive_session_removal(sessions: &[Session]) -> Result<Vec<Sessio
|
||||
let display_map: std::collections::HashMap<String, Session> = sessions
|
||||
.iter()
|
||||
.map(|s| {
|
||||
let desc = if s.description.is_empty() {
|
||||
"(no description)"
|
||||
let desc = if s.name.is_empty() {
|
||||
"(no name)"
|
||||
} else {
|
||||
&s.description
|
||||
&s.name
|
||||
};
|
||||
let truncated_desc = safe_truncate(desc, TRUNCATED_DESC_LENGTH);
|
||||
let display_text = format!("{} - {} ({})", s.updated_at, truncated_desc, s.id);
|
||||
@@ -155,10 +155,7 @@ pub async fn handle_session_list(
|
||||
|
||||
println!("Available sessions:");
|
||||
for session in sessions {
|
||||
let output = format!(
|
||||
"{} - {} - {}",
|
||||
session.id, session.description, session.updated_at
|
||||
);
|
||||
let output = format!("{} - {} - {}", session.id, session.name, session.updated_at);
|
||||
println!("{}", output);
|
||||
}
|
||||
}
|
||||
@@ -189,7 +186,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.description)
|
||||
export_session_to_markdown(conversation.messages().to_vec(), &session.name)
|
||||
}
|
||||
_ => return Err(anyhow::anyhow!("Unsupported format: {}", format)),
|
||||
};
|
||||
@@ -323,10 +320,10 @@ pub async fn prompt_interactive_session_selection() -> Result<String> {
|
||||
let display_map: std::collections::HashMap<String, Session> = sessions
|
||||
.iter()
|
||||
.map(|s| {
|
||||
let desc = if s.description.is_empty() {
|
||||
"(no description)"
|
||||
let desc = if s.name.is_empty() {
|
||||
"(no name)"
|
||||
} else {
|
||||
&s.description
|
||||
&s.name
|
||||
};
|
||||
let truncated_desc = safe_truncate(desc, TRUNCATED_DESC_LENGTH);
|
||||
|
||||
|
||||
@@ -290,7 +290,7 @@ async fn list_sessions() -> Json<serde_json::Value> {
|
||||
session_info.push(serde_json::json!({
|
||||
"name": session.id,
|
||||
"path": session.id,
|
||||
"description": session.description,
|
||||
"description": session.name,
|
||||
"message_count": session.message_count,
|
||||
"working_dir": session.working_dir
|
||||
}));
|
||||
|
||||
@@ -354,7 +354,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_description,
|
||||
super::routes::session::update_session_name,
|
||||
super::routes::session::delete_session,
|
||||
super::routes::session::export_session,
|
||||
super::routes::session::import_session,
|
||||
@@ -395,7 +395,7 @@ derive_utoipa!(Icon as IconSchema);
|
||||
super::routes::reply::ChatRequest,
|
||||
super::routes::session::ImportSessionRequest,
|
||||
super::routes::session::SessionListResponse,
|
||||
super::routes::session::UpdateSessionDescriptionRequest,
|
||||
super::routes::session::UpdateSessionNameRequest,
|
||||
super::routes::session::UpdateSessionUserRecipeValuesRequest,
|
||||
super::routes::session::UpdateSessionUserRecipeValuesResponse,
|
||||
Message,
|
||||
|
||||
@@ -122,9 +122,9 @@ async fn start_agent(
|
||||
}
|
||||
|
||||
let counter = state.session_counter.fetch_add(1, Ordering::SeqCst) + 1;
|
||||
let description = format!("New session {}", counter);
|
||||
let name = format!("New session {}", counter);
|
||||
|
||||
let mut session = SessionManager::create_session(PathBuf::from(&working_dir), description)
|
||||
let mut session = SessionManager::create_session(PathBuf::from(&working_dir), name)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Failed to create session: {}", err);
|
||||
|
||||
@@ -68,10 +68,10 @@ fn default_limit() -> u32 {
|
||||
#[derive(Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SessionDisplayInfo {
|
||||
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)
|
||||
id: String,
|
||||
name: String,
|
||||
created_at: String,
|
||||
working_dir: String,
|
||||
schedule_id: Option<String>,
|
||||
message_count: usize,
|
||||
total_tokens: Option<i32>,
|
||||
@@ -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.description,
|
||||
name: session.name,
|
||||
created_at: parse_session_name_to_iso(&session_name),
|
||||
working_dir: session.working_dir.to_string_lossy().into_owned(),
|
||||
schedule_id: session.schedule_id,
|
||||
|
||||
@@ -26,9 +26,9 @@ pub struct SessionListResponse {
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateSessionDescriptionRequest {
|
||||
/// Updated description (name) for the session (max 200 characters)
|
||||
description: String,
|
||||
pub struct UpdateSessionNameRequest {
|
||||
/// Updated name for the session (max 200 characters)
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
@@ -49,7 +49,7 @@ pub struct ImportSessionRequest {
|
||||
json: String,
|
||||
}
|
||||
|
||||
const MAX_DESCRIPTION_LENGTH: usize = 200;
|
||||
const MAX_NAME_LENGTH: usize = 200;
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
@@ -118,14 +118,14 @@ async fn get_session_insights() -> Result<Json<SessionInsights>, StatusCode> {
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/sessions/{session_id}/description",
|
||||
request_body = UpdateSessionDescriptionRequest,
|
||||
path = "/sessions/{session_id}/name",
|
||||
request_body = UpdateSessionNameRequest,
|
||||
params(
|
||||
("session_id" = String, Path, description = "Unique identifier for the session")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Session description updated successfully"),
|
||||
(status = 400, description = "Bad request - Description too long (max 200 characters)"),
|
||||
(status = 200, description = "Session name updated successfully"),
|
||||
(status = 400, description = "Bad request - Name 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")
|
||||
@@ -135,16 +135,20 @@ async fn get_session_insights() -> Result<Json<SessionInsights>, StatusCode> {
|
||||
),
|
||||
tag = "Session Management"
|
||||
)]
|
||||
async fn update_session_description(
|
||||
async fn update_session_name(
|
||||
Path(session_id): Path<String>,
|
||||
Json(request): Json<UpdateSessionDescriptionRequest>,
|
||||
Json(request): Json<UpdateSessionNameRequest>,
|
||||
) -> Result<StatusCode, StatusCode> {
|
||||
if request.description.len() > MAX_DESCRIPTION_LENGTH {
|
||||
let name = request.name.trim();
|
||||
if name.is_empty() {
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
}
|
||||
if name.len() > MAX_NAME_LENGTH {
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
SessionManager::update_session(&session_id)
|
||||
.description(request.description)
|
||||
.user_provided_name(name.to_string())
|
||||
.apply()
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
@@ -311,10 +315,7 @@ pub fn routes(state: Arc<AppState>) -> 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}/description",
|
||||
put(update_session_description),
|
||||
)
|
||||
.route("/sessions/{session_id}/name", put(update_session_name))
|
||||
.route(
|
||||
"/sessions/{session_id}/user_recipe_values",
|
||||
put(update_session_user_recipe_values),
|
||||
|
||||
@@ -902,9 +902,7 @@ 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_description(&session_id, provider).await
|
||||
{
|
||||
if let Err(e) = SessionManager::maybe_update_name(&session_id, provider).await {
|
||||
warn!("Failed to generate session description: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ use tokio::sync::OnceCell;
|
||||
use tracing::{info, warn};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
const CURRENT_SCHEMA_VERSION: i32 = 3;
|
||||
const CURRENT_SCHEMA_VERSION: i32 = 4;
|
||||
|
||||
static SESSION_STORAGE: OnceCell<Arc<SessionStorage>> = OnceCell::const_new();
|
||||
|
||||
@@ -27,7 +27,11 @@ pub struct Session {
|
||||
pub id: String,
|
||||
#[schema(value_type = String)]
|
||||
pub working_dir: PathBuf,
|
||||
pub description: String,
|
||||
// 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 created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub extension_data: ExtensionData,
|
||||
@@ -46,7 +50,8 @@ pub struct Session {
|
||||
|
||||
pub struct SessionUpdateBuilder {
|
||||
session_id: String,
|
||||
description: Option<String>,
|
||||
name: Option<String>,
|
||||
user_set_name: Option<bool>,
|
||||
working_dir: Option<PathBuf>,
|
||||
extension_data: Option<ExtensionData>,
|
||||
total_tokens: Option<Option<i32>>,
|
||||
@@ -71,7 +76,8 @@ impl SessionUpdateBuilder {
|
||||
fn new(session_id: String) -> Self {
|
||||
Self {
|
||||
session_id,
|
||||
description: None,
|
||||
name: None,
|
||||
user_set_name: None,
|
||||
working_dir: None,
|
||||
extension_data: None,
|
||||
total_tokens: None,
|
||||
@@ -86,8 +92,21 @@ impl SessionUpdateBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn description(mut self, description: impl Into<String>) -> Self {
|
||||
self.description = Some(description.into());
|
||||
pub fn user_provided_name(mut self, name: impl Into<String>) -> 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<String>) -> Self {
|
||||
let name = name.into().trim().to_string();
|
||||
if !name.is_empty() {
|
||||
self.name = Some(name);
|
||||
self.user_set_name = Some(false);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
@@ -164,10 +183,10 @@ impl SessionManager {
|
||||
.map(Arc::clone)
|
||||
}
|
||||
|
||||
pub async fn create_session(working_dir: PathBuf, description: String) -> Result<Session> {
|
||||
pub async fn create_session(working_dir: PathBuf, name: String) -> Result<Session> {
|
||||
Self::instance()
|
||||
.await?
|
||||
.create_session(working_dir, description)
|
||||
.create_session(working_dir, name)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -217,8 +236,13 @@ impl SessionManager {
|
||||
Self::instance().await?.import_session(json).await
|
||||
}
|
||||
|
||||
pub async fn maybe_update_description(id: &str, provider: Arc<dyn Provider>) -> Result<()> {
|
||||
pub async fn maybe_update_name(id: &str, provider: Arc<dyn Provider>) -> 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"))?;
|
||||
@@ -230,9 +254,9 @@ impl SessionManager {
|
||||
.count();
|
||||
|
||||
if user_message_count <= MSG_COUNT_FOR_SESSION_NAME_GENERATION {
|
||||
let description = provider.generate_session_name(&conversation).await?;
|
||||
let name = provider.generate_session_name(&conversation).await?;
|
||||
Self::update_session(id)
|
||||
.description(description)
|
||||
.system_generated_name(name)
|
||||
.apply()
|
||||
.await
|
||||
} else {
|
||||
@@ -267,7 +291,8 @@ impl Default for Session {
|
||||
Self {
|
||||
id: String::new(),
|
||||
working_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
|
||||
description: String::new(),
|
||||
name: String::new(),
|
||||
user_set_name: false,
|
||||
created_at: Default::default(),
|
||||
updated_at: Default::default(),
|
||||
extension_data: ExtensionData::default(),
|
||||
@@ -304,10 +329,22 @@ 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 = {
|
||||
let name_val: String = row.try_get("name").unwrap_or_default();
|
||||
if !name_val.is_empty() {
|
||||
name_val
|
||||
} else {
|
||||
row.try_get("description").unwrap_or_default()
|
||||
}
|
||||
};
|
||||
|
||||
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::<String, _>("working_dir")?),
|
||||
description: row.try_get("description")?,
|
||||
name,
|
||||
user_set_name,
|
||||
created_at: row.try_get("created_at")?,
|
||||
updated_at: row.try_get("updated_at")?,
|
||||
extension_data: serde_json::from_str(&row.try_get::<String, _>("extension_data")?)
|
||||
@@ -393,7 +430,9 @@ impl SessionStorage {
|
||||
r#"
|
||||
CREATE TABLE sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
user_set_name BOOLEAN DEFAULT FALSE,
|
||||
working_dir TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
@@ -501,15 +540,16 @@ impl SessionStorage {
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO sessions (
|
||||
id, description, working_dir, created_at, updated_at, extension_data,
|
||||
id, name, user_set_name, 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.description)
|
||||
.bind(&session.name)
|
||||
.bind(session.user_set_name)
|
||||
.bind(session.working_dir.to_string_lossy().as_ref())
|
||||
.bind(session.created_at)
|
||||
.bind(session.updated_at)
|
||||
@@ -617,6 +657,23 @@ impl SessionStorage {
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
}
|
||||
4 => {
|
||||
sqlx::query(
|
||||
r#"
|
||||
ALTER TABLE sessions ADD COLUMN name TEXT DEFAULT ''
|
||||
"#,
|
||||
)
|
||||
.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);
|
||||
}
|
||||
@@ -625,11 +682,11 @@ impl SessionStorage {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_session(&self, working_dir: PathBuf, description: String) -> Result<Session> {
|
||||
async fn create_session(&self, working_dir: PathBuf, name: String) -> Result<Session> {
|
||||
let today = chrono::Utc::now().format("%Y%m%d").to_string();
|
||||
Ok(sqlx::query_as(
|
||||
r#"
|
||||
INSERT INTO sessions (id, description, working_dir, extension_data)
|
||||
INSERT INTO sessions (id, name, user_set_name, working_dir, extension_data)
|
||||
VALUES (
|
||||
? || '_' || CAST(COALESCE((
|
||||
SELECT MAX(CAST(SUBSTR(id, 10) AS INTEGER))
|
||||
@@ -637,6 +694,7 @@ impl SessionStorage {
|
||||
WHERE id LIKE ? || '_%'
|
||||
), 0) + 1 AS TEXT),
|
||||
?,
|
||||
FALSE,
|
||||
?,
|
||||
'{}'
|
||||
)
|
||||
@@ -645,7 +703,7 @@ impl SessionStorage {
|
||||
)
|
||||
.bind(&today)
|
||||
.bind(&today)
|
||||
.bind(&description)
|
||||
.bind(&name)
|
||||
.bind(working_dir.to_string_lossy().as_ref())
|
||||
.fetch_one(&self.pool)
|
||||
.await?)
|
||||
@@ -654,7 +712,7 @@ impl SessionStorage {
|
||||
async fn get_session(&self, id: &str, include_messages: bool) -> Result<Session> {
|
||||
let mut session = sqlx::query_as::<_, Session>(
|
||||
r#"
|
||||
SELECT id, working_dir, description, created_at, updated_at, extension_data,
|
||||
SELECT id, working_dir, name, description, user_set_name, 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
|
||||
@@ -700,7 +758,8 @@ impl SessionStorage {
|
||||
};
|
||||
}
|
||||
|
||||
add_update!(builder.description, "description");
|
||||
add_update!(builder.name, "name");
|
||||
add_update!(builder.user_set_name, "user_set_name");
|
||||
add_update!(builder.working_dir, "working_dir");
|
||||
add_update!(builder.extension_data, "extension_data");
|
||||
add_update!(builder.total_tokens, "total_tokens");
|
||||
@@ -720,15 +779,16 @@ impl SessionStorage {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if !updates.is_empty() {
|
||||
query.push_str(", ");
|
||||
}
|
||||
query.push_str(", ");
|
||||
query.push_str("updated_at = datetime('now') WHERE id = ?");
|
||||
|
||||
let mut q = sqlx::query(&query);
|
||||
|
||||
if let Some(desc) = builder.description {
|
||||
q = q.bind(desc);
|
||||
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(wd) = builder.working_dir {
|
||||
q = q.bind(wd.to_string_lossy().to_string());
|
||||
@@ -869,7 +929,7 @@ impl SessionStorage {
|
||||
async fn list_sessions(&self) -> Result<Vec<Session>> {
|
||||
sqlx::query_as::<_, Session>(
|
||||
r#"
|
||||
SELECT s.id, s.working_dir, s.description, s.created_at, s.updated_at, s.extension_data,
|
||||
SELECT s.id, s.working_dir, s.name, s.description, s.user_set_name, 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,
|
||||
@@ -935,23 +995,26 @@ impl SessionStorage {
|
||||
let import: Session = serde_json::from_str(json)?;
|
||||
|
||||
let session = self
|
||||
.create_session(import.working_dir.clone(), import.description.clone())
|
||||
.create_session(import.working_dir.clone(), import.name.clone())
|
||||
.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?;
|
||||
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?;
|
||||
|
||||
if let Some(conversation) = import.conversation {
|
||||
self.replace_conversation(&session.id, &conversation)
|
||||
@@ -1021,7 +1084,7 @@ mod tests {
|
||||
session_storage
|
||||
.apply_update(
|
||||
SessionUpdateBuilder::new(session.id.clone())
|
||||
.description(format!("Updated session {}", i))
|
||||
.user_provided_name(format!("Updated session {}", i))
|
||||
.total_tokens(Some(100 * i)),
|
||||
)
|
||||
.await
|
||||
@@ -1054,7 +1117,7 @@ mod tests {
|
||||
|
||||
for session in &sessions {
|
||||
assert_eq!(session.message_count, 2);
|
||||
assert!(session.description.starts_with("Updated session"));
|
||||
assert!(session.name.starts_with("Updated session"));
|
||||
}
|
||||
|
||||
let insights = storage.get_insights().await.unwrap();
|
||||
@@ -1125,7 +1188,7 @@ mod tests {
|
||||
let imported = storage.import_session(&exported).await.unwrap();
|
||||
|
||||
assert_ne!(imported.id, original.id);
|
||||
assert_eq!(imported.description, DESCRIPTION);
|
||||
assert_eq!(imported.name, 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));
|
||||
@@ -1138,4 +1201,28 @@ 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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,7 +381,8 @@ pub fn create_test_session_metadata(message_count: usize, working_dir: &str) ->
|
||||
Session {
|
||||
id: "".to_string(),
|
||||
working_dir: PathBuf::from(working_dir),
|
||||
description: "Test session".to_string(),
|
||||
name: "Test session".to_string(),
|
||||
user_set_name: false,
|
||||
created_at: Default::default(),
|
||||
schedule_id: Some("test_job".to_string()),
|
||||
recipe: None,
|
||||
|
||||
Executable
+876
@@ -0,0 +1,876 @@
|
||||
#!/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 <version>"
|
||||
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}_<timestamp>.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 <backup-file>"
|
||||
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<current-version>_<timestamp>.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 <<EOF
|
||||
DESCRIPTION=$description
|
||||
AUTHOR=$author
|
||||
DATE=$date
|
||||
NOTES=Auto-generated from session_manager.rs
|
||||
EOF
|
||||
}
|
||||
|
||||
generate_migrations() {
|
||||
if [[ ! -f "${RUST_SESSION_MANAGER}" ]]; then
|
||||
echo -e "${RED}ERROR: Rust source file not found: ${RUST_SESSION_MANAGER}${NC}" >&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] <command> [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 <version>${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 <file>${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 <version>' 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 <version>"
|
||||
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 "$@"
|
||||
+62
-59
@@ -1794,57 +1794,6 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/sessions/{session_id}/description": {
|
||||
"put": {
|
||||
"tags": [
|
||||
"Session Management"
|
||||
],
|
||||
"operationId": "update_session_description",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "session_id",
|
||||
"in": "path",
|
||||
"description": "Unique identifier for the session",
|
||||
"required": true,
|
||||
"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"
|
||||
},
|
||||
"404": {
|
||||
"description": "Session not found"
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/sessions/{session_id}/export": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -1890,6 +1839,57 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/sessions/{session_id}/name": {
|
||||
"put": {
|
||||
"tags": [
|
||||
"Session Management"
|
||||
],
|
||||
"operationId": "update_session_name",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "session_id",
|
||||
"in": "path",
|
||||
"description": "Unique identifier for the session",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"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)"
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized - Invalid or missing API key"
|
||||
},
|
||||
"404": {
|
||||
"description": "Session not found"
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/sessions/{session_id}/user_recipe_values": {
|
||||
"put": {
|
||||
"tags": [
|
||||
@@ -3898,7 +3898,7 @@
|
||||
"required": [
|
||||
"id",
|
||||
"working_dir",
|
||||
"description",
|
||||
"name",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"extension_data",
|
||||
@@ -3932,9 +3932,6 @@
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"extension_data": {
|
||||
"$ref": "#/components/schemas/ExtensionData"
|
||||
},
|
||||
@@ -3950,6 +3947,9 @@
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"output_tokens": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
@@ -3983,6 +3983,9 @@
|
||||
},
|
||||
"nullable": true
|
||||
},
|
||||
"user_set_name": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"working_dir": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -4518,15 +4521,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"UpdateSessionDescriptionRequest": {
|
||||
"UpdateSessionNameRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"description"
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"description": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Updated description (name) for the session (max 200 characters)"
|
||||
"description": "Updated name for the session (max 200 characters)"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -120,7 +120,7 @@ vi.mock('./contexts/ChatContext', () => ({
|
||||
useChatContext: () => ({
|
||||
chat: {
|
||||
id: 'test-id',
|
||||
title: 'Test Chat',
|
||||
name: 'Test Chat',
|
||||
messages: [],
|
||||
messageHistoryIndex: 0,
|
||||
recipe: null,
|
||||
|
||||
@@ -316,7 +316,7 @@ export function AppInner() {
|
||||
|
||||
const [chat, setChat] = useState<ChatType>({
|
||||
sessionId: '',
|
||||
title: 'Pair Chat',
|
||||
name: 'Pair Chat',
|
||||
messages: [],
|
||||
messageHistoryIndex: 0,
|
||||
recipe: null,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import type { Client, Options as Options2, TDataShape } from './client';
|
||||
import { client } from './client.gen';
|
||||
import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, 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, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, 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, 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, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateRouterToolSelectorData, UpdateRouterToolSelectorErrors, UpdateRouterToolSelectorResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionDescriptionData, UpdateSessionDescriptionErrors, UpdateSessionDescriptionResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen';
|
||||
import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, 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, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, 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, 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, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateRouterToolSelectorData, UpdateRouterToolSelectorErrors, UpdateRouterToolSelectorResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen';
|
||||
|
||||
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
|
||||
/**
|
||||
@@ -478,9 +478,16 @@ export const getSession = <ThrowOnError extends boolean = false>(options: Option
|
||||
});
|
||||
};
|
||||
|
||||
export const updateSessionDescription = <ThrowOnError extends boolean = false>(options: Options<UpdateSessionDescriptionData, ThrowOnError>) => {
|
||||
return (options.client ?? client).put<UpdateSessionDescriptionResponses, UpdateSessionDescriptionErrors, ThrowOnError>({
|
||||
url: '/sessions/{session_id}/description',
|
||||
export const exportSession = <ThrowOnError extends boolean = false>(options: Options<ExportSessionData, ThrowOnError>) => {
|
||||
return (options.client ?? client).get<ExportSessionResponses, ExportSessionErrors, ThrowOnError>({
|
||||
url: '/sessions/{session_id}/export',
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
export const updateSessionName = <ThrowOnError extends boolean = false>(options: Options<UpdateSessionNameData, ThrowOnError>) => {
|
||||
return (options.client ?? client).put<UpdateSessionNameResponses, UpdateSessionNameErrors, ThrowOnError>({
|
||||
url: '/sessions/{session_id}/name',
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -489,13 +496,6 @@ export const updateSessionDescription = <ThrowOnError extends boolean = false>(o
|
||||
});
|
||||
};
|
||||
|
||||
export const exportSession = <ThrowOnError extends boolean = false>(options: Options<ExportSessionData, ThrowOnError>) => {
|
||||
return (options.client ?? client).get<ExportSessionResponses, ExportSessionErrors, ThrowOnError>({
|
||||
url: '/sessions/{session_id}/export',
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
export const updateSessionUserRecipeValues = <ThrowOnError extends boolean = false>(options: Options<UpdateSessionUserRecipeValuesData, ThrowOnError>) => {
|
||||
return (options.client ?? client).put<UpdateSessionUserRecipeValuesResponses, UpdateSessionUserRecipeValuesErrors, ThrowOnError>({
|
||||
url: '/sessions/{session_id}/user_recipe_values',
|
||||
|
||||
@@ -644,11 +644,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;
|
||||
@@ -657,6 +657,7 @@ export type Session = {
|
||||
user_recipe_values?: {
|
||||
[key: string]: string;
|
||||
} | null;
|
||||
user_set_name?: boolean;
|
||||
working_dir: string;
|
||||
};
|
||||
|
||||
@@ -839,11 +840,11 @@ export type UpdateScheduleRequest = {
|
||||
cron: string;
|
||||
};
|
||||
|
||||
export type UpdateSessionDescriptionRequest = {
|
||||
export type UpdateSessionNameRequest = {
|
||||
/**
|
||||
* Updated description (name) for the session (max 200 characters)
|
||||
* Updated name for the session (max 200 characters)
|
||||
*/
|
||||
description: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type UpdateSessionUserRecipeValuesRequest = {
|
||||
@@ -2282,44 +2283,6 @@ export type GetSessionResponses = {
|
||||
|
||||
export type GetSessionResponse = GetSessionResponses[keyof GetSessionResponses];
|
||||
|
||||
export type UpdateSessionDescriptionData = {
|
||||
body: UpdateSessionDescriptionRequest;
|
||||
path: {
|
||||
/**
|
||||
* Unique identifier for the session
|
||||
*/
|
||||
session_id: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/sessions/{session_id}/description';
|
||||
};
|
||||
|
||||
export type UpdateSessionDescriptionErrors = {
|
||||
/**
|
||||
* Bad request - Description too long (max 200 characters)
|
||||
*/
|
||||
400: unknown;
|
||||
/**
|
||||
* Unauthorized - Invalid or missing API key
|
||||
*/
|
||||
401: unknown;
|
||||
/**
|
||||
* Session not found
|
||||
*/
|
||||
404: unknown;
|
||||
/**
|
||||
* Internal server error
|
||||
*/
|
||||
500: unknown;
|
||||
};
|
||||
|
||||
export type UpdateSessionDescriptionResponses = {
|
||||
/**
|
||||
* Session description updated successfully
|
||||
*/
|
||||
200: unknown;
|
||||
};
|
||||
|
||||
export type ExportSessionData = {
|
||||
body?: never;
|
||||
path: {
|
||||
@@ -2356,6 +2319,44 @@ export type ExportSessionResponses = {
|
||||
|
||||
export type ExportSessionResponse = ExportSessionResponses[keyof ExportSessionResponses];
|
||||
|
||||
export type UpdateSessionNameData = {
|
||||
body: UpdateSessionNameRequest;
|
||||
path: {
|
||||
/**
|
||||
* Unique identifier for the session
|
||||
*/
|
||||
session_id: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/sessions/{session_id}/name';
|
||||
};
|
||||
|
||||
export type UpdateSessionNameErrors = {
|
||||
/**
|
||||
* Bad request - Name too long (max 200 characters)
|
||||
*/
|
||||
400: unknown;
|
||||
/**
|
||||
* Unauthorized - Invalid or missing API key
|
||||
*/
|
||||
401: unknown;
|
||||
/**
|
||||
* Session not found
|
||||
*/
|
||||
404: unknown;
|
||||
/**
|
||||
* Internal server error
|
||||
*/
|
||||
500: unknown;
|
||||
};
|
||||
|
||||
export type UpdateSessionNameResponses = {
|
||||
/**
|
||||
* Session name updated successfully
|
||||
*/
|
||||
200: unknown;
|
||||
};
|
||||
|
||||
export type UpdateSessionUserRecipeValuesData = {
|
||||
body: UpdateSessionUserRecipeValuesRequest;
|
||||
path: {
|
||||
|
||||
@@ -174,7 +174,7 @@ function BaseChatContent({
|
||||
messages,
|
||||
recipe,
|
||||
sessionId,
|
||||
title: session?.description || 'No Session',
|
||||
name: session?.name || 'No Session',
|
||||
};
|
||||
|
||||
const initialPrompt = messages.length == 0 && recipe?.prompt ? recipe.prompt : '';
|
||||
|
||||
@@ -116,16 +116,16 @@ const AppSidebar: React.FC<SidebarProps> = ({ currentPath }) => {
|
||||
|
||||
if (
|
||||
currentPath === '/pair' &&
|
||||
chatContext?.chat?.title &&
|
||||
chatContext.chat.title !== DEFAULT_CHAT_TITLE
|
||||
chatContext?.chat?.name &&
|
||||
chatContext.chat.name !== DEFAULT_CHAT_TITLE
|
||||
) {
|
||||
titleBits.push(chatContext.chat.title);
|
||||
titleBits.push(chatContext.chat.name);
|
||||
} else if (currentPath !== '/' && currentItem) {
|
||||
titleBits.push(currentItem.label);
|
||||
}
|
||||
|
||||
document.title = titleBits.join(' - ');
|
||||
}, [currentPath, chatContext?.chat?.title]);
|
||||
}, [currentPath, chatContext?.chat?.name]);
|
||||
|
||||
const isActivePath = (path: string) => {
|
||||
return currentPath === path;
|
||||
|
||||
@@ -182,7 +182,7 @@ const SessionHistoryView: React.FC<SessionHistoryViewProps> = ({
|
||||
config.baseUrl,
|
||||
session.working_dir,
|
||||
messages,
|
||||
session.description || 'Shared Session',
|
||||
session.name || 'Shared Session',
|
||||
session.total_tokens || 0
|
||||
);
|
||||
|
||||
@@ -267,7 +267,7 @@ const SessionHistoryView: React.FC<SessionHistoryViewProps> = ({
|
||||
<div className="flex-1 flex flex-col min-h-0 px-8">
|
||||
<SessionHeader
|
||||
onBack={onBack}
|
||||
title={session.description || 'Session Details'}
|
||||
title={session.name}
|
||||
actionButtons={!isLoading ? actionButtons : null}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
|
||||
@@ -12,7 +12,7 @@ const SessionItem: React.FC<SessionItemProps> = ({ session, extraActions }) => {
|
||||
return (
|
||||
<Card className="p-4 mb-2 hover:bg-accent/50 cursor-pointer flex justify-between items-center">
|
||||
<div>
|
||||
<div className="font-medium">{session.description || `Session ${session.id}`}</div>
|
||||
<div className="font-medium">{session.name}</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{formatDate(session.updated_at)} • {session.message_count} messages
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
importSession,
|
||||
listSessions,
|
||||
Session,
|
||||
updateSessionDescription,
|
||||
updateSessionName,
|
||||
} from '../../api';
|
||||
|
||||
interface EditSessionModalProps {
|
||||
@@ -45,7 +45,7 @@ const EditSessionModal = React.memo<EditSessionModalProps>(
|
||||
|
||||
useEffect(() => {
|
||||
if (session && isOpen) {
|
||||
setDescription(session.description || session.id);
|
||||
setDescription(session.name);
|
||||
} else if (!isOpen) {
|
||||
// Reset state when modal closes
|
||||
setDescription('');
|
||||
@@ -57,16 +57,16 @@ const EditSessionModal = React.memo<EditSessionModalProps>(
|
||||
if (!session || disabled) return;
|
||||
|
||||
const trimmedDescription = description.trim();
|
||||
if (trimmedDescription === session.description) {
|
||||
if (trimmedDescription === session.name) {
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
setIsUpdating(true);
|
||||
try {
|
||||
await updateSessionDescription({
|
||||
await updateSessionName({
|
||||
path: { session_id: session.id },
|
||||
body: { description: trimmedDescription },
|
||||
body: { name: trimmedDescription },
|
||||
throwOnError: true,
|
||||
});
|
||||
await onSave(session.id, trimmedDescription);
|
||||
@@ -80,7 +80,7 @@ const EditSessionModal = React.memo<EditSessionModalProps>(
|
||||
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.description || session.id);
|
||||
setDescription(session.name);
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
@@ -333,7 +333,7 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
|
||||
startTransition(() => {
|
||||
const searchTerm = caseSensitive ? debouncedSearchTerm : debouncedSearchTerm.toLowerCase();
|
||||
const filtered = sessions.filter((session) => {
|
||||
const description = session.description || session.id;
|
||||
const description = session.name;
|
||||
const workingDir = session.working_dir;
|
||||
const sessionId = session.id;
|
||||
|
||||
@@ -397,7 +397,7 @@ const SessionListView: React.FC<SessionListViewProps> = 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, description: newDescription } : s))
|
||||
prevSessions.map((s) => (s.id === sessionId ? { ...s, name: newDescription } : s))
|
||||
);
|
||||
}, []);
|
||||
|
||||
@@ -416,7 +416,7 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
|
||||
|
||||
setShowDeleteConfirmation(false);
|
||||
const sessionToDeleteId = sessionToDelete.id;
|
||||
const sessionName = sessionToDelete.description || sessionToDelete.id;
|
||||
const sessionName = sessionToDelete.name;
|
||||
setSessionToDelete(null);
|
||||
|
||||
try {
|
||||
@@ -451,7 +451,7 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${session.description || session.id}.json`;
|
||||
a.download = `${session.name}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
@@ -557,9 +557,7 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<h3 className="text-base mb-1 pr-16 break-words">
|
||||
{session.description || session.id}
|
||||
</h3>
|
||||
<h3 className="text-base mb-1 pr-16 break-words">{session.name}</h3>
|
||||
|
||||
<div className="flex items-center text-text-muted text-xs mb-1">
|
||||
<Calendar className="w-3 h-3 mr-1 flex-shrink-0" />
|
||||
@@ -806,7 +804,7 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
|
||||
<ConfirmationModal
|
||||
isOpen={showDeleteConfirmation}
|
||||
title="Delete Session"
|
||||
message={`Are you sure you want to delete the session "${sessionToDelete?.description || sessionToDelete?.id}"? This action cannot be undone.`}
|
||||
message={`Are you sure you want to delete the session "${sessionToDelete?.name}"? This action cannot be undone.`}
|
||||
confirmLabel="Delete Session"
|
||||
cancelLabel="Cancel"
|
||||
confirmVariant="destructive"
|
||||
|
||||
@@ -334,9 +334,7 @@ export function SessionInsights() {
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<ChatSmart className="h-4 w-4 text-text-muted" />
|
||||
<span className="truncate max-w-[300px]">
|
||||
{session.description || session.id}
|
||||
</span>
|
||||
<span className="truncate max-w-[300px]">{session.name}</span>
|
||||
</div>
|
||||
<span className="text-text-muted font-mono font-light">
|
||||
{formatDateOnly(session.updated_at)}
|
||||
|
||||
@@ -82,13 +82,14 @@ const SessionsView: React.FC = () => {
|
||||
selectedSession || {
|
||||
id: initialSessionId || '',
|
||||
conversation: [],
|
||||
description: 'Loading...',
|
||||
name: 'Loading...',
|
||||
working_dir: '',
|
||||
message_count: 0,
|
||||
total_tokens: 0,
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
extension_data: {},
|
||||
user_set_name: false,
|
||||
}
|
||||
}
|
||||
isLoading={isLoadingSession}
|
||||
|
||||
@@ -55,7 +55,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
||||
const resetChat = () => {
|
||||
setChat({
|
||||
sessionId: '',
|
||||
title: DEFAULT_CHAT_TITLE,
|
||||
name: DEFAULT_CHAT_TITLE,
|
||||
messages: [],
|
||||
messageHistoryIndex: 0,
|
||||
recipe: null,
|
||||
|
||||
@@ -82,7 +82,7 @@ export function useAgent(): UseAgentReturn {
|
||||
const messages = agentSession.conversation || [];
|
||||
return {
|
||||
sessionId: agentSession.id,
|
||||
title: agentSession.recipe?.title || agentSession.description,
|
||||
name: agentSession.recipe?.title || agentSession.name,
|
||||
messageHistoryIndex: 0,
|
||||
messages,
|
||||
recipe: agentSession.recipe,
|
||||
@@ -184,7 +184,7 @@ export function useAgent(): UseAgentReturn {
|
||||
const messages = initContext.recipe && !initContext.resumeSessionId ? [] : conversation;
|
||||
let initChat: ChatType = {
|
||||
sessionId: agentSession.id,
|
||||
title: agentSession.recipe?.title || agentSession.description,
|
||||
name: agentSession.recipe?.title || agentSession.name,
|
||||
messageHistoryIndex: 0,
|
||||
messages: messages,
|
||||
recipe: recipe,
|
||||
|
||||
@@ -131,7 +131,7 @@ describe('useChatEngine', () => {
|
||||
const mockChat: ChatType = {
|
||||
sessionId: 'test-chat',
|
||||
messages: initialMessages,
|
||||
title: 'Test Chat',
|
||||
name: 'Test Chat',
|
||||
messageHistoryIndex: 0,
|
||||
};
|
||||
|
||||
|
||||
@@ -288,7 +288,7 @@ export function useChatStream({
|
||||
const session = response.data;
|
||||
log.session('loaded', sessionId, {
|
||||
messageCount: session?.conversation?.length || 0,
|
||||
description: session?.description,
|
||||
name: session?.name,
|
||||
});
|
||||
|
||||
setSession(session);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Message } from '../api';
|
||||
|
||||
export interface ChatType {
|
||||
sessionId: string;
|
||||
title: string;
|
||||
name: string;
|
||||
messageHistoryIndex: number;
|
||||
messages: Message[];
|
||||
recipe?: Recipe | null; // Add recipe configuration to chat state
|
||||
|
||||
Reference in New Issue
Block a user