Add session forking capability (#5882)
Co-authored-by: Zane Staggs <zane@squareup.com>
This commit is contained in:
+84
-45
@@ -322,15 +322,37 @@ async fn get_or_create_session_id(
|
|||||||
|
|
||||||
let session_manager = SessionManager::instance();
|
let session_manager = SessionManager::instance();
|
||||||
|
|
||||||
let Some(id) = identifier else {
|
let resolved_id = if resume {
|
||||||
return if resume {
|
let Some(id) = identifier else {
|
||||||
let sessions = session_manager.list_sessions().await?;
|
let sessions = session_manager.list_sessions().await?;
|
||||||
let session_id = sessions
|
let session_id = sessions
|
||||||
.first()
|
.first()
|
||||||
.map(|s| s.id.clone())
|
.map(|s| s.id.clone())
|
||||||
.ok_or_else(|| anyhow::anyhow!("No session found to resume"))?;
|
.ok_or_else(|| anyhow::anyhow!("No session found to resume"))?;
|
||||||
Ok(Some(session_id))
|
return Ok(Some(session_id));
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(session_id) = id.session_id {
|
||||||
|
session_id
|
||||||
|
} else if let Some(name) = id.name {
|
||||||
|
let sessions = session_manager.list_sessions().await?;
|
||||||
|
sessions
|
||||||
|
.into_iter()
|
||||||
|
.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) = id.path {
|
||||||
|
path.file_stem()
|
||||||
|
.and_then(|s| s.to_str())
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.ok_or_else(|| {
|
||||||
|
anyhow::anyhow!("Could not extract session ID from path: {:?}", path)
|
||||||
|
})?
|
||||||
} else {
|
} else {
|
||||||
|
return Err(anyhow::anyhow!("Invalid identifier"));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let Some(id) = identifier else {
|
||||||
let session = session_manager
|
let session = session_manager
|
||||||
.create_session(
|
.create_session(
|
||||||
std::env::current_dir()?,
|
std::env::current_dir()?,
|
||||||
@@ -338,58 +360,39 @@ async fn get_or_create_session_id(
|
|||||||
SessionType::User,
|
SessionType::User,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Some(session.id))
|
return Ok(Some(session.id));
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(session_id) = id.session_id {
|
if id.session_id.is_some() {
|
||||||
Ok(Some(session_id))
|
return Err(anyhow::anyhow!("Cannot use --session-id without --resume"));
|
||||||
} else if let Some(name) = id.name {
|
}
|
||||||
if resume {
|
|
||||||
let sessions = session_manager.list_sessions().await?;
|
|
||||||
let session_id = sessions
|
|
||||||
.into_iter()
|
|
||||||
.find(|s| s.name == name || s.id == name)
|
|
||||||
.map(|s| s.id)
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("No session found with name '{}'", name))?;
|
|
||||||
Ok(Some(session_id))
|
|
||||||
} else {
|
|
||||||
let session = session_manager
|
|
||||||
.create_session(std::env::current_dir()?, name.clone(), SessionType::User)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
|
let has_user_provided_name = id.name.is_some();
|
||||||
|
let name = id.name.unwrap_or_else(|| "CLI Session".to_string());
|
||||||
|
let session = session_manager
|
||||||
|
.create_session(std::env::current_dir()?, name.clone(), SessionType::User)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if has_user_provided_name {
|
||||||
session_manager
|
session_manager
|
||||||
.update(&session.id)
|
.update(&session.id)
|
||||||
.user_provided_name(name)
|
.user_provided_name(name)
|
||||||
.apply()
|
.apply()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(Some(session.id))
|
|
||||||
}
|
}
|
||||||
} else if let Some(path) = id.path {
|
|
||||||
let session_id = path
|
return Ok(Some(session.id));
|
||||||
.file_stem()
|
};
|
||||||
.and_then(|s| s.to_str())
|
|
||||||
.map(|s| s.to_string())
|
Ok(Some(resolved_id))
|
||||||
.ok_or_else(|| anyhow::anyhow!("Could not extract session ID from path: {:?}", path))?;
|
|
||||||
Ok(Some(session_id))
|
|
||||||
} else {
|
|
||||||
let session = session_manager
|
|
||||||
.create_session(
|
|
||||||
std::env::current_dir()?,
|
|
||||||
"CLI Session".to_string(),
|
|
||||||
SessionType::User,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(Some(session.id))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn lookup_session_id(identifier: Identifier) -> Result<String> {
|
async fn lookup_session_id(identifier: Identifier) -> Result<String> {
|
||||||
|
let session_manager = SessionManager::instance();
|
||||||
|
|
||||||
if let Some(session_id) = identifier.session_id {
|
if let Some(session_id) = identifier.session_id {
|
||||||
Ok(session_id)
|
Ok(session_id)
|
||||||
} else if let Some(name) = identifier.name {
|
} else if let Some(name) = identifier.name {
|
||||||
let session_manager = SessionManager::instance();
|
|
||||||
let sessions = session_manager.list_sessions().await?;
|
let sessions = session_manager.list_sessions().await?;
|
||||||
sessions
|
sessions
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -722,6 +725,15 @@ enum Command {
|
|||||||
)]
|
)]
|
||||||
resume: bool,
|
resume: bool,
|
||||||
|
|
||||||
|
/// Fork a previous session (creates new session with copied history)
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
requires = "resume",
|
||||||
|
help = "Fork a previous session (creates new session with copied history)",
|
||||||
|
long_help = "Create a new session by copying all messages from a previous session. Must be used with --resume. If --name or --session-id is provided, forks that specific session. Otherwise forks the most recently used session."
|
||||||
|
)]
|
||||||
|
fork: bool,
|
||||||
|
|
||||||
/// Show message history when resuming
|
/// Show message history when resuming
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
@@ -1047,6 +1059,7 @@ async fn handle_session_subcommand(command: SessionCommand) -> Result<()> {
|
|||||||
async fn handle_interactive_session(
|
async fn handle_interactive_session(
|
||||||
identifier: Option<Identifier>,
|
identifier: Option<Identifier>,
|
||||||
resume: bool,
|
resume: bool,
|
||||||
|
fork: bool,
|
||||||
history: bool,
|
history: bool,
|
||||||
session_opts: SessionOptions,
|
session_opts: SessionOptions,
|
||||||
extension_opts: ExtensionOptions,
|
extension_opts: ExtensionOptions,
|
||||||
@@ -1056,7 +1069,13 @@ async fn handle_interactive_session(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let session_start = std::time::Instant::now();
|
let session_start = std::time::Instant::now();
|
||||||
let session_type = if resume { "resumed" } else { "new" };
|
let session_type = if fork {
|
||||||
|
"forked"
|
||||||
|
} else if resume {
|
||||||
|
"resumed"
|
||||||
|
} else {
|
||||||
|
"new"
|
||||||
|
};
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
counter.goose.session_starts = 1,
|
counter.goose.session_starts = 1,
|
||||||
@@ -1076,11 +1095,21 @@ async fn handle_interactive_session(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let session_id = get_or_create_session_id(identifier, resume, false).await?;
|
let mut session_id = get_or_create_session_id(identifier, resume, false).await?;
|
||||||
|
|
||||||
|
if fork {
|
||||||
|
if let Some(id) = session_id {
|
||||||
|
let session_manager = SessionManager::instance();
|
||||||
|
let original = session_manager.get_session(&id, false).await?;
|
||||||
|
let copied = session_manager.copy_session(&id, original.name).await?;
|
||||||
|
session_id = Some(copied.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut session: crate::CliSession = build_session(SessionBuilderConfig {
|
let mut session: crate::CliSession = build_session(SessionBuilderConfig {
|
||||||
session_id,
|
session_id,
|
||||||
resume,
|
resume,
|
||||||
|
fork,
|
||||||
no_session: false,
|
no_session: false,
|
||||||
extensions: extension_opts.extensions,
|
extensions: extension_opts.extensions,
|
||||||
streamable_http_extensions: extension_opts.streamable_http_extensions,
|
streamable_http_extensions: extension_opts.streamable_http_extensions,
|
||||||
@@ -1099,7 +1128,7 @@ async fn handle_interactive_session(
|
|||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
if resume && history {
|
if (resume || fork) && history {
|
||||||
session.render_message_history();
|
session.render_message_history();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1283,6 +1312,7 @@ async fn handle_run_command(
|
|||||||
let mut session = build_session(SessionBuilderConfig {
|
let mut session = build_session(SessionBuilderConfig {
|
||||||
session_id,
|
session_id,
|
||||||
resume: run_behavior.resume,
|
resume: run_behavior.resume,
|
||||||
|
fork: false,
|
||||||
no_session: run_behavior.no_session,
|
no_session: run_behavior.no_session,
|
||||||
extensions: extension_opts.extensions,
|
extensions: extension_opts.extensions,
|
||||||
streamable_http_extensions: extension_opts.streamable_http_extensions,
|
streamable_http_extensions: extension_opts.streamable_http_extensions,
|
||||||
@@ -1407,6 +1437,7 @@ async fn handle_default_session() -> Result<()> {
|
|||||||
let mut session = build_session(SessionBuilderConfig {
|
let mut session = build_session(SessionBuilderConfig {
|
||||||
session_id,
|
session_id,
|
||||||
resume: false,
|
resume: false,
|
||||||
|
fork: false,
|
||||||
no_session: false,
|
no_session: false,
|
||||||
extensions: Vec::new(),
|
extensions: Vec::new(),
|
||||||
streamable_http_extensions: Vec::new(),
|
streamable_http_extensions: Vec::new(),
|
||||||
@@ -1458,12 +1489,20 @@ pub async fn cli() -> anyhow::Result<()> {
|
|||||||
command: None,
|
command: None,
|
||||||
identifier,
|
identifier,
|
||||||
resume,
|
resume,
|
||||||
|
fork,
|
||||||
history,
|
history,
|
||||||
session_opts,
|
session_opts,
|
||||||
extension_opts,
|
extension_opts,
|
||||||
}) => {
|
}) => {
|
||||||
handle_interactive_session(identifier, resume, history, session_opts, extension_opts)
|
handle_interactive_session(
|
||||||
.await
|
identifier,
|
||||||
|
resume,
|
||||||
|
fork,
|
||||||
|
history,
|
||||||
|
session_opts,
|
||||||
|
extension_opts,
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
Some(Command::Project {}) => {
|
Some(Command::Project {}) => {
|
||||||
handle_project_default()?;
|
handle_project_default()?;
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ pub async fn agent_generator(
|
|||||||
let base_session = build_session(SessionBuilderConfig {
|
let base_session = build_session(SessionBuilderConfig {
|
||||||
session_id: Some(session_id),
|
session_id: Some(session_id),
|
||||||
resume: false,
|
resume: false,
|
||||||
|
fork: false,
|
||||||
no_session: false,
|
no_session: false,
|
||||||
extensions: requirements.external,
|
extensions: requirements.external,
|
||||||
streamable_http_extensions: requirements.streamable_http,
|
streamable_http_extensions: requirements.streamable_http,
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ pub struct SessionBuilderConfig {
|
|||||||
pub session_id: Option<String>,
|
pub session_id: Option<String>,
|
||||||
/// Whether to resume an existing session
|
/// Whether to resume an existing session
|
||||||
pub resume: bool,
|
pub resume: bool,
|
||||||
|
/// Whether to fork an existing session (creates a copy of the original/existing session then resumes the copy)
|
||||||
|
pub fork: bool,
|
||||||
/// Whether to run without a session file
|
/// Whether to run without a session file
|
||||||
pub no_session: bool,
|
pub no_session: bool,
|
||||||
/// List of stdio extension commands to add
|
/// List of stdio extension commands to add
|
||||||
@@ -121,6 +123,7 @@ impl Default for SessionBuilderConfig {
|
|||||||
SessionBuilderConfig {
|
SessionBuilderConfig {
|
||||||
session_id: None,
|
session_id: None,
|
||||||
resume: false,
|
resume: false,
|
||||||
|
fork: false,
|
||||||
no_session: false,
|
no_session: false,
|
||||||
extensions: Vec::new(),
|
extensions: Vec::new(),
|
||||||
streamable_http_extensions: Vec::new(),
|
streamable_http_extensions: Vec::new(),
|
||||||
@@ -659,6 +662,7 @@ mod tests {
|
|||||||
let config = SessionBuilderConfig {
|
let config = SessionBuilderConfig {
|
||||||
session_id: None,
|
session_id: None,
|
||||||
resume: false,
|
resume: false,
|
||||||
|
fork: false,
|
||||||
no_session: false,
|
no_session: false,
|
||||||
extensions: vec!["echo test".to_string()],
|
extensions: vec!["echo test".to_string()],
|
||||||
streamable_http_extensions: vec!["http://localhost:8080/mcp".to_string()],
|
streamable_http_extensions: vec!["http://localhost:8080/mcp".to_string()],
|
||||||
@@ -705,6 +709,7 @@ mod tests {
|
|||||||
assert!(config.scheduled_job_id.is_none());
|
assert!(config.scheduled_job_id.is_none());
|
||||||
assert!(!config.interactive);
|
assert!(!config.interactive);
|
||||||
assert!(!config.quiet);
|
assert!(!config.quiet);
|
||||||
|
assert!(!config.fork);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -380,7 +380,7 @@ derive_utoipa!(Icon as IconSchema);
|
|||||||
super::routes::session::export_session,
|
super::routes::session::export_session,
|
||||||
super::routes::session::import_session,
|
super::routes::session::import_session,
|
||||||
super::routes::session::update_session_user_recipe_values,
|
super::routes::session::update_session_user_recipe_values,
|
||||||
super::routes::session::edit_message,
|
super::routes::session::fork_session,
|
||||||
super::routes::session::get_session_extensions,
|
super::routes::session::get_session_extensions,
|
||||||
super::routes::schedule::create_schedule,
|
super::routes::schedule::create_schedule,
|
||||||
super::routes::schedule::list_schedules,
|
super::routes::schedule::list_schedules,
|
||||||
@@ -442,9 +442,8 @@ derive_utoipa!(Icon as IconSchema);
|
|||||||
super::routes::session::UpdateSessionNameRequest,
|
super::routes::session::UpdateSessionNameRequest,
|
||||||
super::routes::session::UpdateSessionUserRecipeValuesRequest,
|
super::routes::session::UpdateSessionUserRecipeValuesRequest,
|
||||||
super::routes::session::UpdateSessionUserRecipeValuesResponse,
|
super::routes::session::UpdateSessionUserRecipeValuesResponse,
|
||||||
super::routes::session::EditType,
|
super::routes::session::ForkRequest,
|
||||||
super::routes::session::EditMessageRequest,
|
super::routes::session::ForkResponse,
|
||||||
super::routes::session::EditMessageResponse,
|
|
||||||
super::routes::session::SessionExtensionsResponse,
|
super::routes::session::SessionExtensionsResponse,
|
||||||
Message,
|
Message,
|
||||||
MessageContent,
|
MessageContent,
|
||||||
|
|||||||
@@ -51,28 +51,17 @@ pub struct ImportSessionRequest {
|
|||||||
json: String,
|
json: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, ToSchema)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
|
||||||
pub enum EditType {
|
|
||||||
Fork,
|
|
||||||
Edit,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, ToSchema)]
|
#[derive(Deserialize, ToSchema)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct EditMessageRequest {
|
pub struct ForkRequest {
|
||||||
timestamp: i64,
|
timestamp: Option<i64>,
|
||||||
#[serde(default = "default_edit_type")]
|
truncate: bool,
|
||||||
edit_type: EditType,
|
copy: bool,
|
||||||
}
|
|
||||||
|
|
||||||
fn default_edit_type() -> EditType {
|
|
||||||
EditType::Fork
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, ToSchema)]
|
#[derive(Serialize, ToSchema)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct EditMessageResponse {
|
pub struct ForkResponse {
|
||||||
session_id: String,
|
session_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,16 +358,16 @@ async fn import_session(
|
|||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
post,
|
post,
|
||||||
path = "/sessions/{session_id}/edit_message",
|
path = "/sessions/{session_id}/fork",
|
||||||
request_body = EditMessageRequest,
|
request_body = ForkRequest,
|
||||||
params(
|
params(
|
||||||
("session_id" = String, Path, description = "Unique identifier for the session")
|
("session_id" = String, Path, description = "Unique identifier for the session")
|
||||||
),
|
),
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Session prepared for editing - frontend should submit the edited message", body = EditMessageResponse),
|
(status = 200, description = "Session forked successfully", body = ForkResponse),
|
||||||
(status = 400, description = "Bad request - Invalid message timestamp"),
|
(status = 400, description = "Bad request - truncate=true requires timestamp"),
|
||||||
(status = 401, description = "Unauthorized - Invalid or missing API key"),
|
(status = 401, description = "Unauthorized - Invalid or missing API key"),
|
||||||
(status = 404, description = "Session or message not found"),
|
(status = 404, description = "Session not found"),
|
||||||
(status = 500, description = "Internal server error")
|
(status = 500, description = "Internal server error")
|
||||||
),
|
),
|
||||||
security(
|
security(
|
||||||
@@ -386,51 +375,75 @@ async fn import_session(
|
|||||||
),
|
),
|
||||||
tag = "Session Management"
|
tag = "Session Management"
|
||||||
)]
|
)]
|
||||||
async fn edit_message(
|
async fn fork_session(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(session_id): Path<String>,
|
Path(session_id): Path<String>,
|
||||||
Json(request): Json<EditMessageRequest>,
|
Json(request): Json<ForkRequest>,
|
||||||
) -> Result<Json<EditMessageResponse>, StatusCode> {
|
) -> Result<Json<ForkResponse>, ErrorResponse> {
|
||||||
let manager = state.session_manager();
|
if request.truncate && request.timestamp.is_none() {
|
||||||
match request.edit_type {
|
return Err(ErrorResponse {
|
||||||
EditType::Fork => {
|
message: "truncate=true requires a timestamp".to_string(),
|
||||||
let new_session = manager
|
status: StatusCode::BAD_REQUEST,
|
||||||
.copy_session(&session_id, "(edited)".to_string())
|
});
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
tracing::error!("Failed to copy session: {}", e);
|
|
||||||
goose::posthog::emit_error("session_copy_failed", &e.to_string());
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR
|
|
||||||
})?;
|
|
||||||
|
|
||||||
manager
|
|
||||||
.truncate_conversation(&new_session.id, request.timestamp)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
tracing::error!("Failed to truncate conversation: {}", e);
|
|
||||||
goose::posthog::emit_error("session_truncate_failed", &e.to_string());
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(Json(EditMessageResponse {
|
|
||||||
session_id: new_session.id,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
EditType::Edit => {
|
|
||||||
manager
|
|
||||||
.truncate_conversation(&session_id, request.timestamp)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
tracing::error!("Failed to truncate conversation: {}", e);
|
|
||||||
goose::posthog::emit_error("session_truncate_failed", &e.to_string());
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(Json(EditMessageResponse {
|
|
||||||
session_id: session_id.clone(),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let session_manager = state.session_manager();
|
||||||
|
|
||||||
|
let target_session_id = if request.copy {
|
||||||
|
let original = session_manager
|
||||||
|
.get_session(&session_id, false)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("Failed to get session: {}", e);
|
||||||
|
goose::posthog::emit_error("session_get_failed", &e.to_string());
|
||||||
|
ErrorResponse {
|
||||||
|
message: if e.to_string().contains("not found") {
|
||||||
|
format!("Session {} not found", session_id)
|
||||||
|
} else {
|
||||||
|
format!("Failed to get session: {}", e)
|
||||||
|
},
|
||||||
|
status: if e.to_string().contains("not found") {
|
||||||
|
StatusCode::NOT_FOUND
|
||||||
|
} else {
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let copied = session_manager
|
||||||
|
.copy_session(&session_id, original.name)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("Failed to copy session: {}", e);
|
||||||
|
goose::posthog::emit_error("session_copy_failed", &e.to_string());
|
||||||
|
ErrorResponse {
|
||||||
|
message: format!("Failed to copy session: {}", e),
|
||||||
|
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
copied.id
|
||||||
|
} else {
|
||||||
|
session_id.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
if request.truncate {
|
||||||
|
session_manager
|
||||||
|
.truncate_conversation(&target_session_id, request.timestamp.unwrap_or(0))
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("Failed to truncate conversation: {}", e);
|
||||||
|
goose::posthog::emit_error("session_truncate_failed", &e.to_string());
|
||||||
|
ErrorResponse {
|
||||||
|
message: format!("Failed to truncate conversation: {}", e),
|
||||||
|
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Json(ForkResponse {
|
||||||
|
session_id: target_session_id,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, ToSchema)]
|
#[derive(Serialize, ToSchema)]
|
||||||
@@ -487,7 +500,7 @@ pub fn routes(state: Arc<AppState>) -> Router {
|
|||||||
"/sessions/{session_id}/user_recipe_values",
|
"/sessions/{session_id}/user_recipe_values",
|
||||||
put(update_session_user_recipe_values),
|
put(update_session_user_recipe_values),
|
||||||
)
|
)
|
||||||
.route("/sessions/{session_id}/edit_message", post(edit_message))
|
.route("/sessions/{session_id}/fork", post(fork_session))
|
||||||
.route(
|
.route(
|
||||||
"/sessions/{session_id}/extensions",
|
"/sessions/{session_id}/extensions",
|
||||||
get(get_session_extensions),
|
get(get_session_extensions),
|
||||||
|
|||||||
@@ -1285,14 +1285,23 @@ impl SessionStorage {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
session_manager
|
let mut builder = session_manager
|
||||||
.update(&new_session.id)
|
.update(&new_session.id)
|
||||||
.extension_data(original_session.extension_data)
|
.extension_data(original_session.extension_data)
|
||||||
.schedule_id(original_session.schedule_id)
|
.schedule_id(original_session.schedule_id)
|
||||||
.recipe(original_session.recipe)
|
.recipe(original_session.recipe)
|
||||||
.user_recipe_values(original_session.user_recipe_values)
|
.user_recipe_values(original_session.user_recipe_values);
|
||||||
.apply()
|
|
||||||
.await?;
|
// Preserve provider and model config from original session
|
||||||
|
if let Some(provider_name) = original_session.provider_name {
|
||||||
|
builder = builder.provider_name(provider_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(model_config) = original_session.model_config {
|
||||||
|
builder = builder.model_config(model_config);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.apply().await?;
|
||||||
|
|
||||||
if let Some(conversation) = original_session.conversation {
|
if let Some(conversation) = original_session.conversation {
|
||||||
self.replace_conversation(&new_session.id, &conversation)
|
self.replace_conversation(&new_session.id, &conversation)
|
||||||
|
|||||||
+89
-91
@@ -2472,64 +2472,6 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/sessions/{session_id}/edit_message": {
|
|
||||||
"post": {
|
|
||||||
"tags": [
|
|
||||||
"Session Management"
|
|
||||||
],
|
|
||||||
"operationId": "edit_message",
|
|
||||||
"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/EditMessageRequest"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Session prepared for editing - frontend should submit the edited message",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/EditMessageResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Bad request - Invalid message timestamp"
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Unauthorized - Invalid or missing API key"
|
|
||||||
},
|
|
||||||
"404": {
|
|
||||||
"description": "Session or message not found"
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal server error"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"api_key": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/sessions/{session_id}/export": {
|
"/sessions/{session_id}/export": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -2620,6 +2562,64 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/sessions/{session_id}/fork": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Session Management"
|
||||||
|
],
|
||||||
|
"operationId": "fork_session",
|
||||||
|
"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/ForkRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Session forked successfully",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ForkResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad request - truncate=true requires timestamp"
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized - Invalid or missing API key"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Session not found"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/sessions/{session_id}/name": {
|
"/sessions/{session_id}/name": {
|
||||||
"put": {
|
"put": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -3453,39 +3453,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"EditMessageRequest": {
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"timestamp"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"editType": {
|
|
||||||
"$ref": "#/components/schemas/EditType"
|
|
||||||
},
|
|
||||||
"timestamp": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int64"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"EditMessageResponse": {
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"sessionId"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"sessionId": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"EditType": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"fork",
|
|
||||||
"edit"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"EmbeddedResource": {
|
"EmbeddedResource": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
@@ -3961,6 +3928,37 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ForkRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"truncate",
|
||||||
|
"copy"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"copy": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"truncate": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ForkResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"sessionId"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"sessionId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"FrontendToolRequest": {
|
"FrontendToolRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -189,17 +189,6 @@ export type DetectProviderResponse = {
|
|||||||
provider_name: string;
|
provider_name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EditMessageRequest = {
|
|
||||||
editType?: EditType;
|
|
||||||
timestamp: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type EditMessageResponse = {
|
|
||||||
sessionId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type EditType = 'fork' | 'edit';
|
|
||||||
|
|
||||||
export type EmbeddedResource = {
|
export type EmbeddedResource = {
|
||||||
_meta?: {
|
_meta?: {
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
@@ -352,6 +341,16 @@ export type ExtensionResponse = {
|
|||||||
warnings?: Array<string>;
|
warnings?: Array<string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ForkRequest = {
|
||||||
|
copy: boolean;
|
||||||
|
timestamp?: number | null;
|
||||||
|
truncate: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ForkResponse = {
|
||||||
|
sessionId: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type FrontendToolRequest = {
|
export type FrontendToolRequest = {
|
||||||
id: string;
|
id: string;
|
||||||
toolCall: {
|
toolCall: {
|
||||||
@@ -3178,46 +3177,6 @@ export type GetSessionResponses = {
|
|||||||
|
|
||||||
export type GetSessionResponse = GetSessionResponses[keyof GetSessionResponses];
|
export type GetSessionResponse = GetSessionResponses[keyof GetSessionResponses];
|
||||||
|
|
||||||
export type EditMessageData = {
|
|
||||||
body: EditMessageRequest;
|
|
||||||
path: {
|
|
||||||
/**
|
|
||||||
* Unique identifier for the session
|
|
||||||
*/
|
|
||||||
session_id: string;
|
|
||||||
};
|
|
||||||
query?: never;
|
|
||||||
url: '/sessions/{session_id}/edit_message';
|
|
||||||
};
|
|
||||||
|
|
||||||
export type EditMessageErrors = {
|
|
||||||
/**
|
|
||||||
* Bad request - Invalid message timestamp
|
|
||||||
*/
|
|
||||||
400: unknown;
|
|
||||||
/**
|
|
||||||
* Unauthorized - Invalid or missing API key
|
|
||||||
*/
|
|
||||||
401: unknown;
|
|
||||||
/**
|
|
||||||
* Session or message not found
|
|
||||||
*/
|
|
||||||
404: unknown;
|
|
||||||
/**
|
|
||||||
* Internal server error
|
|
||||||
*/
|
|
||||||
500: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type EditMessageResponses = {
|
|
||||||
/**
|
|
||||||
* Session prepared for editing - frontend should submit the edited message
|
|
||||||
*/
|
|
||||||
200: EditMessageResponse;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type EditMessageResponse2 = EditMessageResponses[keyof EditMessageResponses];
|
|
||||||
|
|
||||||
export type ExportSessionData = {
|
export type ExportSessionData = {
|
||||||
body?: never;
|
body?: never;
|
||||||
path: {
|
path: {
|
||||||
@@ -3290,6 +3249,46 @@ export type GetSessionExtensionsResponses = {
|
|||||||
|
|
||||||
export type GetSessionExtensionsResponse = GetSessionExtensionsResponses[keyof GetSessionExtensionsResponses];
|
export type GetSessionExtensionsResponse = GetSessionExtensionsResponses[keyof GetSessionExtensionsResponses];
|
||||||
|
|
||||||
|
export type ForkSessionData = {
|
||||||
|
body: ForkRequest;
|
||||||
|
path: {
|
||||||
|
/**
|
||||||
|
* Unique identifier for the session
|
||||||
|
*/
|
||||||
|
session_id: string;
|
||||||
|
};
|
||||||
|
query?: never;
|
||||||
|
url: '/sessions/{session_id}/fork';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ForkSessionErrors = {
|
||||||
|
/**
|
||||||
|
* Bad request - truncate=true requires timestamp
|
||||||
|
*/
|
||||||
|
400: unknown;
|
||||||
|
/**
|
||||||
|
* Unauthorized - Invalid or missing API key
|
||||||
|
*/
|
||||||
|
401: unknown;
|
||||||
|
/**
|
||||||
|
* Session not found
|
||||||
|
*/
|
||||||
|
404: unknown;
|
||||||
|
/**
|
||||||
|
* Internal server error
|
||||||
|
*/
|
||||||
|
500: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ForkSessionResponses = {
|
||||||
|
/**
|
||||||
|
* Session forked successfully
|
||||||
|
*/
|
||||||
|
200: ForkResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ForkSessionResponse = ForkSessionResponses[keyof ForkSessionResponses];
|
||||||
|
|
||||||
export type UpdateSessionNameData = {
|
export type UpdateSessionNameData = {
|
||||||
body: UpdateSessionNameRequest;
|
body: UpdateSessionNameRequest;
|
||||||
path: {
|
path: {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { AppEvents } from '../constants/events';
|
import { AppEvents } from '../constants/events';
|
||||||
import React, { useRef, useState, useEffect, useMemo, useCallback } from 'react';
|
import React, { useRef, useState, useEffect, useMemo, useCallback } from 'react';
|
||||||
import { Bug, ScrollText, ChefHat } from 'lucide-react';
|
import { Bug, ChefHat, ScrollText } from 'lucide-react';
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/Tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/Tooltip';
|
||||||
import { Button } from './ui/button';
|
import { Button } from './ui/button';
|
||||||
import type { View } from '../utils/navigationUtils';
|
import type { View } from '../utils/navigationUtils';
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
Download,
|
Download,
|
||||||
Upload,
|
Upload,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
|
Copy,
|
||||||
Puzzle,
|
Puzzle,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Card } from '../ui/card';
|
import { Card } from '../ui/card';
|
||||||
@@ -28,6 +29,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/
|
|||||||
import {
|
import {
|
||||||
deleteSession,
|
deleteSession,
|
||||||
exportSession,
|
exportSession,
|
||||||
|
forkSession,
|
||||||
importSession,
|
importSession,
|
||||||
listSessions,
|
listSessions,
|
||||||
Session,
|
Session,
|
||||||
@@ -436,6 +438,25 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
|
|||||||
setShowDeleteConfirmation(true);
|
setShowDeleteConfirmation(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleDuplicateSession = useCallback(
|
||||||
|
async (session: Session) => {
|
||||||
|
try {
|
||||||
|
await forkSession({
|
||||||
|
path: { session_id: session.id },
|
||||||
|
body: { truncate: false, copy: true },
|
||||||
|
throwOnError: true,
|
||||||
|
});
|
||||||
|
toast.success(`Session "${session.name}" duplicated successfully`);
|
||||||
|
await loadSessions();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error duplicating session:', error);
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
toast.error(`Failed to duplicate session: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[loadSessions]
|
||||||
|
);
|
||||||
|
|
||||||
const handleConfirmDelete = useCallback(async () => {
|
const handleConfirmDelete = useCallback(async () => {
|
||||||
if (!sessionToDelete) return;
|
if (!sessionToDelete) return;
|
||||||
|
|
||||||
@@ -530,27 +551,37 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
|
|||||||
const SessionItem = React.memo(function SessionItem({
|
const SessionItem = React.memo(function SessionItem({
|
||||||
session,
|
session,
|
||||||
onEditClick,
|
onEditClick,
|
||||||
|
onDuplicateClick,
|
||||||
onDeleteClick,
|
onDeleteClick,
|
||||||
onExportClick,
|
onExportClick,
|
||||||
onOpenInNewWindow,
|
onOpenInNewWindow,
|
||||||
}: {
|
}: {
|
||||||
session: Session;
|
session: Session;
|
||||||
onEditClick: (session: Session) => void;
|
onEditClick: (session: Session) => void;
|
||||||
|
onDuplicateClick: (session: Session) => void;
|
||||||
onDeleteClick: (session: Session) => void;
|
onDeleteClick: (session: Session) => void;
|
||||||
onExportClick: (session: Session, e: React.MouseEvent) => void;
|
onExportClick: (session: Session, e: React.MouseEvent) => void;
|
||||||
onOpenInNewWindow: (session: Session, e: React.MouseEvent) => void;
|
onOpenInNewWindow: (session: Session, e: React.MouseEvent) => void;
|
||||||
}) {
|
}) {
|
||||||
const handleEditClick = useCallback(
|
const handleEditClick = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
e.stopPropagation(); // Prevent card click
|
e.stopPropagation();
|
||||||
onEditClick(session);
|
onEditClick(session);
|
||||||
},
|
},
|
||||||
[onEditClick, session]
|
[onEditClick, session]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleDuplicateClick = useCallback(
|
||||||
|
(e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDuplicateClick(session);
|
||||||
|
},
|
||||||
|
[onDuplicateClick, session]
|
||||||
|
);
|
||||||
|
|
||||||
const handleDeleteClick = useCallback(
|
const handleDeleteClick = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
e.stopPropagation(); // Prevent card click
|
e.stopPropagation();
|
||||||
onDeleteClick(session);
|
onDeleteClick(session);
|
||||||
},
|
},
|
||||||
[onDeleteClick, session]
|
[onDeleteClick, session]
|
||||||
@@ -605,6 +636,13 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
|
|||||||
>
|
>
|
||||||
<Edit2 className="w-3 h-3 text-textSubtle hover:text-textStandard" />
|
<Edit2 className="w-3 h-3 text-textSubtle hover:text-textStandard" />
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleDuplicateClick}
|
||||||
|
className="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer"
|
||||||
|
title="Duplicate session"
|
||||||
|
>
|
||||||
|
<Copy className="w-3 h-3 text-textSubtle hover:text-textStandard" />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleDeleteClick}
|
onClick={handleDeleteClick}
|
||||||
className="p-2 rounded hover:bg-red-50 dark:hover:bg-red-900/20 cursor-pointer transition-colors"
|
className="p-2 rounded hover:bg-red-50 dark:hover:bg-red-900/20 cursor-pointer transition-colors"
|
||||||
@@ -757,6 +795,7 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
|
|||||||
key={session.id}
|
key={session.id}
|
||||||
session={session}
|
session={session}
|
||||||
onEditClick={handleEditSession}
|
onEditClick={handleEditSession}
|
||||||
|
onDuplicateClick={handleDuplicateSession}
|
||||||
onDeleteClick={handleDeleteSession}
|
onDeleteClick={handleDeleteSession}
|
||||||
onExportClick={handleExportSession}
|
onExportClick={handleExportSession}
|
||||||
onOpenInNewWindow={handleOpenInNewWindow}
|
onOpenInNewWindow={handleOpenInNewWindow}
|
||||||
|
|||||||
@@ -650,27 +650,28 @@ export function useChatStream({
|
|||||||
const currentState = stateRef.current;
|
const currentState = stateRef.current;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { editMessage } = await import('../api');
|
const { forkSession } = await import('../api');
|
||||||
const message = currentState.messages.find((m) => m.id === messageId);
|
const message = currentState.messages.find((m) => m.id === messageId);
|
||||||
|
|
||||||
if (!message) {
|
if (!message) {
|
||||||
throw new Error(`Message with id ${messageId} not found in current messages`);
|
throw new Error(`Message with id ${messageId} not found in current messages`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await editMessage({
|
const response = await forkSession({
|
||||||
path: {
|
path: {
|
||||||
session_id: sessionId,
|
session_id: sessionId,
|
||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
timestamp: message.created,
|
timestamp: message.created,
|
||||||
editType,
|
truncate: true,
|
||||||
|
copy: editType === 'fork',
|
||||||
},
|
},
|
||||||
throwOnError: true,
|
throwOnError: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const targetSessionId = response.data?.sessionId;
|
const targetSessionId = response.data?.sessionId;
|
||||||
if (!targetSessionId) {
|
if (!targetSessionId) {
|
||||||
throw new Error('No session ID returned from edit_message');
|
throw new Error('No session ID returned from fork');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (editType === 'fork') {
|
if (editType === 'fork') {
|
||||||
|
|||||||
Reference in New Issue
Block a user