diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/SessionListResponse.kt b/app/src/main/java/com/m57/hermescontrol/data/model/SessionListResponse.kt
index 24fd6f48..4dbc7bf2 100644
--- a/app/src/main/java/com/m57/hermescontrol/data/model/SessionListResponse.kt
+++ b/app/src/main/java/com/m57/hermescontrol/data/model/SessionListResponse.kt
@@ -27,7 +27,9 @@ data class SessionInfo(
@Serializable
data class SessionStatsResponse(
val total: Int = 0,
- val active: Int = 0,
+ // Backend GET /api/sessions/stats returns "messages" (total stored
+ // messages) — the value the Sessions stats row shows.
+ val messages: Int = 0,
)
@Serializable
diff --git a/app/src/main/java/com/m57/hermescontrol/ui/sessions/SessionsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/sessions/SessionsScreen.kt
index d3643bf6..a8b5b415 100644
--- a/app/src/main/java/com/m57/hermescontrol/ui/sessions/SessionsScreen.kt
+++ b/app/src/main/java/com/m57/hermescontrol/ui/sessions/SessionsScreen.kt
@@ -37,6 +37,7 @@ import androidx.compose.material.icons.filled.Code
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.DeleteSweep
import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material.icons.filled.Email
import androidx.compose.material.icons.filled.History
import androidx.compose.material.icons.filled.Language
import androidx.compose.material.icons.filled.Search
@@ -53,8 +54,8 @@ import androidx.compose.material3.Checkbox
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
@@ -531,34 +532,26 @@ fun SessionsScreen(
modifier = Modifier.weight(1f),
)
Spacer(modifier = Modifier.width(spacing.sm))
- IconButton(onClick = { viewModel.toggleSelecting() }) {
- Icon(
- imageVector = if (state.isSelecting) Icons.Filled.Close else Icons.Filled.SelectAll,
- contentDescription =
- if (state.isSelecting) {
- stringResource(R.string.content_desc_exit_selection)
- } else {
- stringResource(R.string.content_desc_enter_selection)
- },
- )
- }
- Spacer(modifier = Modifier.width(spacing.sm))
// Empty-session cleanup (issue #787) — grey/disabled when 0.
- IconButton(
- onClick = { viewModel.requestEmptyCleanup() },
- enabled = state.emptyCount > 0,
+ BadgedBox(
+ badge = {
+ if (state.emptyCount > 0) {
+ Badge { Text("${state.emptyCount}") }
+ }
+ },
) {
- BadgedBox(
- badge = {
- if (state.emptyCount > 0) {
- Badge { Text("${state.emptyCount}") }
- }
- },
+ FilledTonalButton(
+ onClick = { viewModel.requestEmptyCleanup() },
+ enabled = state.emptyCount > 0,
+ contentPadding = PaddingValues(horizontal = spacing.md, vertical = 8.dp),
) {
Icon(
imageVector = Icons.Filled.DeleteSweep,
- contentDescription = stringResource(R.string.sessions_empty_cleanup_desc),
+ contentDescription = null,
+ modifier = Modifier.size(18.dp),
)
+ Spacer(modifier = Modifier.width(spacing.xs))
+ Text(stringResource(R.string.sessions_empty_cleanup_desc))
}
}
}
@@ -695,20 +688,20 @@ fun SessionsScreen(
) {
StatCard(
label = stringResource(R.string.sessions_stat_total),
- value = if (state.isLoadingStats) "…" else state.stats.total.toString(),
+ value = if (state.isLoadingStats) "…" else formatCompactCount(state.stats.total),
icon = Icons.Filled.History,
modifier = Modifier.weight(1f).fillMaxHeight(),
)
StatCard(
- label = stringResource(R.string.sessions_stat_active),
- value = if (state.isLoadingStats) "…" else state.stats.active.toString(),
- icon = Icons.Filled.CheckCircle,
- accentColor = statusColors.success,
+ label = stringResource(R.string.sessions_stat_messages),
+ value = if (state.isLoadingStats) "…" else formatCompactCount(state.stats.messages),
+ icon = Icons.Filled.Email,
modifier = Modifier.weight(1f).fillMaxHeight(),
)
- // Prune button card
+ // Prune button — compact card sized to its label, so the
+ // stat cards keep enough width for large counts.
Card(
- modifier = Modifier.weight(1f).fillMaxHeight(),
+ modifier = Modifier.fillMaxHeight(),
colors =
CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainer,
@@ -716,17 +709,22 @@ fun SessionsScreen(
onClick = { viewModel.showPruneDialog() },
) {
Box(
- modifier = Modifier.fillMaxSize().padding(spacing.md),
+ // NOTE: fillMaxHeight only — never fillMaxSize here.
+ // An unweighted Row child with fillMaxSize expands to the
+ // full row width and starves the weighted stat cards.
+ modifier = Modifier.fillMaxHeight().padding(horizontal = spacing.md),
contentAlignment = Alignment.Center,
) {
- Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(spacing.xs),
+ ) {
Icon(
imageVector = Icons.Filled.DeleteSweep,
contentDescription = null,
tint = statusColors.warning,
modifier = Modifier.size(20.dp),
)
- Spacer(modifier = Modifier.height(spacing.xs))
Text(
text = stringResource(R.string.sessions_action_prune),
style = MaterialTheme.typography.labelMedium,
diff --git a/app/src/main/java/com/m57/hermescontrol/ui/sessions/SessionsViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/sessions/SessionsViewModel.kt
index 44f58213..54e1fb43 100644
--- a/app/src/main/java/com/m57/hermescontrol/ui/sessions/SessionsViewModel.kt
+++ b/app/src/main/java/com/m57/hermescontrol/ui/sessions/SessionsViewModel.kt
@@ -21,12 +21,37 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
+import java.util.Locale
data class SessionStats(
val total: Int = 0,
- val active: Int = 0,
+ val messages: Int = 0,
)
+/**
+ * Compact count for stat cards: max 3 digits + unit letter.
+ * 987 → "987", 100987 → "100k", 1234567 → "1.23m", 100000000 → "100m".
+ */
+internal fun formatCompactCount(value: Int): String {
+ if (value < 1_000) return value.toString()
+ val (divisor, suffix) =
+ when {
+ value < 1_000_000 -> 1_000 to "k"
+ value < 1_000_000_000 -> 1_000_000 to "m"
+ else -> 1_000_000_000 to "b"
+ }
+ val scaled = value.toDouble() / divisor
+ val digits =
+ when {
+ scaled >= 100 -> scaled.toInt().toString()
+ scaled >= 10 -> String.format(Locale.US, "%.1f", scaled).trimZeroes()
+ else -> String.format(Locale.US, "%.2f", scaled).trimZeroes()
+ }
+ return "$digits$suffix"
+}
+
+private fun String.trimZeroes(): String = dropLastWhile { it == '0' }.trimEnd('.')
+
data class SessionsUiState(
val isLoading: Boolean = false,
val isLoadingMore: Boolean = false,
@@ -244,7 +269,7 @@ class SessionsViewModel :
it.copy(
isLoadingStats = false,
stats =
- SessionStats(total = data.total, active = data.active),
+ SessionStats(total = data.total, messages = data.messages),
)
}
},
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index 11902b45..0b7ba3a3 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -329,7 +329,7 @@
세션 검색…
전체
- 활성
+ 메시지
정리
오래된 세션 정리
지정된 일수보다 오래된 세션을 삭제합니다.
@@ -342,8 +342,6 @@
전체 선택
선택 해제
%1$d개 삭제
- 세션 선택
- 선택 종료
툴셋이 없습니다
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 0e38b0f5..9ec6ab22 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -506,13 +506,13 @@
Match
Model
Total
- Active
+ Messages
Prune
Prune Old Sessions
Delete sessions older than the specified number of days.
Days threshold
Prune
- Delete empty sessions
+ Empty
Delete empty sessions?
Delete %1$d empty session(s). Active and archived sessions are kept.
Delete
@@ -527,8 +527,6 @@
Rename
Rename Session
Session title
- Select sessions
- Exit selection
No toolsets reported
diff --git a/app/src/test/java/com/m57/hermescontrol/ui/sessions/SessionsStatsFormatTest.kt b/app/src/test/java/com/m57/hermescontrol/ui/sessions/SessionsStatsFormatTest.kt
new file mode 100644
index 00000000..6c06f560
--- /dev/null
+++ b/app/src/test/java/com/m57/hermescontrol/ui/sessions/SessionsStatsFormatTest.kt
@@ -0,0 +1,44 @@
+package com.m57.hermescontrol.ui.sessions
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class SessionsStatsFormatTest {
+ @Test
+ fun `under 1000 shows raw number`() {
+ assertEquals("0", formatCompactCount(0))
+ assertEquals("7", formatCompactCount(7))
+ assertEquals("987", formatCompactCount(987))
+ assertEquals("999", formatCompactCount(999))
+ }
+
+ @Test
+ fun `thousands compact to 3 digits plus k`() {
+ assertEquals("1k", formatCompactCount(1_000))
+ assertEquals("10k", formatCompactCount(10_000))
+ assertEquals("100k", formatCompactCount(100_987))
+ assertEquals("999k", formatCompactCount(999_999))
+ }
+
+ @Test
+ fun `fractional thousands keep one or two digits`() {
+ assertEquals("1.5k", formatCompactCount(1_500))
+ assertEquals("1.05k", formatCompactCount(1_050))
+ assertEquals("12.5k", formatCompactCount(12_500))
+ }
+
+ @Test
+ fun `millions compact to 3 digits plus m`() {
+ assertEquals("1m", formatCompactCount(1_000_000))
+ assertEquals("1.23m", formatCompactCount(1_234_567))
+ assertEquals("100m", formatCompactCount(100_000_000))
+ assertEquals("999m", formatCompactCount(999_999_999))
+ }
+
+ @Test
+ fun `billions compact to b`() {
+ assertEquals("1b", formatCompactCount(1_000_000_000))
+ assertEquals("2b", formatCompactCount(2_000_000_000))
+ assertEquals("2.15b", formatCompactCount(Int.MAX_VALUE))
+ }
+}