From be91189afbd6ff49bc95452aac2b10c8cc510b66 Mon Sep 17 00:00:00 2001 From: Lifei Zhou Date: Thu, 11 Jun 2026 07:42:54 +1000 Subject: [PATCH] feat: use acp search session in Desktop (#9717) --- crates/goose-server/src/openapi.rs | 11 +- crates/goose-server/src/routes/session.rs | 250 +------------- crates/goose-server/src/session_event_bus.rs | 8 - crates/goose-server/src/state.rs | 6 - .../goose/src/session/chat_history_search.rs | 134 -------- crates/goose/src/session/session_manager.rs | 182 ----------- ui/desktop/openapi.json | 309 ------------------ ui/desktop/src/acp/sessions.ts | 24 +- ui/desktop/src/api/index.ts | 4 +- ui/desktop/src/api/sdk.gen.ts | 21 +- ui/desktop/src/api/types.gen.ts | 219 ------------- .../src/components/conversation/SearchBar.tsx | 90 ++--- .../components/conversation/SearchView.tsx | 20 +- .../components/sessions/SessionListView.tsx | 261 ++++++--------- ui/desktop/tests/integration/goosed.test.ts | 9 - 15 files changed, 195 insertions(+), 1353 deletions(-) diff --git a/crates/goose-server/src/openapi.rs b/crates/goose-server/src/openapi.rs index b4973f9aa..278244898 100644 --- a/crates/goose-server/src/openapi.rs +++ b/crates/goose-server/src/openapi.rs @@ -8,7 +8,7 @@ use goose::download_manager::{DownloadProgress, DownloadStatus}; use goose::model::ModelConfig; use goose::permission::permission_confirmation::{Permission, PrincipalType}; use goose::providers::base::{ConfigKey, ModelInfo, ProviderMetadata, ProviderType}; -use goose::session::{Session, SessionInsights, SessionType, SystemInfo}; +use goose::session::{Session, SessionType, SystemInfo}; use goose_providers::thinking::ThinkingEffort; use rmcp::model::{ Annotations, Content, EmbeddedResource, Icon, IconTheme, ImageContent, JsonObject, @@ -440,14 +440,8 @@ derive_utoipa!(IconTheme as IconThemeSchema); super::routes::session_events::session_events, super::routes::session_events::session_reply, super::routes::session_events::session_cancel, - super::routes::session::list_sessions, - super::routes::session::search_sessions, super::routes::session::get_session, - super::routes::session::get_session_insights, super::routes::session::update_session_name, - super::routes::session::delete_session, - super::routes::session::export_session, - super::routes::session::import_session, super::routes::session::share_session_nostr, super::routes::session::import_session_nostr, super::routes::session::update_session_user_recipe_values, @@ -522,11 +516,9 @@ derive_utoipa!(IconTheme as IconThemeSchema); super::routes::session_events::SessionReplyRequest, super::routes::session_events::SessionReplyResponse, super::routes::session_events::CancelRequest, - super::routes::session::ImportSessionRequest, super::routes::session::ShareSessionNostrRequest, super::routes::session::ShareSessionNostrResponse, super::routes::session::ImportSessionNostrRequest, - super::routes::session::SessionListResponse, super::routes::session::UpdateSessionNameRequest, super::routes::session::UpdateSessionUserRecipeValuesRequest, super::routes::session::UpdateSessionUserRecipeValuesResponse, @@ -587,7 +579,6 @@ derive_utoipa!(IconTheme as IconThemeSchema); super::routes::config_management::ProviderModelInfoQuery, Session, goose::config::goose_mode::GooseMode, - SessionInsights, SessionType, SystemInfo, Conversation, diff --git a/crates/goose-server/src/routes/session.rs b/crates/goose-server/src/routes/session.rs index 96bbc9590..da7d52347 100644 --- a/crates/goose-server/src/routes/session.rs +++ b/crates/goose-server/src/routes/session.rs @@ -6,27 +6,21 @@ use axum::routing::post; use axum::{ extract::Path, http::StatusCode, - routing::{delete, get, put}, + routing::{get, put}, Json, Router, }; use goose::agents::ExtensionConfig; use goose::recipe::Recipe; #[cfg(feature = "nostr")] use goose::session::nostr_share; -use goose::session::session_manager::{SessionInsights, SessionType}; +#[cfg(feature = "nostr")] +use goose::session::session_manager::SessionType; use goose::session::{EnabledExtensionsState, Session}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::Arc; use utoipa::ToSchema; -#[derive(Serialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct SessionListResponse { - /// List of available session information objects - sessions: Vec, -} - #[derive(Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct UpdateSessionNameRequest { @@ -46,12 +40,6 @@ pub struct UpdateSessionUserRecipeValuesResponse { recipe: Recipe, } -#[derive(Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct ImportSessionRequest { - json: String, -} - #[cfg_attr(not(feature = "nostr"), allow(dead_code))] #[derive(Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] @@ -92,31 +80,6 @@ pub struct ForkResponse { const MAX_NAME_LENGTH: usize = 200; -#[utoipa::path( - get, - path = "/sessions", - responses( - (status = 200, description = "List of available sessions retrieved successfully", body = SessionListResponse), - (status = 401, description = "Unauthorized - Invalid or missing API key"), - (status = 500, description = "Internal server error") - ), - security( - ("api_key" = []) - ), - tag = "Session Management" -)] -async fn list_sessions( - State(state): State>, -) -> Result, StatusCode> { - let sessions = state - .session_manager() - .list_sessions() - .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - - Ok(Json(SessionListResponse { sessions })) -} - #[utoipa::path( get, path = "/sessions/{session_id}", @@ -146,29 +109,6 @@ async fn get_session( Ok(Json(session)) } -#[utoipa::path( - get, - path = "/sessions/insights", - responses( - (status = 200, description = "Session insights retrieved successfully", body = SessionInsights), - (status = 401, description = "Unauthorized - Invalid or missing API key"), - (status = 500, description = "Internal server error") - ), - security( - ("api_key" = []) - ), - tag = "Session Management" -)] -async fn get_session_insights( - State(state): State>, -) -> Result, StatusCode> { - let insights = state - .session_manager() - .get_insights() - .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - Ok(Json(insights)) -} #[utoipa::path( put, @@ -289,107 +229,6 @@ async fn update_session_user_recipe_values( } } -#[utoipa::path( - delete, - path = "/sessions/{session_id}", - params( - ("session_id" = String, Path, description = "Unique identifier for the session") - ), - responses( - (status = 200, description = "Session deleted successfully"), - (status = 401, description = "Unauthorized - Invalid or missing API key"), - (status = 404, description = "Session not found"), - (status = 500, description = "Internal server error") - ), - security( - ("api_key" = []) - ), - tag = "Session Management" -)] -async fn delete_session( - State(state): State>, - Path(session_id): Path, -) -> Result { - state - .session_manager() - .delete_session(&session_id) - .await - .map_err(|e| { - if e.to_string().contains("not found") { - StatusCode::NOT_FOUND - } else { - StatusCode::INTERNAL_SERVER_ERROR - } - })?; - - // Cancel any in-flight replies before dropping the bus, so spawned - // agent tasks stop consuming tokens for a deleted session. - if let Some(bus) = state.get_event_bus(&session_id).await { - bus.cancel_all_requests().await; - } - state.remove_event_bus(&session_id).await; - - Ok(StatusCode::OK) -} - -#[utoipa::path( - get, - path = "/sessions/{session_id}/export", - params( - ("session_id" = String, Path, description = "Unique identifier for the session") - ), - responses( - (status = 200, description = "Session exported successfully", body = String), - (status = 401, description = "Unauthorized - Invalid or missing API key"), - (status = 404, description = "Session not found"), - (status = 500, description = "Internal server error") - ), - security( - ("api_key" = []) - ), - tag = "Session Management" -)] -async fn export_session( - State(state): State>, - Path(session_id): Path, -) -> Result, StatusCode> { - let exported = state - .session_manager() - .export_session(&session_id) - .await - .map_err(|_| StatusCode::NOT_FOUND)?; - - Ok(Json(exported)) -} - -#[utoipa::path( - post, - path = "/sessions/import", - request_body = ImportSessionRequest, - responses( - (status = 200, description = "Session imported successfully", body = Session), - (status = 401, description = "Unauthorized - Invalid or missing API key"), - (status = 400, description = "Bad request - Invalid JSON"), - (status = 500, description = "Internal server error") - ), - security( - ("api_key" = []) - ), - tag = "Session Management" -)] -async fn import_session( - State(state): State>, - Json(request): Json, -) -> Result, StatusCode> { - let session = state - .session_manager() - .import_session(&request.json, Some(SessionType::User)) - .await - .map_err(|_| StatusCode::BAD_REQUEST)?; - - Ok(Json(session)) -} - #[cfg_attr(not(feature = "nostr"), allow(unused_variables))] #[utoipa::path( post, @@ -613,24 +452,15 @@ async fn get_session_extensions( pub fn routes(state: Arc) -> Router { Router::new() - .route("/sessions", get(list_sessions)) - .route("/sessions/search", get(search_sessions)) .route("/sessions/{session_id}", get(get_session)) - .route("/sessions/{session_id}", delete(delete_session)) - .route("/sessions/{session_id}/export", get(export_session)) .route( "/sessions/{session_id}/share/nostr", post(share_session_nostr).layer(DefaultBodyLimit::max(25 * 1024 * 1024)), ) - .route( - "/sessions/import", - post(import_session).layer(DefaultBodyLimit::max(25 * 1024 * 1024)), - ) .route( "/sessions/import/nostr", post(import_session_nostr).layer(DefaultBodyLimit::max(25 * 1024 * 1024)), ) - .route("/sessions/insights", get(get_session_insights)) .route("/sessions/{session_id}/name", put(update_session_name)) .route( "/sessions/{session_id}/user_recipe_values", @@ -643,77 +473,3 @@ pub fn routes(state: Arc) -> Router { ) .with_state(state) } -#[derive(Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct SearchSessionsQuery { - /// Search query string (keywords separated by spaces) - query: String, - /// Maximum number of results to return (default: 10, max: 50) - #[serde(default = "default_limit")] - limit: usize, - /// Filter results to sessions after this date (ISO 8601 format) - after_date: Option, - /// Filter results to sessions before this date (ISO 8601 format) - before_date: Option, -} - -fn default_limit() -> usize { - 10 -} - -#[utoipa::path( - get, - path = "/sessions/search", - params( - ("query" = String, Query, description = "Search query string"), - ("limit" = Option, Query, description = "Maximum results (default: 10, max: 50)"), - ("after_date" = Option, Query, description = "Filter after date (ISO 8601)"), - ("before_date" = Option, Query, description = "Filter before date (ISO 8601)") - ), - responses( - (status = 200, description = "Matching sessions", body = Vec), - (status = 400, description = "Bad request - Invalid query"), - (status = 401, description = "Unauthorized"), - (status = 500, description = "Internal server error") - ), - security( - ("api_key" = []) - ), - tag = "Session Management" -)] -async fn search_sessions( - State(state): State>, - axum::extract::Query(params): axum::extract::Query, -) -> Result>, StatusCode> { - let query = params.query.trim(); - if query.is_empty() { - return Err(StatusCode::BAD_REQUEST); - } - - let limit = params.limit.min(50); - - let after_date = params - .after_date - .and_then(|s| chrono::DateTime::parse_from_rfc3339(&s).ok()) - .map(|dt| dt.with_timezone(&chrono::Utc)); - - let before_date = params - .before_date - .and_then(|s| chrono::DateTime::parse_from_rfc3339(&s).ok()) - .map(|dt| dt.with_timezone(&chrono::Utc)); - - let sessions = state - .session_manager() - .search_chat_sessions( - query, - Some(limit), - after_date, - before_date, - None, - vec![SessionType::User, SessionType::Scheduled], - ) - .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - - Ok(Json(sessions)) -} diff --git a/crates/goose-server/src/session_event_bus.rs b/crates/goose-server/src/session_event_bus.rs index aa94231c8..c8880de11 100644 --- a/crates/goose-server/src/session_event_bus.rs +++ b/crates/goose-server/src/session_event_bus.rs @@ -150,14 +150,6 @@ impl SessionEventBus { } } - /// Cancel all active requests (e.g. when deleting a session). - pub async fn cancel_all_requests(&self) { - let requests = self.active_requests.lock().await; - for token in requests.values() { - token.cancel(); - } - } - /// Remove the cancellation token for a completed request. pub async fn cleanup_request(&self, request_id: &str) { let mut requests = self.active_requests.lock().await; diff --git a/crates/goose-server/src/state.rs b/crates/goose-server/src/state.rs index a9dee95f6..6d109c51e 100644 --- a/crates/goose-server/src/state.rs +++ b/crates/goose-server/src/state.rs @@ -149,12 +149,6 @@ impl AppState { buses.get(session_id).cloned() } - /// Remove the event bus for a session, freeing its replay buffer. - pub async fn remove_event_bus(&self, session_id: &str) { - let mut buses = self.session_buses.lock().await; - buses.remove(session_id); - } - pub async fn get_agent(&self, session_id: String) -> anyhow::Result> { self.agent_manager.get_or_create_agent(session_id).await } diff --git a/crates/goose/src/session/chat_history_search.rs b/crates/goose/src/session/chat_history_search.rs index 22a89807e..c06abb846 100644 --- a/crates/goose/src/session/chat_history_search.rs +++ b/crates/goose/src/session/chat_history_search.rs @@ -302,137 +302,3 @@ impl<'a> ChatHistorySearch<'a> { } } } - -pub(crate) struct ChatSessionSearch<'a> { - pool: &'a Pool, - query: &'a str, - limit: usize, - after_date: Option>, - before_date: Option>, - exclude_session_id: Option, - session_types: Vec, -} - -impl<'a> ChatSessionSearch<'a> { - pub fn new( - pool: &'a Pool, - query: &'a str, - limit: Option, - after_date: Option>, - before_date: Option>, - exclude_session_id: Option, - session_types: Vec, - ) -> Self { - Self { - pool, - query, - limit: limit.unwrap_or(10), - after_date, - before_date, - exclude_session_id, - session_types, - } - } - - pub async fn execute(self) -> Result> { - let keywords = self.parse_keywords(); - if keywords.is_empty() { - return Ok(Vec::new()); - } - - let sql = self.build_sql(&keywords); - let mut query_builder = sqlx::query_scalar::<_, String>(&sql); - - for keyword in &keywords { - query_builder = query_builder.bind(keyword); - } - - if let Some(after) = self.after_date { - query_builder = query_builder.bind(after); - } - if let Some(before) = self.before_date { - query_builder = query_builder.bind(before); - } - - if let Some(exclude_id) = &self.exclude_session_id { - query_builder = query_builder.bind(exclude_id); - } - - for t in &self.session_types { - query_builder = query_builder.bind(t.to_string()); - } - - query_builder = query_builder.bind(self.limit as i64); - - Ok(query_builder.fetch_all(self.pool).await?) - } - - fn parse_keywords(&self) -> Vec { - self.query - .split_whitespace() - .map(|word| format!("%{}%", word.to_lowercase())) - .collect() - } - - fn build_sql(&self, keywords: &[String]) -> String { - let mut sql = String::from( - r#" - SELECT s.id - FROM sessions s - WHERE EXISTS ( - SELECT 1 - FROM messages m - WHERE m.session_id = s.id - AND EXISTS ( - SELECT 1 FROM json_each(m.content_json) - WHERE json_extract(value, '$.type') = 'text' - AND ( - "#, - ); - - for (i, _) in keywords.iter().enumerate() { - if i > 0 { - sql.push_str(" OR "); - } - sql.push_str("LOWER(json_extract(value, '$.text')) LIKE ?"); - } - - sql.push_str( - r#" - ) - ) - "#, - ); - - if self.after_date.is_some() { - sql.push_str(" AND m.timestamp >= ?"); - } - if self.before_date.is_some() { - sql.push_str(" AND m.timestamp <= ?"); - } - - sql.push_str( - r#" - ) - "#, - ); - - if self.exclude_session_id.is_some() { - sql.push_str(" AND s.id != ?"); - } - - if !self.session_types.is_empty() { - let placeholders: String = self - .session_types - .iter() - .map(|_| "?") - .collect::>() - .join(", "); - sql.push_str(&format!(" AND s.session_type IN ({})", placeholders)); - } - - sql.push_str(" ORDER BY s.updated_at DESC, s.id DESC LIMIT ?"); - - sql - } -} diff --git a/crates/goose/src/session/session_manager.rs b/crates/goose/src/session/session_manager.rs index 6a10a1abe..c216ada06 100644 --- a/crates/goose/src/session/session_manager.rs +++ b/crates/goose/src/session/session_manager.rs @@ -526,27 +526,6 @@ impl SessionManager { .await } - pub async fn search_chat_sessions( - &self, - query: &str, - limit: Option, - after_date: Option>, - before_date: Option>, - exclude_session_id: Option, - session_types: Vec, - ) -> Result> { - self.storage - .search_chat_sessions( - query, - limit, - after_date, - before_date, - exclude_session_id, - session_types, - ) - .await - } - pub async fn update_message_metadata(id: &str, message_id: &str, f: F) -> Result<()> where F: FnOnce( @@ -1924,41 +1903,6 @@ impl SessionStorage { .await } - async fn search_chat_sessions( - &self, - query: &str, - limit: Option, - after_date: Option>, - before_date: Option>, - exclude_session_id: Option, - session_types: Vec, - ) -> Result> { - use crate::session::chat_history_search::ChatSessionSearch; - - let pool = self.pool().await?; - let session_ids = ChatSessionSearch::new( - pool, - query, - limit, - after_date, - before_date, - exclude_session_id, - session_types, - ) - .execute() - .await?; - - let mut sessions = Vec::with_capacity(session_ids.len()); - for session_id in session_ids { - match self.get_session(&session_id, false).await { - Ok(session) => sessions.push(session), - Err(err) if err.to_string() == "Session not found" => continue, - Err(err) => return Err(err), - } - } - Ok(sessions) - } - async fn update_message_metadata( &self, session_id: &str, @@ -2228,132 +2172,6 @@ mod tests { assert_eq!(results.results[0].messages.len(), 2); } - #[tokio::test] - async fn test_search_chat_sessions_limits_distinct_sessions() { - let temp_dir = TempDir::new().unwrap(); - let sm = SessionManager::new(temp_dir.path().to_path_buf()); - - let older_target = create_search_session( - &sm, - "Older target", - SessionType::User, - "2026-05-01T00:00:00Z", - &[( - "does Acme have an email address for John Doe", - "2026-05-01T00:00:00Z", - )], - ) - .await; - - let newer_noise = create_search_session( - &sm, - "Newer noise", - SessionType::User, - "2026-05-22T00:00:00Z", - &[ - ("Acme person name looking for Acme", "2026-05-22T00:00:00Z"), - ( - "another Acme person name looking for Acme", - "2026-05-22T00:01:00Z", - ), - ], - ) - .await; - - let results = sm - .search_chat_sessions("Acme", Some(2), None, None, None, vec![SessionType::User]) - .await - .unwrap(); - let ids = results - .iter() - .map(|session| session.id.clone()) - .collect::>(); - - assert_eq!(ids, vec![newer_noise, older_target]); - } - - #[tokio::test] - async fn test_search_chat_sessions_applies_all_filters() { - let temp_dir = TempDir::new().unwrap(); - let sm = SessionManager::new(temp_dir.path().to_path_buf()); - - let excluded = create_search_session( - &sm, - "Excluded user", - SessionType::User, - "2026-05-20T00:00:00Z", - &[("Acme John excluded session", "2026-05-15T00:00:00Z")], - ) - .await; - - let scheduled_target = create_search_session( - &sm, - "Scheduled target", - SessionType::Scheduled, - "2026-05-19T00:00:00Z", - &[( - "John appears in scheduled Acme work", - "2026-05-16T00:00:00Z", - )], - ) - .await; - - let user_target = create_search_session( - &sm, - "User target", - SessionType::User, - "2026-05-18T00:00:00Z", - &[( - "Acme has an email address question for John Doe", - "2026-05-14T00:00:00Z", - )], - ) - .await; - - let _before_window = create_search_session( - &sm, - "Before window", - SessionType::User, - "2026-05-17T00:00:00Z", - &[("Acme John before date window", "2026-05-09T00:00:00Z")], - ) - .await; - - let _wrong_type = create_search_session( - &sm, - "ACP target", - SessionType::Acp, - "2026-05-16T00:00:00Z", - &[("Acme John wrong session type", "2026-05-15T00:00:00Z")], - ) - .await; - - let after = chrono::DateTime::parse_from_rfc3339("2026-05-10T00:00:00Z") - .unwrap() - .with_timezone(&chrono::Utc); - let before = chrono::DateTime::parse_from_rfc3339("2026-05-17T00:00:00Z") - .unwrap() - .with_timezone(&chrono::Utc); - - let results = sm - .search_chat_sessions( - "Acme John", - Some(10), - Some(after), - Some(before), - Some(excluded), - vec![SessionType::User, SessionType::Scheduled], - ) - .await - .unwrap(); - let ids = results - .iter() - .map(|session| session.id.clone()) - .collect::>(); - - assert_eq!(ids, vec![scheduled_target, user_target]); - } - async fn expected_session_list_ids(sm: &SessionManager, session_ids: &[String]) -> Vec { let mut sessions = Vec::new(); for session_id in session_ids { diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index dce8ef106..bf8bb14a9 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -3203,81 +3203,6 @@ } } }, - "/sessions": { - "get": { - "tags": [ - "Session Management" - ], - "operationId": "list_sessions", - "responses": { - "200": { - "description": "List of available sessions retrieved successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SessionListResponse" - } - } - } - }, - "401": { - "description": "Unauthorized - Invalid or missing API key" - }, - "500": { - "description": "Internal server error" - } - }, - "security": [ - { - "api_key": [] - } - ] - } - }, - "/sessions/import": { - "post": { - "tags": [ - "Session Management" - ], - "operationId": "import_session", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ImportSessionRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Session imported successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Session" - } - } - } - }, - "400": { - "description": "Bad request - Invalid JSON" - }, - "401": { - "description": "Unauthorized - Invalid or missing API key" - }, - "500": { - "description": "Internal server error" - } - }, - "security": [ - { - "api_key": [] - } - ] - } - }, "/sessions/import/nostr": { "post": { "tags": [ @@ -3322,116 +3247,6 @@ ] } }, - "/sessions/insights": { - "get": { - "tags": [ - "Session Management" - ], - "operationId": "get_session_insights", - "responses": { - "200": { - "description": "Session insights retrieved successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SessionInsights" - } - } - } - }, - "401": { - "description": "Unauthorized - Invalid or missing API key" - }, - "500": { - "description": "Internal server error" - } - }, - "security": [ - { - "api_key": [] - } - ] - } - }, - "/sessions/search": { - "get": { - "tags": [ - "Session Management" - ], - "operationId": "search_sessions", - "parameters": [ - { - "name": "query", - "in": "query", - "description": "Search query string", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "limit", - "in": "query", - "description": "Maximum results (default: 10, max: 50)", - "required": false, - "schema": { - "type": "integer", - "nullable": true, - "minimum": 0 - } - }, - { - "name": "after_date", - "in": "query", - "description": "Filter after date (ISO 8601)", - "required": false, - "schema": { - "type": "string", - "nullable": true - } - }, - { - "name": "before_date", - "in": "query", - "description": "Filter before date (ISO 8601)", - "required": false, - "schema": { - "type": "string", - "nullable": true - } - } - ], - "responses": { - "200": { - "description": "Matching sessions", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Session" - } - } - } - } - }, - "400": { - "description": "Bad request - Invalid query" - }, - "401": { - "description": "Unauthorized" - }, - "500": { - "description": "Internal server error" - } - }, - "security": [ - { - "api_key": [] - } - ] - } - }, "/sessions/{id}/cancel": { "post": { "tags": [ @@ -3596,87 +3411,6 @@ "api_key": [] } ] - }, - "delete": { - "tags": [ - "Session Management" - ], - "operationId": "delete_session", - "parameters": [ - { - "name": "session_id", - "in": "path", - "description": "Unique identifier for the session", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Session deleted successfully" - }, - "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": [ - "Session Management" - ], - "operationId": "export_session", - "parameters": [ - { - "name": "session_id", - "in": "path", - "description": "Unique identifier for the session", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Session exported successfully", - "content": { - "text/plain": { - "schema": { - "type": "string" - } - } - } - }, - "401": { - "description": "Unauthorized - Invalid or missing API key" - }, - "404": { - "description": "Session not found" - }, - "500": { - "description": "Internal server error" - } - }, - "security": [ - { - "api_key": [] - } - ] } }, "/sessions/{session_id}/extensions": { @@ -5928,17 +5662,6 @@ } } }, - "ImportSessionRequest": { - "type": "object", - "required": [ - "json" - ], - "properties": { - "json": { - "type": "string" - } - } - }, "InferenceMetadata": { "type": "object", "required": [ @@ -8399,38 +8122,6 @@ } } }, - "SessionInsights": { - "type": "object", - "required": [ - "totalSessions", - "totalTokens" - ], - "properties": { - "totalSessions": { - "type": "integer", - "minimum": 0 - }, - "totalTokens": { - "type": "integer", - "format": "int64" - } - } - }, - "SessionListResponse": { - "type": "object", - "required": [ - "sessions" - ], - "properties": { - "sessions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Session" - }, - "description": "List of available session information objects" - } - } - }, "SessionReplyRequest": { "type": "object", "required": [ diff --git a/ui/desktop/src/acp/sessions.ts b/ui/desktop/src/acp/sessions.ts index 9eeac367e..d48ce1b59 100644 --- a/ui/desktop/src/acp/sessions.ts +++ b/ui/desktop/src/acp/sessions.ts @@ -51,9 +51,27 @@ function sessionInfoToListItem(s: SessionInfo): SessionListItem { }; } -export async function acpListSessions(cursor?: string | null): Promise { +export interface SessionListFilter { + keyword?: string; +} + +const SESSION_LIST_TYPES = ['user', 'scheduled'] as const; + +export async function acpListSessions( + cursor?: string | null, + filter?: SessionListFilter +): Promise { const client = await getAcpClient(); - const request: ListSessionsRequest = cursor ? { cursor } : {}; + const request: ListSessionsRequest = {}; + if (cursor) { + request.cursor = cursor; + } + const meta: Record = { types: SESSION_LIST_TYPES }; + const keyword = filter?.keyword?.trim(); + if (keyword) { + meta.query = keyword; + } + request._meta = meta; const response = await client.listSessions(request); return { sessions: response.sessions.map(sessionInfoToListItem), @@ -67,7 +85,7 @@ export async function acpListRecentSessions(maxSessions: number): Promise = Options2 & { /** @@ -498,17 +498,6 @@ export const sessionsHandler = (options: O export const unpauseSchedule = (options: Options) => (options.client ?? client).post({ url: '/schedule/{id}/unpause', ...options }); -export const listSessions = (options?: Options) => (options?.client ?? client).get({ url: '/sessions', ...options }); - -export const importSession = (options: Options) => (options.client ?? client).post({ - url: '/sessions/import', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options.headers - } -}); - export const importSessionNostr = (options: Options) => (options.client ?? client).post({ url: '/sessions/import/nostr', ...options, @@ -518,10 +507,6 @@ export const importSessionNostr = (options } }); -export const getSessionInsights = (options?: Options) => (options?.client ?? client).get({ url: '/sessions/insights', ...options }); - -export const searchSessions = (options: Options) => (options.client ?? client).get({ url: '/sessions/search', ...options }); - export const sessionCancel = (options: Options) => (options.client ?? client).post({ url: '/sessions/{id}/cancel', ...options, @@ -542,12 +527,8 @@ export const sessionReply = (options: Opti } }); -export const deleteSession = (options: Options) => (options.client ?? client).delete({ url: '/sessions/{session_id}', ...options }); - export const getSession = (options: Options) => (options.client ?? client).get({ url: '/sessions/{session_id}', ...options }); -export const exportSession = (options: Options) => (options.client ?? client).get({ url: '/sessions/{session_id}/export', ...options }); - export const getSessionExtensions = (options: Options) => (options.client ?? client).get({ url: '/sessions/{session_id}/extensions', ...options }); export const forkSession = (options: Options) => (options.client ?? client).post({ diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index 426dc28d3..ca49c97ee 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -608,10 +608,6 @@ export type ImportSessionNostrRequest = { deeplink: string; }; -export type ImportSessionRequest = { - json: string; -}; - export type InferenceMetadata = { provider: string; requestedModel: string; @@ -1381,18 +1377,6 @@ export type SessionExtensionsResponse = { extensions: Array; }; -export type SessionInsights = { - totalSessions: number; - totalTokens: number; -}; - -export type SessionListResponse = { - /** - * List of available session information objects - */ - sessions: Array; -}; - export type SessionReplyRequest = { override_conversation?: Array | null; /** @@ -4223,64 +4207,6 @@ export type UnpauseScheduleResponses = { export type UnpauseScheduleResponse = UnpauseScheduleResponses[keyof UnpauseScheduleResponses]; -export type ListSessionsData = { - body?: never; - path?: never; - query?: never; - url: '/sessions'; -}; - -export type ListSessionsErrors = { - /** - * Unauthorized - Invalid or missing API key - */ - 401: unknown; - /** - * Internal server error - */ - 500: unknown; -}; - -export type ListSessionsResponses = { - /** - * List of available sessions retrieved successfully - */ - 200: SessionListResponse; -}; - -export type ListSessionsResponse = ListSessionsResponses[keyof ListSessionsResponses]; - -export type ImportSessionData = { - body: ImportSessionRequest; - path?: never; - query?: never; - url: '/sessions/import'; -}; - -export type ImportSessionErrors = { - /** - * Bad request - Invalid JSON - */ - 400: unknown; - /** - * Unauthorized - Invalid or missing API key - */ - 401: unknown; - /** - * Internal server error - */ - 500: unknown; -}; - -export type ImportSessionResponses = { - /** - * Session imported successfully - */ - 200: Session; -}; - -export type ImportSessionResponse = ImportSessionResponses[keyof ImportSessionResponses]; - export type ImportSessionNostrData = { body: ImportSessionNostrRequest; path?: never; @@ -4312,81 +4238,6 @@ export type ImportSessionNostrResponses = { export type ImportSessionNostrResponse = ImportSessionNostrResponses[keyof ImportSessionNostrResponses]; -export type GetSessionInsightsData = { - body?: never; - path?: never; - query?: never; - url: '/sessions/insights'; -}; - -export type GetSessionInsightsErrors = { - /** - * Unauthorized - Invalid or missing API key - */ - 401: unknown; - /** - * Internal server error - */ - 500: unknown; -}; - -export type GetSessionInsightsResponses = { - /** - * Session insights retrieved successfully - */ - 200: SessionInsights; -}; - -export type GetSessionInsightsResponse = GetSessionInsightsResponses[keyof GetSessionInsightsResponses]; - -export type SearchSessionsData = { - body?: never; - path?: never; - query: { - /** - * Search query string - */ - query: string; - /** - * Maximum results (default: 10, max: 50) - */ - limit?: number | null; - /** - * Filter after date (ISO 8601) - */ - after_date?: string | null; - /** - * Filter before date (ISO 8601) - */ - before_date?: string | null; - }; - url: '/sessions/search'; -}; - -export type SearchSessionsErrors = { - /** - * Bad request - Invalid query - */ - 400: unknown; - /** - * Unauthorized - */ - 401: unknown; - /** - * Internal server error - */ - 500: unknown; -}; - -export type SearchSessionsResponses = { - /** - * Matching sessions - */ - 200: Array; -}; - -export type SearchSessionsResponse = SearchSessionsResponses[keyof SearchSessionsResponses]; - export type SessionCancelData = { body: CancelRequest; path: { @@ -4474,40 +4325,6 @@ export type SessionReplyResponses = { export type SessionReplyResponse2 = SessionReplyResponses[keyof SessionReplyResponses]; -export type DeleteSessionData = { - body?: never; - path: { - /** - * Unique identifier for the session - */ - session_id: string; - }; - query?: never; - url: '/sessions/{session_id}'; -}; - -export type DeleteSessionErrors = { - /** - * Unauthorized - Invalid or missing API key - */ - 401: unknown; - /** - * Session not found - */ - 404: unknown; - /** - * Internal server error - */ - 500: unknown; -}; - -export type DeleteSessionResponses = { - /** - * Session deleted successfully - */ - 200: unknown; -}; - export type GetSessionData = { body?: never; path: { @@ -4544,42 +4361,6 @@ export type GetSessionResponses = { export type GetSessionResponse = GetSessionResponses[keyof GetSessionResponses]; -export type ExportSessionData = { - body?: never; - path: { - /** - * Unique identifier for the session - */ - session_id: string; - }; - query?: never; - url: '/sessions/{session_id}/export'; -}; - -export type ExportSessionErrors = { - /** - * Unauthorized - Invalid or missing API key - */ - 401: unknown; - /** - * Session not found - */ - 404: unknown; - /** - * Internal server error - */ - 500: unknown; -}; - -export type ExportSessionResponses = { - /** - * Session exported successfully - */ - 200: string; -}; - -export type ExportSessionResponse = ExportSessionResponses[keyof ExportSessionResponses]; - export type GetSessionExtensionsData = { body?: never; path: { diff --git a/ui/desktop/src/components/conversation/SearchBar.tsx b/ui/desktop/src/components/conversation/SearchBar.tsx index 35c2ad7e7..3d3964f37 100644 --- a/ui/desktop/src/components/conversation/SearchBar.tsx +++ b/ui/desktop/src/components/conversation/SearchBar.tsx @@ -49,6 +49,11 @@ interface SearchBarProps { initialSearchTerm?: string; /** Placeholder text for the search input */ placeholder?: string; + /** Show the case-sensitivity toggle (default: true). */ + showCaseSensitive?: boolean; + /** Show the previous/next match navigation arrows (default: true). When hidden, the + * result counter shows the total instead of "current/total". */ + showNavigation?: boolean; } /** @@ -62,6 +67,8 @@ export const SearchBar: React.FC = ({ inputRef: externalInputRef, initialSearchTerm = '', placeholder, + showCaseSensitive = true, + showNavigation = true, }: SearchBarProps) => { const intl = useIntl(); const resolvedPlaceholder = placeholder ?? intl.formatMessage(i18nMessages.defaultPlaceholder); @@ -197,52 +204,57 @@ export const SearchBar: React.FC = ({
- {(() => { - return localSearchResults?.count && localSearchResults.count > 0 && searchTerm - ? `${localSearchResults.currentIndex}/${localSearchResults.count}` - : null; - })()} + {showNavigation && + localSearchResults?.count && + localSearchResults.count > 0 && + searchTerm + ? `${localSearchResults.currentIndex}/${localSearchResults.count}` + : null}
- + {showCaseSensitive && ( + + )} -
- - -
+ {showNavigation && ( +
+ + +
+ )}
@@ -916,6 +847,17 @@ const SessionListView: React.FC = React.memo( } if (sessions.length === 0) { + // `sessions` holds the keyword-filtered set, so an empty result while searching + // means "no matches" rather than "no sessions at all". + if (debouncedSearchTerm) { + return ( +
+ +

{intl.formatMessage(i18n.noMatching)}

+

{intl.formatMessage(i18n.noMatchingDesc)}

+
+ ); + } return (
@@ -925,16 +867,6 @@ const SessionListView: React.FC = React.memo( ); } - if (dateGroups.length === 0 && searchResults !== null) { - return ( -
- -

{intl.formatMessage(i18n.noMatching)}

-

{intl.formatMessage(i18n.noMatchingDesc)}

-
- ); - } - return (
{visibleDateGroups.map((group) => ( @@ -1014,10 +946,11 @@ const SessionListView: React.FC = React.memo(
{/* Skeleton layer - always rendered but conditionally visible */}
{ - const sessionsResponse = await listSessions({ client: ctx.client }); - expect(sessionsResponse.response).toBeOkResponse(); - expect(sessionsResponse.data).toBeDefined(); - expect(sessionsResponse.data!.sessions).toBeDefined(); - expect(Array.isArray(sessionsResponse.data!.sessions)).toBe(true); - }); - it('should persist goose_mode on the session', async () => { await upsertConfig({ client: ctx.client,