feat(memory): dedicated Memory screen (moved out of System tab) (#835)
* feat(memory): dedicated Memory screen — providers, builtin files & reset (moved from System tab) * feat(memory): move self-improvement & learning activity section from System tab to Memory screen * fix(memory): remove top padding, single-line reset buttons, providers spacing * fix(memory): tighten provider card padding * fix(memory): distinct AutoStories icon for Memory tab * chore: ignore local debug apk * feat(ui): fresh icon set for drawer screens * feat(ui): custom Neurology icon for Memory tab * fix(ui): theme-driven fill for NeurologyIcon (checkColorLiterals) The custom Neurology vector used SolidColor(Color.Black), tripping the hardcoded-color guard (issue #622). The fill only contributes alpha — Icon()'s tint replaces the RGB at draw time, mirroring Material's own icon set — so the literal belongs in theme/ like every other hardcoded color: new IconDefaultFill token, scoped there by the guard's design. --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
fd5d851405
commit
7d00d7b81b
13 files changed
+764
-313
No files matched your search
@@ -39,6 +39,8 @@ import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable data object McpServersScreen : NavKey
|
||||
|
||||
@Serializable data object MemoryScreen : NavKey
|
||||
|
||||
@Serializable data object WebhooksScreen : NavKey
|
||||
|
||||
@Serializable data object ModelScreen : NavKey
|
||||
|
||||
@@ -3,7 +3,6 @@ package com.m57.hermescontrol
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Chat
|
||||
import androidx.compose.material.icons.automirrored.filled.ListAlt
|
||||
import androidx.compose.material.icons.filled.AccountBalanceWallet
|
||||
import androidx.compose.material.icons.filled.AccountCircle
|
||||
import androidx.compose.material.icons.filled.BarChart
|
||||
@@ -11,23 +10,27 @@ import androidx.compose.material.icons.filled.Bolt
|
||||
import androidx.compose.material.icons.filled.Build
|
||||
import androidx.compose.material.icons.filled.Cloud
|
||||
import androidx.compose.material.icons.filled.Code
|
||||
import androidx.compose.material.icons.filled.Dashboard
|
||||
import androidx.compose.material.icons.filled.Devices
|
||||
import androidx.compose.material.icons.filled.Dns
|
||||
import androidx.compose.material.icons.filled.EmojiEvents
|
||||
import androidx.compose.material.icons.filled.Extension
|
||||
import androidx.compose.material.icons.filled.Folder
|
||||
import androidx.compose.material.icons.filled.History
|
||||
import androidx.compose.material.icons.filled.HistoryEdu
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.Inventory2
|
||||
import androidx.compose.material.icons.filled.Key
|
||||
import androidx.compose.material.icons.filled.Memory
|
||||
import androidx.compose.material.icons.filled.Power
|
||||
import androidx.compose.material.icons.filled.Psychology
|
||||
import androidx.compose.material.icons.filled.Schedule
|
||||
import androidx.compose.material.icons.filled.Sensors
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.filled.ViewKanban
|
||||
import androidx.compose.material.icons.filled.Webhook
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import com.m57.hermescontrol.ui.common.NeurologyIcon
|
||||
import com.m57.hermescontrol.ui.achievements.AchievementsScreen as AchievementsScreenContent
|
||||
import com.m57.hermescontrol.ui.analytics.AnalyticsScreen as AnalyticsScreenContent
|
||||
import com.m57.hermescontrol.ui.billing.BillingScreen as BillingScreenContent
|
||||
@@ -41,6 +44,7 @@ import com.m57.hermescontrol.ui.kanban.KanbanScreen as KanbanScreenContent
|
||||
import com.m57.hermescontrol.ui.keys.KeysScreen as KeysScreenContent
|
||||
import com.m57.hermescontrol.ui.logs.LogsScreen as LogsScreenContent
|
||||
import com.m57.hermescontrol.ui.mcp.McpServersScreen as McpServersScreenContent
|
||||
import com.m57.hermescontrol.ui.memory.MemoryScreen as MemoryScreenContent
|
||||
import com.m57.hermescontrol.ui.model.ModelScreen as ModelScreenContent
|
||||
import com.m57.hermescontrol.ui.pairing.PairingScreen as PairingScreenContent
|
||||
import com.m57.hermescontrol.ui.plugins.PluginsScreen as PluginsScreenContent
|
||||
@@ -107,13 +111,13 @@ object ScreenRegistry {
|
||||
ScreenDefinition(
|
||||
KanbanScreen,
|
||||
R.string.screen_kanban,
|
||||
Icons.Filled.Dashboard,
|
||||
Icons.Filled.ViewKanban,
|
||||
DrawerSection.AUTOMATE,
|
||||
) { sessionId, openDrawer -> KanbanScreenContent(onOpenDrawer = openDrawer) },
|
||||
ScreenDefinition(
|
||||
SkillsScreen,
|
||||
R.string.screen_skills,
|
||||
Icons.Filled.Extension,
|
||||
Icons.Filled.Inventory2,
|
||||
DrawerSection.CONFIGURE,
|
||||
) { sessionId, openDrawer -> SkillsScreenContent(onOpenDrawer = openDrawer) },
|
||||
ScreenDefinition(
|
||||
@@ -125,7 +129,7 @@ object ScreenRegistry {
|
||||
ScreenDefinition(
|
||||
PluginsScreen,
|
||||
R.string.screen_plugins,
|
||||
Icons.Filled.Memory,
|
||||
Icons.Filled.Extension,
|
||||
DrawerSection.CONFIGURE,
|
||||
) { sessionId, openDrawer -> PluginsScreenContent(onOpenDrawer = openDrawer) },
|
||||
ScreenDefinition(
|
||||
@@ -137,9 +141,15 @@ object ScreenRegistry {
|
||||
ScreenDefinition(
|
||||
McpServersScreen,
|
||||
R.string.screen_mcp_servers,
|
||||
Icons.Filled.Dashboard,
|
||||
Icons.Filled.Power,
|
||||
DrawerSection.CONFIGURE,
|
||||
) { sessionId, openDrawer -> McpServersScreenContent(onOpenDrawer = openDrawer) },
|
||||
ScreenDefinition(
|
||||
MemoryScreen,
|
||||
R.string.screen_memory,
|
||||
NeurologyIcon,
|
||||
DrawerSection.CONFIGURE,
|
||||
) { sessionId, openDrawer -> MemoryScreenContent(onOpenDrawer = openDrawer) },
|
||||
ScreenDefinition(
|
||||
ModelScreen,
|
||||
R.string.screen_models,
|
||||
@@ -161,7 +171,7 @@ object ScreenRegistry {
|
||||
ScreenDefinition(
|
||||
ChannelsScreen,
|
||||
R.string.screen_channels,
|
||||
Icons.AutoMirrored.Filled.ListAlt,
|
||||
Icons.Filled.Sensors,
|
||||
DrawerSection.CONFIGURE,
|
||||
) { sessionId, openDrawer -> ChannelsScreenContent(onOpenDrawer = openDrawer) },
|
||||
ScreenDefinition(
|
||||
@@ -179,7 +189,7 @@ object ScreenRegistry {
|
||||
ScreenDefinition(
|
||||
SystemScreen,
|
||||
R.string.screen_system,
|
||||
Icons.Filled.Info,
|
||||
Icons.Filled.Dns,
|
||||
DrawerSection.INSPECT,
|
||||
) { sessionId, openDrawer -> SystemScreenContent(onOpenDrawer = openDrawer) },
|
||||
ScreenDefinition(
|
||||
|
||||
@@ -106,3 +106,10 @@ val CodeDiffDeleteBg = Color(0x26FF5C5C)
|
||||
val CodeDiffDeleteText = Color(0xFFFF5C5C)
|
||||
val CodeDiffHunkBg = Color(0x264DA8FF)
|
||||
val CodeDiffHunkText = Color(0xFF4DA8FF)
|
||||
|
||||
// ── Custom vector icons ─────────────────────────────────────────────
|
||||
// Default fill for hand-declared ImageVector icons (e.g. NeurologyIcon).
|
||||
// Mirrors Material's own icon set: vectors are drawn opaque black and
|
||||
// Icon()'s tint (LocalContentColor) replaces the RGB at draw time — only
|
||||
// the alpha channel survives, so this must stay opaque.
|
||||
val IconDefaultFill = Color.Black
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.m57.hermescontrol.ui.common
|
||||
|
||||
import androidx.compose.ui.graphics.PathFillType
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.StrokeJoin
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.addPathNodes
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.m57.hermescontrol.theme.IconDefaultFill
|
||||
|
||||
/**
|
||||
* Material Symbols "neurology" (outlined, 24dp) — the glyph ships on
|
||||
* fonts.google.com but is NOT in the material-icons-extended snapshot the
|
||||
* app depends on, so it is declared here as a custom vector from Google's
|
||||
* original path data (viewBox 0 -960 960 960, translated into compose space).
|
||||
*/
|
||||
val NeurologyIcon: ImageVector by lazy {
|
||||
ImageVector.Builder(
|
||||
name = "Neurology",
|
||||
defaultWidth = 24.dp,
|
||||
defaultHeight = 24.dp,
|
||||
viewportWidth = 960f,
|
||||
viewportHeight = 960f,
|
||||
).apply {
|
||||
addGroup(
|
||||
name = "translate",
|
||||
rotate = 0f,
|
||||
pivotX = 0f,
|
||||
pivotY = 0f,
|
||||
scaleX = 1f,
|
||||
scaleY = 1f,
|
||||
translationX = 0f,
|
||||
translationY = 960f,
|
||||
clipPathData = emptyList(),
|
||||
)
|
||||
addPath(
|
||||
pathData = addPathNodes(PATH),
|
||||
pathFillType = PathFillType.NonZero,
|
||||
name = "NeurologyPath",
|
||||
fill = SolidColor(IconDefaultFill),
|
||||
fillAlpha = 1f,
|
||||
stroke = null,
|
||||
strokeAlpha = 1f,
|
||||
strokeLineWidth = 1f,
|
||||
strokeLineCap = StrokeCap.Butt,
|
||||
strokeLineJoin = StrokeJoin.Miter,
|
||||
strokeLineMiter = 4f,
|
||||
trimPathStart = 0f,
|
||||
trimPathEnd = 1f,
|
||||
trimPathOffset = 0f,
|
||||
)
|
||||
}.build()
|
||||
}
|
||||
|
||||
private const val PATH =
|
||||
"M390-120q-51 0-88-35.5T260-241q-60-8-100-53t-40-106q0-21 5.5-41.5T142-480q-11-18-16.5-38t-5.5-42q0-61 40-105.5t99-52.5q3-51 41-86.5t90-35.5q26 0 48.5 10t41.5 27q18-17 41-27t49-10q52 0 89.5 35t40.5 86q59 8 99.5 53T840-560q0 22-5.5 42T818-480q11 18 16.5 38.5T840-400q0 62-40.5 106.5T699-241q-5 50-41.5 85.5T570-120q-25 0-48.5-9.5T480-156q-19 17-42 26.5t-48 9.5Zm130-590v460q0 21 14.5 35.5T570-200q20 0 34.5-16t15.5-36q-21-8-38.5-21.5T550-306q-10-14-7.5-30t16.5-26q14-10 30-7.5t26 16.5q11 16 28 24.5t37 8.5q33 0 56.5-23.5T760-400q0-5-.5-10t-2.5-10q-17 10-36.5 15t-40.5 5q-17 0-28.5-11.5T640-440q0-17 11.5-28.5T680-480q33 0 56.5-23.5T760-560q0-33-23.5-56T680-640q-11 18-28.5 31.5T613-587q-16 6-31-1t-20-23q-5-16 1.5-31t22.5-20q15-5 24.5-18t9.5-30q0-21-14.5-35.5T570-760q-21 0-35.5 14.5T520-710Zm-80 460v-460q0-21-14.5-35.5T390-760q-21 0-35.5 14.5T340-710q0 16 9 29.5t24 18.5q16 5 23 20t2 31q-6 16-21 23t-31 1q-21-8-38.5-21.5T279-640q-32 1-55.5 24.5T200-560q0 33 23.5 56.5T280-480q17 0 28.5 11.5T320-440q0 17-11.5 28.5T280-400q-21 0-40.5-5T203-420q-2 5-2.5 10t-.5 10q0 33 23.5 56.5T280-320q20 0 37-8.5t28-24.5q10-14 26-16.5t30 7.5q14 10 16.5 26t-7.5 30q-14 19-32 33t-39 22q1 20 16 35.5t35 15.5q21 0 35.5-14.5T440-250Zm40-230Z"
|
||||
@@ -0,0 +1,434 @@
|
||||
@file:OptIn(
|
||||
androidx.compose.material3.ExperimentalMaterial3Api::class,
|
||||
androidx.compose.foundation.layout.ExperimentalLayoutApi::class,
|
||||
)
|
||||
|
||||
package com.m57.hermescontrol.ui.memory
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
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.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.m57.hermescontrol.MemoryProviderDetailKey
|
||||
import com.m57.hermescontrol.NavigationController
|
||||
import com.m57.hermescontrol.R
|
||||
import com.m57.hermescontrol.data.model.LearningGraphResponse
|
||||
import com.m57.hermescontrol.data.model.MemoryProviderStatusRow
|
||||
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.NavIcon
|
||||
import com.m57.hermescontrol.ui.common.SectionHeader
|
||||
import com.m57.hermescontrol.ui.common.SkeletonListState
|
||||
import com.m57.hermescontrol.ui.common.StatCard
|
||||
import com.m57.hermescontrol.ui.common.StatusBadge
|
||||
import com.m57.hermescontrol.ui.common.StatusBadgeType
|
||||
import com.m57.hermescontrol.ui.common.ToastEffect
|
||||
import com.m57.hermescontrol.ui.common.listContentPadding
|
||||
import com.m57.hermescontrol.ui.common.listItemSpacing
|
||||
import com.m57.hermescontrol.ui.plugins.memoryProviderStatusLabel
|
||||
import com.m57.hermescontrol.ui.plugins.memoryProviderStatusType
|
||||
|
||||
/**
|
||||
* Memory management home — owns the memory surface that used to live in the
|
||||
* System tab (active provider, builtin files + reset) plus the provider list
|
||||
* that drills into per-provider config/setup (issue #783).
|
||||
*/
|
||||
@Composable
|
||||
fun MemoryScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
onOpenDrawer: (() -> Unit)? = null,
|
||||
viewModel: MemoryViewModel = viewModel { MemoryViewModel() },
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
var resetTarget by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.load()
|
||||
}
|
||||
|
||||
ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast)
|
||||
|
||||
resetTarget?.let { target ->
|
||||
AlertDialog(
|
||||
onDismissRequest = { resetTarget = null },
|
||||
title = { Text(stringResource(R.string.memory_reset_confirm_title)) },
|
||||
text = { Text(stringResource(R.string.memory_reset_confirm_desc, target)) },
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
resetTarget = null
|
||||
viewModel.resetMemory(target)
|
||||
},
|
||||
) {
|
||||
Text(stringResource(R.string.action_ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { resetTarget = null }) {
|
||||
Text(stringResource(R.string.system_confirm_cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
HermesScaffold(
|
||||
title = { Text(stringResource(R.string.screen_memory)) },
|
||||
navigationIcon = onOpenDrawer?.let { NavIcon.Menu(it) },
|
||||
isRefreshing = state.isLoading,
|
||||
onRefresh = { viewModel.load() },
|
||||
modifier = modifier,
|
||||
) { paddingValues ->
|
||||
when {
|
||||
state.isLoading && state.memory == null -> {
|
||||
SkeletonListState(modifier = Modifier)
|
||||
}
|
||||
|
||||
state.errorMessage != null && state.memory == null -> {
|
||||
ErrorState(
|
||||
message = state.errorMessage ?: "",
|
||||
onRetry = { viewModel.load() },
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
|
||||
state.memory == null -> {
|
||||
EmptyState(
|
||||
title = stringResource(R.string.memory_empty_title),
|
||||
subtitle = stringResource(R.string.memory_empty_desc),
|
||||
onAction = { viewModel.load() },
|
||||
actionLabel = stringResource(R.string.content_desc_refresh),
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
MemoryContent(
|
||||
memory = state.memory!!,
|
||||
learningGraph = state.learningGraph,
|
||||
resetting = state.resetting,
|
||||
onResetRequest = { resetTarget = it },
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MemoryContent(
|
||||
memory: com.m57.hermescontrol.data.model.MemoryResponse,
|
||||
learningGraph: LearningGraphResponse?,
|
||||
resetting: String?,
|
||||
onResetRequest: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
contentPadding = listContentPadding,
|
||||
verticalArrangement = listItemSpacing,
|
||||
) {
|
||||
// Active provider + builtin files
|
||||
item {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors =
|
||||
CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
if (memory.active.isNullOrBlank()) {
|
||||
Text(
|
||||
text = stringResource(R.string.memory_builtin),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = stringResource(R.string.memory_active, memory.active),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
}
|
||||
|
||||
memory.builtin_files?.let { files ->
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text =
|
||||
stringResource(
|
||||
R.string.memory_builtin_sizes,
|
||||
files.memory?.let { formatBytes(it) } ?: "?",
|
||||
files.user?.let { formatBytes(it) } ?: "?",
|
||||
),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
FilledTonalButton(
|
||||
onClick = { onResetRequest("memory") },
|
||||
enabled = resetting == null,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.memory_reset_memory),
|
||||
maxLines = 1,
|
||||
softWrap = false,
|
||||
)
|
||||
}
|
||||
FilledTonalButton(
|
||||
onClick = { onResetRequest("user") },
|
||||
enabled = resetting == null,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.memory_reset_user),
|
||||
maxLines = 1,
|
||||
softWrap = false,
|
||||
)
|
||||
}
|
||||
FilledTonalButton(
|
||||
onClick = { onResetRequest("all") },
|
||||
enabled = resetting == null,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.memory_reset_all),
|
||||
maxLines = 1,
|
||||
softWrap = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Providers
|
||||
if (memory.providers.isNotEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(R.string.memory_providers_heading).uppercase(),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold,
|
||||
modifier = Modifier.padding(start = 16.dp, end = 16.dp),
|
||||
)
|
||||
}
|
||||
items(memory.providers, key = { it.name }) { provider ->
|
||||
MemoryProviderRow(
|
||||
provider = provider,
|
||||
isActive = provider.name == memory.active,
|
||||
onClick = {
|
||||
NavigationController.navigateTo(
|
||||
MemoryProviderDetailKey(
|
||||
name = provider.name,
|
||||
label = provider.name,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Self-Improvement & learning activity (moved from System tab)
|
||||
item {
|
||||
SelfImprovementSection(graph = learningGraph)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SelfImprovementSection(graph: LearningGraphResponse?) {
|
||||
SectionHeader(title = stringResource(R.string.memory_sec_self_improvement))
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors =
|
||||
CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
text = stringResource(R.string.memory_self_improvement_desc),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
if (graph == null || graph.nodes.isEmpty()) {
|
||||
Text(
|
||||
text = stringResource(R.string.memory_self_improvement_no_activity),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
|
||||
)
|
||||
} else {
|
||||
val skillNodes = graph.nodes.filter { it.kind == "skill" }
|
||||
val memoryNodes = graph.nodes.filter { it.kind == "memory" }
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
StatCard(
|
||||
label = "Learned Skills",
|
||||
value = "${skillNodes.size}",
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
StatCard(
|
||||
label = "Memories & Facts",
|
||||
value = "${memoryNodes.size}",
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = "Recent Agent Self-Improvement Events",
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
val sortedNodes =
|
||||
graph.nodes
|
||||
.filter { it.timestamp != null || it.kind == "skill" }
|
||||
.sortedByDescending { it.timestamp ?: 0L }
|
||||
.take(8)
|
||||
|
||||
sortedNodes.forEach { node ->
|
||||
Surface(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
val icon = if (node.kind == "skill") "⚡" else "🧠"
|
||||
Text(text = icon, fontSize = 16.sp)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = node.label,
|
||||
style =
|
||||
MaterialTheme.typography.bodyMedium.copy(
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
val cat = node.category ?: node.kind
|
||||
val creator = node.createdBy?.let { " • by $it" } ?: ""
|
||||
Text(
|
||||
text = "[$cat]$creator",
|
||||
style = MaterialTheme.typography.bodySmall.copy(fontSize = 11.sp),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
node.useCount.takeIf { it > 0 }?.let { uses ->
|
||||
StatusBadge(
|
||||
text = "$uses uses",
|
||||
status = StatusBadgeType.INFO,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MemoryProviderRow(
|
||||
provider: MemoryProviderStatusRow,
|
||||
isActive: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Card(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = provider.name,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
if (isActive) {
|
||||
StatusBadge(
|
||||
text = stringResource(R.string.memory_provider_active),
|
||||
status = StatusBadgeType.SUCCESS,
|
||||
)
|
||||
}
|
||||
StatusBadge(
|
||||
text = memoryProviderStatusLabel(provider.status),
|
||||
status = memoryProviderStatusType(provider.status),
|
||||
)
|
||||
}
|
||||
if (provider.description.isNotBlank()) {
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(
|
||||
text = provider.description,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatBytes(bytes: Long): String =
|
||||
when {
|
||||
bytes >= 1024 * 1024 -> "%.1f MB".format(bytes / (1024.0 * 1024.0))
|
||||
bytes >= 1024 -> "%.1f KB".format(bytes / 1024.0)
|
||||
else -> "$bytes B"
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.m57.hermescontrol.ui.memory
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.m57.hermescontrol.data.model.LearningGraphResponse
|
||||
import com.m57.hermescontrol.data.model.MemoryResponse
|
||||
import com.m57.hermescontrol.data.remote.ApiClient
|
||||
import com.m57.hermescontrol.data.remote.NetworkResult
|
||||
import com.m57.hermescontrol.data.remote.safeApiCall
|
||||
import com.m57.hermescontrol.ui.common.ToastHost
|
||||
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 MemoryUiState(
|
||||
val isLoading: Boolean = false,
|
||||
val memory: MemoryResponse? = null,
|
||||
val learningGraph: LearningGraphResponse? = null,
|
||||
val errorMessage: String? = null,
|
||||
val toastMessage: String? = null,
|
||||
val resetting: String? = null,
|
||||
)
|
||||
|
||||
/**
|
||||
* Memory management home (moved out of the System tab) — active provider,
|
||||
* builtin memory files with reset, and the provider list that drills into
|
||||
* per-provider config/setup (issue #783). Deliberately no explicit IO hop:
|
||||
* Retrofit suspend calls already run off the caller thread, keeping tests
|
||||
* free of the static Dispatchers mock that bleeds across test classes.
|
||||
*/
|
||||
class MemoryViewModel : ViewModel(), ToastHost {
|
||||
private val _uiState = MutableStateFlow(MemoryUiState())
|
||||
val uiState: StateFlow<MemoryUiState> = _uiState.asStateFlow()
|
||||
|
||||
fun load() {
|
||||
if (_uiState.value.isLoading) return
|
||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
||||
viewModelScope.launch {
|
||||
val result = safeApiCall { ApiClient.hermesApi.getMemory() }
|
||||
when (result) {
|
||||
is NetworkResult.Success ->
|
||||
_uiState.update { it.copy(isLoading = false, memory = result.data) }
|
||||
is NetworkResult.Failure ->
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
errorMessage = result.error.message,
|
||||
)
|
||||
}
|
||||
}
|
||||
loadLearningGraph()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadLearningGraph() {
|
||||
viewModelScope.launch {
|
||||
val result = safeApiCall { ApiClient.hermesApi.getLearningGraph() }
|
||||
if (result is NetworkResult.Success) {
|
||||
_uiState.update { it.copy(learningGraph = result.data) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resetMemory(target: String) {
|
||||
if (_uiState.value.resetting != null) return
|
||||
_uiState.update { it.copy(resetting = target) }
|
||||
viewModelScope.launch {
|
||||
val result = safeApiCall { ApiClient.hermesApi.resetMemory(mapOf("target" to target)) }
|
||||
when (result) {
|
||||
is NetworkResult.Success ->
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
resetting = null,
|
||||
toastMessage = "Memory ($target) reset successfully",
|
||||
)
|
||||
}
|
||||
is NetworkResult.Failure ->
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
resetting = null,
|
||||
toastMessage = "Failed to reset memory: ${result.error.message}",
|
||||
)
|
||||
}
|
||||
}
|
||||
load()
|
||||
}
|
||||
}
|
||||
|
||||
override fun clearToast() {
|
||||
_uiState.update { it.copy(toastMessage = null) }
|
||||
}
|
||||
}
|
||||
@@ -455,7 +455,7 @@ private fun SetupStepRow(step: MemoryProviderSetupResult) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun memoryProviderStatusType(status: String): StatusBadgeType =
|
||||
fun memoryProviderStatusType(status: String): StatusBadgeType =
|
||||
when (status) {
|
||||
"ready" -> StatusBadgeType.SUCCESS
|
||||
"needs_config" -> StatusBadgeType.WARNING
|
||||
|
||||
@@ -21,7 +21,6 @@ import androidx.compose.material.icons.filled.Build
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.CloudDownload
|
||||
import androidx.compose.material.icons.filled.ContentCopy
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.DeleteForever
|
||||
import androidx.compose.material.icons.filled.Devices
|
||||
import androidx.compose.material.icons.filled.HealthAndSafety
|
||||
@@ -49,7 +48,6 @@ import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -70,7 +68,6 @@ import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.m57.hermescontrol.R
|
||||
@@ -152,29 +149,6 @@ fun SystemScreen(
|
||||
)
|
||||
}
|
||||
|
||||
// Memory reset confirmation
|
||||
var resetTarget by remember { mutableStateOf<String?>(null) }
|
||||
if (resetTarget != null) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { resetTarget = null },
|
||||
title = { Text(stringResource(R.string.system_memory_reset_confirm_title)) },
|
||||
text = { Text(stringResource(R.string.system_memory_reset_confirm_desc)) },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
resetTarget?.let { viewModel.resetMemory(it) }
|
||||
resetTarget = null
|
||||
}) {
|
||||
Text(stringResource(R.string.action_ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { resetTarget = null }) {
|
||||
Text(stringResource(R.string.system_confirm_cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Credential remove confirmation
|
||||
var credToRemove by remember { mutableStateOf<Pair<String, Int>?>(null) }
|
||||
if (credToRemove != null) {
|
||||
@@ -290,15 +264,9 @@ fun SystemScreen(
|
||||
// ── 3. Curator ─────────────────────────────────────────
|
||||
curatorSection(state, spacing, statusColors, viewModel)
|
||||
|
||||
// ── 3.5 Self-Improvement & Learning ───────────────────
|
||||
selfImprovementSection(state, spacing, statusColors)
|
||||
|
||||
// ── 4. Gateway ─────────────────────────────────────────
|
||||
gatewaySection(state, spacing, statusColors, viewModel)
|
||||
|
||||
// ── 5. Memory ──────────────────────────────────────────
|
||||
memorySection(state, spacing, resetTarget, { resetTarget = it }, viewModel)
|
||||
|
||||
// ── 6. Credentials ─────────────────────────────────────
|
||||
credentialsSection(state, spacing, viewModel, credToRemove, { credToRemove = it })
|
||||
|
||||
@@ -712,122 +680,6 @@ private fun LazyListScope.curatorSection(
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.selfImprovementSection(
|
||||
state: SystemUiState,
|
||||
spacing: com.m57.hermescontrol.theme.Spacing,
|
||||
statusColors: com.m57.hermescontrol.theme.HermesStatusColors,
|
||||
) {
|
||||
val graph = state.learningGraph
|
||||
item {
|
||||
SectionHeader(title = stringResource(R.string.system_sec_self_improvement))
|
||||
}
|
||||
item {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors =
|
||||
CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(spacing.md)) {
|
||||
Text(
|
||||
text = stringResource(R.string.system_self_improvement_desc),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(spacing.sm))
|
||||
|
||||
if (graph == null || graph.nodes.isEmpty()) {
|
||||
Text(
|
||||
text = stringResource(R.string.system_self_improvement_no_activity),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
|
||||
)
|
||||
} else {
|
||||
val skillNodes = graph.nodes.filter { it.kind == "skill" }
|
||||
val memoryNodes = graph.nodes.filter { it.kind == "memory" }
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(spacing.sm),
|
||||
) {
|
||||
StatCard(
|
||||
label = "Learned Skills",
|
||||
value = "${skillNodes.size}",
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
StatCard(
|
||||
label = "Memories & Facts",
|
||||
value = "${memoryNodes.size}",
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(spacing.md))
|
||||
Text(
|
||||
text = "Recent Agent Self-Improvement Events",
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(spacing.xs))
|
||||
|
||||
val sortedNodes =
|
||||
graph.nodes
|
||||
.filter { it.timestamp != null || it.kind == "skill" }
|
||||
.sortedByDescending { it.timestamp ?: 0L }
|
||||
.take(8)
|
||||
|
||||
sortedNodes.forEach { node ->
|
||||
Surface(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
shape = androidx.compose.foundation.shape.RoundedCornerShape(8.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
val icon = if (node.kind == "skill") "⚡" else "🧠"
|
||||
Text(text = icon, fontSize = 16.sp)
|
||||
Spacer(modifier = Modifier.width(spacing.xs))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = node.label,
|
||||
style =
|
||||
MaterialTheme.typography.bodyMedium.copy(
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
val cat = node.category ?: node.kind
|
||||
val creator = node.createdBy?.let { " • by $it" } ?: ""
|
||||
Text(
|
||||
text = "[$cat]$creator",
|
||||
style = MaterialTheme.typography.bodySmall.copy(fontSize = 11.sp),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
node.useCount.takeIf { it > 0 }?.let { uses ->
|
||||
StatusBadge(
|
||||
text = "$uses uses",
|
||||
status = StatusBadgeType.INFO,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.gatewaySection(
|
||||
state: SystemUiState,
|
||||
spacing: com.m57.hermescontrol.theme.Spacing,
|
||||
@@ -980,100 +832,6 @@ private fun LazyListScope.gatewaySection(
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.memorySection(
|
||||
state: SystemUiState,
|
||||
spacing: com.m57.hermescontrol.theme.Spacing,
|
||||
resetTarget: String?,
|
||||
onResetRequest: (String) -> Unit,
|
||||
viewModel: SystemViewModel,
|
||||
) {
|
||||
state.memory?.let { memory ->
|
||||
item {
|
||||
SectionHeader(title = stringResource(R.string.system_sec_memory))
|
||||
}
|
||||
item {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors =
|
||||
CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(spacing.md)) {
|
||||
// Active provider
|
||||
memory.active?.let { active ->
|
||||
InfoRow(
|
||||
label = stringResource(R.string.system_memory_external, active),
|
||||
value = "",
|
||||
)
|
||||
} ?: InfoRow(
|
||||
label = stringResource(R.string.system_memory_builtin),
|
||||
value = "",
|
||||
)
|
||||
|
||||
// Change in Plugins link
|
||||
InfoRow(
|
||||
label = stringResource(R.string.system_memory_change_plugins),
|
||||
value = "",
|
||||
)
|
||||
|
||||
// Builtin file sizes
|
||||
memory.builtin_files?.let { files ->
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = spacing.sm))
|
||||
InfoRow(
|
||||
label =
|
||||
stringResource(
|
||||
R.string.system_memory_builtin_label,
|
||||
files.memory?.let { formatBytes(it) } ?: "?",
|
||||
files.user?.let { formatBytes(it) } ?: "?",
|
||||
),
|
||||
value = "",
|
||||
)
|
||||
|
||||
// Reset buttons
|
||||
Spacer(modifier = Modifier.height(spacing.sm))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(spacing.sm),
|
||||
) {
|
||||
FilledTonalButton(
|
||||
onClick = { onResetRequest("memory") },
|
||||
modifier = Modifier.weight(1f),
|
||||
contentPadding = actionButtonPadding,
|
||||
) {
|
||||
ActionButtonContent(
|
||||
icon = Icons.Filled.Delete,
|
||||
text = stringResource(R.string.system_memory_reset_memory),
|
||||
)
|
||||
}
|
||||
FilledTonalButton(
|
||||
onClick = { onResetRequest("user") },
|
||||
modifier = Modifier.weight(1f),
|
||||
contentPadding = actionButtonPadding,
|
||||
) {
|
||||
ActionButtonContent(
|
||||
icon = Icons.Filled.Delete,
|
||||
text = stringResource(R.string.system_memory_reset_user),
|
||||
)
|
||||
}
|
||||
FilledTonalButton(
|
||||
onClick = { onResetRequest("all") },
|
||||
modifier = Modifier.weight(1f),
|
||||
contentPadding = actionButtonPadding,
|
||||
) {
|
||||
ActionButtonContent(
|
||||
icon = Icons.Filled.DeleteForever,
|
||||
text = stringResource(R.string.system_memory_reset_all),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.credentialsSection(
|
||||
state: SystemUiState,
|
||||
spacing: com.m57.hermescontrol.theme.Spacing,
|
||||
|
||||
@@ -12,8 +12,6 @@ import com.m57.hermescontrol.data.model.CuratorResponse
|
||||
import com.m57.hermescontrol.data.model.DebugShareResponse
|
||||
import com.m57.hermescontrol.data.model.DoctorResponse
|
||||
import com.m57.hermescontrol.data.model.HookResponse
|
||||
import com.m57.hermescontrol.data.model.LearningGraphResponse
|
||||
import com.m57.hermescontrol.data.model.MemoryResponse
|
||||
import com.m57.hermescontrol.data.model.PortalResponse
|
||||
import com.m57.hermescontrol.data.model.StatusResponse
|
||||
import com.m57.hermescontrol.data.model.SystemStatsResponse
|
||||
@@ -40,8 +38,6 @@ data class SystemUiState(
|
||||
val stats: SystemStatsResponse? = null,
|
||||
val portal: PortalResponse? = null,
|
||||
val curator: CuratorResponse? = null,
|
||||
val memory: MemoryResponse? = null,
|
||||
val learningGraph: LearningGraphResponse? = null,
|
||||
val credentials: List<CredentialPoolProvider> = emptyList(),
|
||||
val checkpoints: CheckpointsResponse? = null,
|
||||
val hooks: HookResponse? = null,
|
||||
@@ -101,8 +97,7 @@ class SystemViewModel :
|
||||
val statusDeferred = async(Dispatchers.IO) { safeApiCall { ApiClient.hermesApi.getStatus() } }
|
||||
val portalDeferred = async(Dispatchers.IO) { safeApiCall { ApiClient.hermesApi.getPortal() } }
|
||||
val curatorDeferred = async(Dispatchers.IO) { safeApiCall { ApiClient.hermesApi.getCurator() } }
|
||||
val memoryDeferred = async(Dispatchers.IO) { safeApiCall { ApiClient.hermesApi.getMemory() } }
|
||||
val learningDeferred = async(Dispatchers.IO) { safeApiCall { ApiClient.hermesApi.getLearningGraph() } }
|
||||
|
||||
val credDeferred = async(Dispatchers.IO) { safeApiCall { ApiClient.hermesApi.getCredentialPool() } }
|
||||
val checkpointsDeferred = async(Dispatchers.IO) { safeApiCall { ApiClient.hermesApi.getCheckpoints() } }
|
||||
val hooksDeferred = async(Dispatchers.IO) { safeApiCall { ApiClient.hermesApi.getHooks() } }
|
||||
@@ -114,8 +109,7 @@ class SystemViewModel :
|
||||
val statusResult = statusDeferred.await()
|
||||
val portalResult = portalDeferred.await()
|
||||
val curatorResult = curatorDeferred.await()
|
||||
val memoryResult = memoryDeferred.await()
|
||||
val learningResult = learningDeferred.await()
|
||||
|
||||
val credResult = credDeferred.await()
|
||||
val checkpointsResult = checkpointsDeferred.await()
|
||||
val hooksResult = hooksDeferred.await()
|
||||
@@ -129,8 +123,6 @@ class SystemViewModel :
|
||||
status = (statusResult as? NetworkResult.Success)?.data,
|
||||
portal = (portalResult as? NetworkResult.Success)?.data,
|
||||
curator = (curatorResult as? NetworkResult.Success)?.data,
|
||||
memory = (memoryResult as? NetworkResult.Success)?.data,
|
||||
learningGraph = (learningResult as? NetworkResult.Success)?.data,
|
||||
credentials =
|
||||
((credResult as? NetworkResult.Success)?.data)?.providers ?: emptyList(),
|
||||
checkpoints = (checkpointsResult as? NetworkResult.Success)?.data,
|
||||
@@ -148,7 +140,6 @@ class SystemViewModel :
|
||||
"status" to statusResult,
|
||||
"portal" to portalResult,
|
||||
"curator" to curatorResult,
|
||||
"memory" to memoryResult,
|
||||
"credentials" to credResult,
|
||||
"checkpoints" to checkpointsResult,
|
||||
"hooks" to hooksResult,
|
||||
@@ -193,15 +184,6 @@ class SystemViewModel :
|
||||
}
|
||||
}
|
||||
|
||||
fun loadMemory() {
|
||||
viewModelScope.launch {
|
||||
val result = withContext(Dispatchers.IO) { safeApiCall { ApiClient.hermesApi.getMemory() } }
|
||||
if (result is NetworkResult.Success) {
|
||||
_uiState.update { it.copy(memory = result.data) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadStatus() {
|
||||
viewModelScope.launch {
|
||||
val result = withContext(Dispatchers.IO) { safeApiCall { ApiClient.hermesApi.getStatus() } }
|
||||
@@ -318,27 +300,6 @@ class SystemViewModel :
|
||||
)
|
||||
}
|
||||
|
||||
// ── Memory actions ─────────────────────────────────────────────────
|
||||
|
||||
fun resetMemory(target: String) {
|
||||
viewModelScope.launch {
|
||||
val result =
|
||||
withContext(Dispatchers.IO) {
|
||||
safeApiCall { ApiClient.hermesApi.resetMemory(mapOf("target" to target)) }
|
||||
}
|
||||
when (result) {
|
||||
is NetworkResult.Success -> {
|
||||
_uiState.update { it.copy(toastMessage = "Memory ($target) reset successfully") }
|
||||
loadMemory()
|
||||
}
|
||||
|
||||
is NetworkResult.Failure -> {
|
||||
_uiState.update { it.copy(toastMessage = "Failed to reset memory: ${result.error.message}") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Credential pool actions ────────────────────────────────────────
|
||||
|
||||
fun updateCredProvider(v: String) {
|
||||
|
||||
Reference in new issue
Block a user