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
@@ -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