fix(#219): add pagination to sessions history tab (#357)

* fix(#219): add pagination to sessions history tab

The /api/sessions endpoint was called without pagination params, so the server returned only its default limit of 20 sessions.

- SessionListResponse: added total/limit/offset fields from server response
- HermesApiService: added @Query params (limit, offset, order) to getSessions()
- SessionsViewModel: added loadMore() that appends pages, hasMore derived from total
- SessionsScreen: added 'Load more sessions' button + loading spinner at bottom of list

* chore: remove plan file from git

* fix: compilation errors in sessions pagination

* chore: gitignore plan files

* fix: remove duplicate brace

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-06-25 00:58:52 +03:00
1 parent 067f4500aa
commit 4ba74c2efa
7 files changed
+114 -7

No files matched your search

@@ -2,6 +2,9 @@ package com.m57.hermescontrol.data.model
data class SessionListResponse(
val sessions: List<SessionInfo>,
val total: Int = 0,
val limit: Int = 0,
val offset: Int = 0,
)
data class SessionInfo(
@@ -67,7 +67,11 @@ interface HermesApiService {
suspend fun getStatus(): Response<StatusResponse>
@GET("api/sessions")
suspend fun getSessions(): Response<SessionListResponse>
suspend fun getSessions(
@Query("limit") limit: Int = 20,
@Query("offset") offset: Int = 0,
@Query("order") order: String = "recent",
): Response<SessionListResponse>
@GET("api/sessions/{id}/messages")
suspend fun getSessionMessages(
@@ -1,12 +1,14 @@
package com.m57.hermescontrol.ui.sessions
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
@@ -14,6 +16,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.History
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -28,6 +31,7 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.m57.hermescontrol.ChatScreen
@@ -156,6 +160,38 @@ fun SessionsScreen(
}
}
}
// Load more button
if (state.hasMore || state.isLoadingMore) {
item {
Box(
modifier =
Modifier
.fillMaxWidth()
.padding(vertical = spacing.sm),
contentAlignment = Alignment.Center,
) {
if (state.isLoadingMore) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
strokeWidth = 2.dp,
)
} else {
Text(
text = "Load more sessions...",
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary,
modifier =
Modifier
.testTag("load_more_sessions")
.clickable(role = Role.Button) {
viewModel.loadMore()
},
)
}
}
}
}
}
}
}
@@ -1,6 +1,7 @@
package com.m57.hermescontrol.ui.sessions
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.m57.hermescontrol.data.model.SessionInfo
import com.m57.hermescontrol.data.remote.ApiClient
import com.m57.hermescontrol.data.remote.safeApiCall
@@ -9,26 +10,47 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
data class SessionsUiState(
val isLoading: Boolean = false,
val isLoadingMore: Boolean = false,
val sessions: List<SessionInfo> = emptyList(),
val total: Int = 0,
val errorMessage: String? = null,
)
) {
val hasMore: Boolean get() = total > sessions.size
}
class SessionsViewModel : ViewModel() {
private val _uiState = MutableStateFlow(SessionsUiState())
val uiState: StateFlow<SessionsUiState> = _uiState.asStateFlow()
/** Page size sent to the server — matches the default the gateway uses. */
private companion object {
const val PAGE_SIZE = 20
}
/** Load (or reload) sessions from page 0. Used by pull-to-refresh and initial load. */
fun loadSessions() {
safeLaunchLoad(
apiCall = { safeApiCall { ApiClient.hermesApi.getSessions() } },
apiCall = {
safeApiCall {
ApiClient.hermesApi.getSessions(
limit = PAGE_SIZE,
offset = 0,
order = "recent",
)
}
},
onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } },
onSuccess = { data ->
_uiState.update {
it.copy(
isLoading = false,
isLoadingMore = false,
sessions = data?.sessions.orEmpty(),
total = data?.total ?: 0,
)
}
},
@@ -36,10 +58,49 @@ class SessionsViewModel : ViewModel() {
_uiState.update {
it.copy(
isLoading = false,
isLoadingMore = false,
errorMessage = "Failed to load sessions: $errorMsg",
)
}
},
)
}
/** Load the next page and append to the existing session list. */
fun loadMore() {
val state = _uiState.value
if (state.isLoadingMore || !state.hasMore) return
_uiState.update { it.copy(isLoadingMore = true) }
viewModelScope.launch {
val result =
safeApiCall {
ApiClient.hermesApi.getSessions(
limit = PAGE_SIZE,
offset = state.sessions.size,
order = "recent",
)
}
when (result) {
is com.m57.hermescontrol.data.remote.NetworkResult.Success -> {
val data = result.data
_uiState.update {
it.copy(
isLoadingMore = false,
sessions = it.sessions + data.sessions,
total = data.total,
)
}
}
is com.m57.hermescontrol.data.remote.NetworkResult.Failure -> {
_uiState.update {
it.copy(
isLoadingMore = false,
errorMessage = "Failed to load more: ${result.error.message}",
)
}
}
}
}
}
}