From 96e4e379c4f84d45b9baf5c1778ec09ed970e709 Mon Sep 17 00:00:00 2001 From: jry Date: Thu, 11 Jun 2026 20:54:38 +0800 Subject: [PATCH] add paginated conversation detail API for mobile --- src-tauri/src/commands/conversations.rs | 65 ++++++++++++++++++++ src-tauri/src/lib.rs | 1 + src-tauri/src/models/conversation.rs | 13 ++++ src-tauri/src/web/handlers/conversations.rs | 25 ++++++++ src-tauri/src/web/router.rs | 4 ++ src/components/message/message-list-view.tsx | 6 +- src/lib/api.ts | 13 ++++ src/lib/tauri.ts | 13 ++++ src/lib/types.ts | 10 +++ 9 files changed, 147 insertions(+), 3 deletions(-) diff --git a/src-tauri/src/commands/conversations.rs b/src-tauri/src/commands/conversations.rs index 51e2d529d..31a95612f 100644 --- a/src-tauri/src/commands/conversations.rs +++ b/src-tauri/src/commands/conversations.rs @@ -14,6 +14,27 @@ use crate::parsers::hermes::HermesParser; use crate::parsers::openclaw::OpenClawParser; use crate::parsers::opencode::OpenCodeParser; use crate::parsers::{path_eq_for_matching, AgentParser, ParseError}; + +const DEFAULT_CONVERSATION_TURNS_PAGE_SIZE: usize = 30; + +fn paginate_turns( + turns: Vec, + before_turn_index: Option, + page_size: Option, +) -> (Vec, bool, Option, usize) { + let total = turns.len(); + let page_size = page_size.unwrap_or(DEFAULT_CONVERSATION_TURNS_PAGE_SIZE).max(1); + let end = before_turn_index.unwrap_or(total).min(total); + let start = end.saturating_sub(page_size); + let has_more_history = start > 0; + let next_before_turn_index = has_more_history.then_some(start); + ( + turns[start..end].to_vec(), + has_more_history, + next_before_turn_index, + page_size, + ) +} use crate::web::event_bridge::{ emit_event, ConversationChange, EventEmitter, TabsChanged, CONVERSATION_CHANGED_EVENT, TABS_CHANGED_EVENT, @@ -821,6 +842,50 @@ pub async fn get_folder_conversation( .await } +pub async fn get_folder_conversation_paginated_core( + conn: &sea_orm::DatabaseConnection, + manager: &crate::acp::manager::ConnectionManager, + emitter: &EventEmitter, + conversation_id: i32, + before_turn_index: Option, + page_size: Option, +) -> Result { + let detail = + get_folder_conversation_with_live_core(conn, manager, emitter, conversation_id).await?; + let (turns, has_more_history, next_before_turn_index, page_size) = + paginate_turns(detail.turns, before_turn_index, page_size); + Ok(DbConversationDetailPage { + summary: detail.summary, + turns, + has_more_history, + next_before_turn_index, + page_size, + session_stats: detail.session_stats, + in_flight_user_turn_id: detail.in_flight_user_turn_id, + }) +} + +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] +pub async fn get_folder_conversation_paginated( + app: tauri::AppHandle, + db: tauri::State<'_, AppDatabase>, + manager: tauri::State<'_, crate::acp::manager::ConnectionManager>, + conversation_id: i32, + before_turn_index: Option, + page_size: Option, +) -> Result { + get_folder_conversation_paginated_core( + &db.conn, + &manager, + &EventEmitter::Tauri(app), + conversation_id, + before_turn_index, + page_size, + ) + .await +} + /// Emit a `conversation://changed` Upsert for `conversation_id` so every /// client's sidebar inserts-or-replaces the row in real time. Re-fetches the /// fresh summary via `get_by_id`, which filters out soft-deleted rows — so an diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index d1df5d430..91988ef35 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -736,6 +736,7 @@ mod tauri_app { conversations::save_opened_tabs, conversations::import_local_conversations, conversations::get_folder_conversation, + conversations::get_folder_conversation_paginated, conversations::list_folders, conversations::get_stats, conversations::get_sidebar_data, diff --git a/src-tauri/src/models/conversation.rs b/src-tauri/src/models/conversation.rs index 4985884fc..804c01121 100644 --- a/src-tauri/src/models/conversation.rs +++ b/src-tauri/src/models/conversation.rs @@ -91,6 +91,19 @@ pub struct DbConversationDetail { pub in_flight_user_turn_id: Option, } +#[derive(Debug, Clone, Serialize)] +pub struct DbConversationDetailPage { + pub summary: DbConversationSummary, + pub turns: Vec, + pub has_more_history: bool, + pub next_before_turn_index: Option, + pub page_size: usize, + #[serde(skip_serializing_if = "Option::is_none")] + pub session_stats: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub in_flight_user_turn_id: Option, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FolderInfo { pub path: String, diff --git a/src-tauri/src/web/handlers/conversations.rs b/src-tauri/src/web/handlers/conversations.rs index c6ac760ef..9664ed238 100644 --- a/src-tauri/src/web/handlers/conversations.rs +++ b/src-tauri/src/web/handlers/conversations.rs @@ -127,6 +127,14 @@ pub struct GetFolderConversationParams { pub conversation_id: i32, } +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetFolderConversationPaginatedParams { + pub conversation_id: i32, + pub before_turn_index: Option, + pub page_size: Option, +} + pub async fn get_folder_conversation( Extension(state): Extension>, Json(params): Json, @@ -142,6 +150,23 @@ pub async fn get_folder_conversation( Ok(Json(result)) } +pub async fn get_folder_conversation_paginated( + Extension(state): Extension>, + Json(params): Json, +) -> Result, AppCommandError> { + let db = &state.db; + let result = conv_commands::get_folder_conversation_paginated_core( + &db.conn, + &state.connection_manager, + &state.emitter, + params.conversation_id, + params.before_turn_index, + params.page_size, + ) + .await?; + Ok(Json(result)) +} + pub async fn list_folders() -> Result>, AppCommandError> { let result = conv_commands::list_folders().await?; Ok(Json(result)) diff --git a/src-tauri/src/web/router.rs b/src-tauri/src/web/router.rs index 73e8d4361..1e5a4dd0b 100644 --- a/src-tauri/src/web/router.rs +++ b/src-tauri/src/web/router.rs @@ -88,6 +88,10 @@ pub fn build_router( "/get_folder_conversation", post(handlers::conversations::get_folder_conversation), ) + .route( + "/get_folder_conversation_paginated", + post(handlers::conversations::get_folder_conversation_paginated), + ) .route( "/list_opened_tabs", post(handlers::conversations::list_opened_tabs), diff --git a/src/components/message/message-list-view.tsx b/src/components/message/message-list-view.tsx index 8c77d2bd0..e7426b0d4 100644 --- a/src/components/message/message-list-view.tsx +++ b/src/components/message/message-list-view.tsx @@ -903,9 +903,9 @@ export function MessageListView({ - - + + { + return getTransport().call("get_folder_conversation_paginated", { + conversationId: params.conversationId, + beforeTurnIndex: params.beforeTurnIndex ?? null, + pageSize: params.pageSize ?? null, + }) +} + export async function removeFolderFromHistory(path: string): Promise { return getTransport().call("remove_folder_from_history", { path }) } diff --git a/src/lib/tauri.ts b/src/lib/tauri.ts index 867f05b25..fe5b902f8 100644 --- a/src/lib/tauri.ts +++ b/src/lib/tauri.ts @@ -5,6 +5,7 @@ import type { ConversationSummary, ConversationDetail, DbConversationDetail, + DbConversationDetailPage, FolderInfo, AgentStats, SidebarData, @@ -591,6 +592,18 @@ export async function getFolderConversation( return invoke("get_folder_conversation", { conversationId }) } +export async function getFolderConversationPaginated(params: { + conversationId: number + beforeTurnIndex?: number | null + pageSize?: number | null +}): Promise { + return invoke("get_folder_conversation_paginated", { + conversationId: params.conversationId, + beforeTurnIndex: params.beforeTurnIndex ?? null, + pageSize: params.pageSize ?? null, + }) +} + export async function removeFolderFromHistory(path: string): Promise { return invoke("remove_folder_from_history", { path }) } diff --git a/src/lib/types.ts b/src/lib/types.ts index 7d859c25f..712aee854 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -353,6 +353,16 @@ export interface DbConversationDetail { in_flight_user_turn_id?: string | null } +export interface DbConversationDetailPage { + summary: DbConversationSummary + turns: MessageTurn[] + has_more_history: boolean + next_before_turn_index?: number | null + page_size: number + session_stats?: SessionStats | null + in_flight_user_turn_id?: string | null +} + export type ConversationStatus = | "in_progress" | "pending_review"