feat: move session history to a dedicated history screen (#175)
* feat: move session history to a dedicated history screen Closes #159. * fix(ci): fix ktlint style issues and unresolved NavigationController reference
This commit is contained in:
@@ -26,6 +26,7 @@ import androidx.compose.material.icons.filled.Code
|
|||||||
import androidx.compose.material.icons.filled.Dashboard
|
import androidx.compose.material.icons.filled.Dashboard
|
||||||
import androidx.compose.material.icons.filled.Devices
|
import androidx.compose.material.icons.filled.Devices
|
||||||
import androidx.compose.material.icons.filled.Extension
|
import androidx.compose.material.icons.filled.Extension
|
||||||
|
import androidx.compose.material.icons.filled.History
|
||||||
import androidx.compose.material.icons.filled.HistoryEdu
|
import androidx.compose.material.icons.filled.HistoryEdu
|
||||||
import androidx.compose.material.icons.filled.Info
|
import androidx.compose.material.icons.filled.Info
|
||||||
import androidx.compose.material.icons.filled.Key
|
import androidx.compose.material.icons.filled.Key
|
||||||
@@ -82,6 +83,7 @@ import com.m57.hermescontrol.ui.model.ModelScreen as ModelScreenContent
|
|||||||
import com.m57.hermescontrol.ui.pairing.PairingScreen as PairingScreenContent
|
import com.m57.hermescontrol.ui.pairing.PairingScreen as PairingScreenContent
|
||||||
import com.m57.hermescontrol.ui.plugins.PluginsScreen as PluginsScreenContent
|
import com.m57.hermescontrol.ui.plugins.PluginsScreen as PluginsScreenContent
|
||||||
import com.m57.hermescontrol.ui.profiles.ProfilesScreen as ProfilesScreenContent
|
import com.m57.hermescontrol.ui.profiles.ProfilesScreen as ProfilesScreenContent
|
||||||
|
import com.m57.hermescontrol.ui.sessions.SessionsScreen as HistoryScreenContent
|
||||||
import com.m57.hermescontrol.ui.settings.SettingsScreen as SettingsScreenContent
|
import com.m57.hermescontrol.ui.settings.SettingsScreen as SettingsScreenContent
|
||||||
import com.m57.hermescontrol.ui.skills.SkillsScreen as SkillsScreenContent
|
import com.m57.hermescontrol.ui.skills.SkillsScreen as SkillsScreenContent
|
||||||
import com.m57.hermescontrol.ui.system.SystemScreen as SystemScreenContent
|
import com.m57.hermescontrol.ui.system.SystemScreen as SystemScreenContent
|
||||||
@@ -118,6 +120,7 @@ private val ALL_NAV_ITEMS: List<BottomNavItem> =
|
|||||||
BottomNavItem(LogsScreen, "Logs", Icons.Filled.HistoryEdu),
|
BottomNavItem(LogsScreen, "Logs", Icons.Filled.HistoryEdu),
|
||||||
BottomNavItem(KanbanScreen, "Kanban", Icons.Filled.Dashboard),
|
BottomNavItem(KanbanScreen, "Kanban", Icons.Filled.Dashboard),
|
||||||
BottomNavItem(AchievementsScreen, "Achievements", Icons.Filled.Info),
|
BottomNavItem(AchievementsScreen, "Achievements", Icons.Filled.Info),
|
||||||
|
BottomNavItem(HistoryScreen, "History", Icons.Filled.History),
|
||||||
)
|
)
|
||||||
|
|
||||||
/** Lookup: data-object simple name → NavKey (used by bottom-nav config). */
|
/** Lookup: data-object simple name → NavKey (used by bottom-nav config). */
|
||||||
@@ -150,6 +153,7 @@ private val DRAWER_ENTRIES =
|
|||||||
listOf(
|
listOf(
|
||||||
// Converse
|
// Converse
|
||||||
DrawerEntry(ChatScreen, "Chat", Icons.AutoMirrored.Filled.Chat, DrawerSection.CONVERSE),
|
DrawerEntry(ChatScreen, "Chat", Icons.AutoMirrored.Filled.Chat, DrawerSection.CONVERSE),
|
||||||
|
DrawerEntry(HistoryScreen, "History", Icons.Filled.History, DrawerSection.CONVERSE),
|
||||||
DrawerEntry(ProfilesScreen, "Profiles", Icons.Filled.AccountCircle, DrawerSection.CONVERSE),
|
DrawerEntry(ProfilesScreen, "Profiles", Icons.Filled.AccountCircle, DrawerSection.CONVERSE),
|
||||||
// Automate
|
// Automate
|
||||||
DrawerEntry(CronJobsScreen, "Cron", Icons.Filled.Schedule, DrawerSection.AUTOMATE),
|
DrawerEntry(CronJobsScreen, "Cron", Icons.Filled.Schedule, DrawerSection.AUTOMATE),
|
||||||
@@ -339,6 +343,12 @@ fun MainNavigation(sessionId: String? = null) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entry<HistoryScreen> {
|
||||||
|
HistoryScreenContent(
|
||||||
|
onOpenDrawer = { openDrawer() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
entry<SettingsScreen> {
|
entry<SettingsScreen> {
|
||||||
SettingsScreenContent(
|
SettingsScreenContent(
|
||||||
onBack = { NavigationController.goBack() },
|
onBack = { NavigationController.goBack() },
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import androidx.navigation3.runtime.NavKey
|
|||||||
*/
|
*/
|
||||||
object NavigationController {
|
object NavigationController {
|
||||||
var backStack: NavBackStack<NavKey>? = null
|
var backStack: NavBackStack<NavKey>? = null
|
||||||
|
var pendingSessionId: String? = null
|
||||||
|
|
||||||
// Bottom-nav primary screens — dynamic, updated by Navigation.kt via
|
// Bottom-nav primary screens — dynamic, updated by Navigation.kt via
|
||||||
// updatePrimaryScreens() when the user customises the bottom nav bar.
|
// updatePrimaryScreens() when the user customises the bottom nav bar.
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import kotlinx.serialization.Serializable
|
|||||||
|
|
||||||
@Serializable data object GatewayScreen : NavKey
|
@Serializable data object GatewayScreen : NavKey
|
||||||
|
|
||||||
|
@Serializable data object HistoryScreen : NavKey
|
||||||
|
|
||||||
@Serializable data object ProfilesScreen : NavKey
|
@Serializable data object ProfilesScreen : NavKey
|
||||||
|
|
||||||
@Serializable data object ToolsetsScreen : NavKey
|
@Serializable data object ToolsetsScreen : NavKey
|
||||||
|
|||||||
@@ -47,9 +47,7 @@ import androidx.compose.foundation.text.KeyboardOptions
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.Send
|
import androidx.compose.material.icons.automirrored.filled.Send
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.History
|
|
||||||
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||||
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
||||||
import androidx.compose.material.icons.filled.Search
|
import androidx.compose.material.icons.filled.Search
|
||||||
@@ -58,11 +56,9 @@ import androidx.compose.material3.AlertDialog
|
|||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.DropdownMenu
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.FilledTonalButton
|
import androidx.compose.material3.FilledTonalButton
|
||||||
import androidx.compose.material3.FloatingActionButton
|
import androidx.compose.material3.FloatingActionButton
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -101,6 +97,7 @@ import androidx.compose.ui.unit.sp
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.m57.hermescontrol.NavigationController
|
||||||
import com.m57.hermescontrol.R
|
import com.m57.hermescontrol.R
|
||||||
import com.m57.hermescontrol.data.local.AuthManager
|
import com.m57.hermescontrol.data.local.AuthManager
|
||||||
import com.m57.hermescontrol.data.ws.ConnectionStatus
|
import com.m57.hermescontrol.data.ws.ConnectionStatus
|
||||||
@@ -132,11 +129,15 @@ fun ChatScreen(
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val lifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current
|
val lifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current
|
||||||
|
|
||||||
// Switch to the session from a notification tap when provided.
|
// Switch to the session from a notification tap or history screen when provided.
|
||||||
// Runs once per sessionId value (re-runs if a different notification taps in).
|
val pendingSessionId = NavigationController.pendingSessionId
|
||||||
LaunchedEffect(sessionId) {
|
LaunchedEffect(sessionId, pendingSessionId) {
|
||||||
if (!sessionId.isNullOrBlank()) {
|
val target = if (!sessionId.isNullOrBlank()) sessionId else pendingSessionId
|
||||||
viewModel.switchSession(sessionId)
|
if (!target.isNullOrBlank()) {
|
||||||
|
viewModel.switchSession(target)
|
||||||
|
if (target == pendingSessionId) {
|
||||||
|
NavigationController.pendingSessionId = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,76 +286,6 @@ fun ChatScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Session picker
|
|
||||||
Box {
|
|
||||||
IconButton(onClick = {
|
|
||||||
viewModel.loadSessions()
|
|
||||||
viewModel.toggleSessionPicker()
|
|
||||||
}) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Filled.History,
|
|
||||||
contentDescription = "Sessions",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
DropdownMenu(
|
|
||||||
expanded = state.showSessionPicker,
|
|
||||||
onDismissRequest = { viewModel.toggleSessionPicker() },
|
|
||||||
) {
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = {
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Icon(
|
|
||||||
Icons.Filled.Add,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(18.dp),
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text("New Session")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
viewModel.createNewSession()
|
|
||||||
viewModel.toggleSessionPicker()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if (state.sessions.isNotEmpty()) {
|
|
||||||
HorizontalDivider()
|
|
||||||
}
|
|
||||||
state.sessions.forEach { session ->
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = {
|
|
||||||
Column {
|
|
||||||
Text(
|
|
||||||
text = session.title,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
maxLines = 1,
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "${session.messageCount} messages",
|
|
||||||
style =
|
|
||||||
MaterialTheme.typography.labelSmall.copy(
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onClick = { viewModel.switchSession(session.id) },
|
|
||||||
trailingIcon = {
|
|
||||||
if (session.id == state.currentSessionId) {
|
|
||||||
Icon(
|
|
||||||
Icons.Filled.ArrowDropDown,
|
|
||||||
contentDescription = "Current",
|
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
|
||||||
modifier = Modifier.size(18.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search toggle
|
// Search toggle
|
||||||
IconButton(onClick = { viewModel.toggleSearch() }) {
|
IconButton(onClick = { viewModel.toggleSearch() }) {
|
||||||
Icon(
|
Icon(
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
package com.m57.hermescontrol.ui.sessions
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
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.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
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.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.m57.hermescontrol.ChatScreen
|
||||||
|
import com.m57.hermescontrol.NavigationController
|
||||||
|
import com.m57.hermescontrol.theme.LocalSpacing
|
||||||
|
import com.m57.hermescontrol.ui.common.EmptyState
|
||||||
|
import com.m57.hermescontrol.ui.common.ErrorState
|
||||||
|
import com.m57.hermescontrol.ui.common.HermesScaffold
|
||||||
|
import com.m57.hermescontrol.ui.common.LoadingState
|
||||||
|
import com.m57.hermescontrol.ui.common.listContentPadding
|
||||||
|
import com.m57.hermescontrol.ui.common.listItemSpacing
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SessionsScreen(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onOpenDrawer: (() -> Unit)? = null,
|
||||||
|
viewModel: SessionsViewModel = viewModel { SessionsViewModel() },
|
||||||
|
) {
|
||||||
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
val spacing = LocalSpacing.current
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
viewModel.loadSessions()
|
||||||
|
}
|
||||||
|
|
||||||
|
HermesScaffold(
|
||||||
|
title = { Text("History") },
|
||||||
|
onOpenDrawer = onOpenDrawer,
|
||||||
|
isRefreshing = state.isLoading,
|
||||||
|
onRefresh = { viewModel.loadSessions() },
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
when {
|
||||||
|
state.isLoading && state.sessions.isEmpty() -> {
|
||||||
|
LoadingState()
|
||||||
|
}
|
||||||
|
|
||||||
|
state.errorMessage != null -> {
|
||||||
|
ErrorState(
|
||||||
|
message = state.errorMessage ?: "Unknown error",
|
||||||
|
onRetry = { viewModel.loadSessions() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
state.sessions.isEmpty() -> {
|
||||||
|
EmptyState(
|
||||||
|
title = "No history",
|
||||||
|
subtitle = "Previous chat sessions from Hermes will appear here.",
|
||||||
|
icon = Icons.Filled.History,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
contentPadding = listContentPadding,
|
||||||
|
verticalArrangement = listItemSpacing,
|
||||||
|
) {
|
||||||
|
items(state.sessions) { session ->
|
||||||
|
Card(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
NavigationController.pendingSessionId = session.id
|
||||||
|
NavigationController.navigateTo(ChatScreen)
|
||||||
|
},
|
||||||
|
colors =
|
||||||
|
CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(spacing.md),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = session.title ?: "Untitled",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(spacing.xs))
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "${session.message_count ?: 0} messages",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
if (!session.status.isNullOrBlank()) {
|
||||||
|
Spacer(modifier = Modifier.width(spacing.sm))
|
||||||
|
Text(
|
||||||
|
text = "• ${session.status}",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
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.NetworkResult
|
||||||
|
import com.m57.hermescontrol.data.remote.safeApiCall
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
data class SessionsUiState(
|
||||||
|
val isLoading: Boolean = false,
|
||||||
|
val sessions: List<SessionInfo> = emptyList(),
|
||||||
|
val errorMessage: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
class SessionsViewModel : ViewModel() {
|
||||||
|
private val _uiState = MutableStateFlow(SessionsUiState())
|
||||||
|
val uiState: StateFlow<SessionsUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
fun loadSessions() {
|
||||||
|
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
||||||
|
viewModelScope.launch {
|
||||||
|
val result =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
safeApiCall { ApiClient.hermesApi.getSessions() }
|
||||||
|
}
|
||||||
|
when (result) {
|
||||||
|
is NetworkResult.Success -> {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
sessions = result.data?.sessions.orEmpty(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is NetworkResult.Failure -> {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
errorMessage = "Failed to load sessions: ${result.error.message}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -94,6 +94,7 @@ class SettingsViewModel : ViewModel() {
|
|||||||
"ProfilesScreen" to "Profiles",
|
"ProfilesScreen" to "Profiles",
|
||||||
"WebhooksScreen" to "Webhooks",
|
"WebhooksScreen" to "Webhooks",
|
||||||
"GatewayScreen" to "Gateway",
|
"GatewayScreen" to "Gateway",
|
||||||
|
"HistoryScreen" to "History",
|
||||||
"ToolsetsScreen" to "Toolsets",
|
"ToolsetsScreen" to "Toolsets",
|
||||||
"PluginsScreen" to "Plugins",
|
"PluginsScreen" to "Plugins",
|
||||||
"ConfigScreen" to "Config",
|
"ConfigScreen" to "Config",
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import com.m57.hermescontrol.data.model.PluginInfo
|
|||||||
import com.m57.hermescontrol.data.model.PluginsHubResponse
|
import com.m57.hermescontrol.data.model.PluginsHubResponse
|
||||||
import com.m57.hermescontrol.data.model.ProfileInfo
|
import com.m57.hermescontrol.data.model.ProfileInfo
|
||||||
import com.m57.hermescontrol.data.model.ProfilesResponse
|
import com.m57.hermescontrol.data.model.ProfilesResponse
|
||||||
|
import com.m57.hermescontrol.data.model.SessionInfo
|
||||||
|
import com.m57.hermescontrol.data.model.SessionListResponse
|
||||||
import com.m57.hermescontrol.data.model.Skill
|
import com.m57.hermescontrol.data.model.Skill
|
||||||
import com.m57.hermescontrol.data.model.StatusResponse
|
import com.m57.hermescontrol.data.model.StatusResponse
|
||||||
import com.m57.hermescontrol.data.model.SystemStatsResponse
|
import com.m57.hermescontrol.data.model.SystemStatsResponse
|
||||||
@@ -34,6 +36,7 @@ import com.m57.hermescontrol.ui.keys.KeysViewModel
|
|||||||
import com.m57.hermescontrol.ui.logs.LogsViewModel
|
import com.m57.hermescontrol.ui.logs.LogsViewModel
|
||||||
import com.m57.hermescontrol.ui.model.ModelViewModel
|
import com.m57.hermescontrol.ui.model.ModelViewModel
|
||||||
import com.m57.hermescontrol.ui.plugins.PluginsViewModel
|
import com.m57.hermescontrol.ui.plugins.PluginsViewModel
|
||||||
|
import com.m57.hermescontrol.ui.sessions.SessionsViewModel
|
||||||
import com.m57.hermescontrol.ui.skills.SkillsViewModel
|
import com.m57.hermescontrol.ui.skills.SkillsViewModel
|
||||||
import com.m57.hermescontrol.ui.system.SystemViewModel
|
import com.m57.hermescontrol.ui.system.SystemViewModel
|
||||||
import io.mockk.*
|
import io.mockk.*
|
||||||
@@ -407,6 +410,44 @@ class E2eIntegrationTest {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sessions History Screen:
|
||||||
|
@Test
|
||||||
|
fun testSessionsListing_success() =
|
||||||
|
runTest {
|
||||||
|
val session = SessionInfo("session-123", "Session 1", "2026-06-15T15:10:00Z", 5, "active")
|
||||||
|
coEvery { mockApiService.getSessions() } returns Response.success(SessionListResponse(listOf(session)))
|
||||||
|
|
||||||
|
val viewModel = SessionsViewModel()
|
||||||
|
viewModel.loadSessions()
|
||||||
|
assertTrue(viewModel.uiState.value.isLoading)
|
||||||
|
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
assertFalse(viewModel.uiState.value.isLoading)
|
||||||
|
assertEquals(1, viewModel.uiState.value.sessions.size)
|
||||||
|
assertEquals("session-123", viewModel.uiState.value.sessions[0].id)
|
||||||
|
assertEquals("Session 1", viewModel.uiState.value.sessions[0].title)
|
||||||
|
assertEquals(5, viewModel.uiState.value.sessions[0].message_count)
|
||||||
|
assertEquals("active", viewModel.uiState.value.sessions[0].status)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSessionsLoad_failure() =
|
||||||
|
runTest {
|
||||||
|
coEvery { mockApiService.getSessions() } returns createErrorResponse(500)
|
||||||
|
|
||||||
|
val viewModel = SessionsViewModel()
|
||||||
|
viewModel.loadSessions()
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
assertFalse(viewModel.uiState.value.isLoading)
|
||||||
|
assertNotNull(viewModel.uiState.value.errorMessage)
|
||||||
|
assertTrue(
|
||||||
|
viewModel.uiState.value.errorMessage!!
|
||||||
|
.contains("HTTP 500"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testCronJobsRefresh() =
|
fun testCronJobsRefresh() =
|
||||||
runTest {
|
runTest {
|
||||||
|
|||||||
Reference in New Issue
Block a user