From 6fe62f9646b308fce40bf922276f70c10d1fb5be Mon Sep 17 00:00:00 2001 From: M57 <41645758+Hy4ri@users.noreply.github.com> Date: Mon, 21 Sep 2026 10:59:47 +0300 Subject: [PATCH 1/3] fix(chat): make context meter strictly backend-reported Remove misleading local/cumulative token fallback in ContextDetailSheet and hide 0% badge when context used is unknown. --- .../ui/chat/components/ContextDetailSheet.kt | 13 ++++++++---- .../ui/chat/components/ContextUsageChip.kt | 20 ++++++++++--------- app/src/main/res/values-ar/strings.xml | 1 + app/src/main/res/values-zh/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 5 files changed, 23 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/components/ContextDetailSheet.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/components/ContextDetailSheet.kt index 50403b63..ca9d6140 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/components/ContextDetailSheet.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/components/ContextDetailSheet.kt @@ -51,12 +51,12 @@ fun ContextDetailSheet( sheetState = sheetState, ) { val full = fullTokens ?: 0L - val used = usedTokens ?: breakdown.inputTokens - val fraction = if (full > 0L) min(1f, used.toFloat() / full.toFloat()) else 0f + val fraction = if (full > 0L && usedTokens != null) min(1f, usedTokens.toFloat() / full.toFloat()) else 0f val pct = (fraction * 100).toInt() val statusColors = LocalHermesStatusColors.current val barColor = when { + usedTokens == null -> MaterialTheme.colorScheme.primary pct >= 90 -> statusColors.error pct >= 70 -> statusColors.warning else -> MaterialTheme.colorScheme.primary @@ -83,7 +83,7 @@ fun ContextDetailSheet( verticalAlignment = Alignment.Bottom, ) { Text( - text = formatTokens(used), + text = if (usedTokens != null) formatTokens(usedTokens) else "—", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold, color = barColor, @@ -115,7 +115,12 @@ fun ContextDetailSheet( ) } Text( - text = "$pct% of context window used", + text = + if (usedTokens != null) { + "$pct% of context window used" + } else { + stringResource(R.string.context_window_usage_unavailable) + }, style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/components/ContextUsageChip.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/components/ContextUsageChip.kt index b9360a12..29039903 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/components/ContextUsageChip.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/components/ContextUsageChip.kt @@ -56,12 +56,12 @@ fun ContextUsageChip( val full = fullTokens if (full == null || full <= 0L) return - val used = usedTokens ?: 0L - val fraction = min(1f, used.toFloat() / full.toFloat()) + val fraction = if (usedTokens != null) min(1f, usedTokens.toFloat() / full.toFloat()) else 0f val pct = (fraction * 100).toInt() val statusColors = LocalHermesStatusColors.current val barColor = when { + usedTokens == null -> MaterialTheme.colorScheme.primary pct >= 90 -> statusColors.error pct >= 70 -> statusColors.warning else -> MaterialTheme.colorScheme.primary @@ -90,7 +90,7 @@ fun ContextUsageChip( Text( text = if (usedTokens != null) { - "${formatTokens(used)} / ${formatTokens(full)} context" + "${formatTokens(usedTokens)} / ${formatTokens(full)} context" } else { "— / ${formatTokens(full)} context" }, @@ -111,12 +111,14 @@ fun ContextUsageChip( maxLines = 1, ) } - Text( - text = "$pct%", - style = MaterialTheme.typography.labelSmall, - color = barColor, - maxLines = 1, - ) + if (usedTokens != null) { + Text( + text = "$pct%", + style = MaterialTheme.typography.labelSmall, + color = barColor, + maxLines = 1, + ) + } } } Box( diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 6c524f74..c0e458e6 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -1310,6 +1310,7 @@ المطالبة (المدخلات) تعكس رموز المطالبة إجمالي السياق المستخدم في هذه الجلسة حتى الآن. نافذة السياق +استخدام نافذة السياق غير متوفر تشغيل GIF تصفية المهام حسب العنوان أو الحالة أو المسؤول... سجلات Hermes diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 7fcf777c..8fd352db 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -1277,6 +1277,7 @@ 提示词(输入) 提示词 token 反映此会话迄今使用的总上下文。 上下文窗口 + 上下文窗口使用量不可用 播放 GIF 按标题、状态或负责人筛选任务… Hermes 日志 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a7da130e..16e3cc74 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1437,6 +1437,7 @@ Prompt (input) Prompt tokens reflect total context used by this session so far. Context window + Context window usage unavailable Play GIF Filter tasks by title, status, or assignee... Hermes Logs From 02e14a4ab4ef317b24bfff0ca9bcbfba9590599d Mon Sep 17 00:00:00 2001 From: M57 <41645758+Hy4ri@users.noreply.github.com> Date: Mon, 21 Sep 2026 11:27:31 +0300 Subject: [PATCH 2/3] test(chat): add ContextUsageComponentsTest regression tests and clarify context tokens note --- .../components/ContextUsageComponentsTest.kt | 108 ++++++++++++++++++ app/src/main/res/values-ar/strings.xml | 2 +- app/src/main/res/values-zh/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 4 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 app/src/androidTest/java/com/m57/hermescontrol/ui/chat/components/ContextUsageComponentsTest.kt diff --git a/app/src/androidTest/java/com/m57/hermescontrol/ui/chat/components/ContextUsageComponentsTest.kt b/app/src/androidTest/java/com/m57/hermescontrol/ui/chat/components/ContextUsageComponentsTest.kt new file mode 100644 index 00000000..e1ec3178 --- /dev/null +++ b/app/src/androidTest/java/com/m57/hermescontrol/ui/chat/components/ContextUsageComponentsTest.kt @@ -0,0 +1,108 @@ +package com.m57.hermescontrol.ui.chat.components + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.MediumTest +import com.m57.hermescontrol.theme.HermesControlTheme +import com.m57.hermescontrol.ui.chat.ContextBreakdown +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Regression coverage for backend-reported context occupancy. + * + * Cumulative REST input-token accounting must never masquerade as the live + * context-window numerator when the backend has not reported context_used. + */ +@RunWith(AndroidJUnit4::class) +@MediumTest +class ContextUsageComponentsTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun chip_missingBackendOccupancy_doesNotShowFakeZeroPercent() { + composeTestRule.setContent { + HermesControlTheme { + ContextUsageChip( + usedTokens = null, + fullTokens = 128_000L, + ) + } + } + + composeTestRule + .onNodeWithText("— / 128k context") + .assertIsDisplayed() + + composeTestRule + .onNodeWithText("0%") + .assertDoesNotExist() + } + + @Test + fun chip_backendOccupancyAvailable_showsDeterminateUsage() { + composeTestRule.setContent { + HermesControlTheme { + ContextUsageChip( + usedTokens = 64_000L, + fullTokens = 128_000L, + ) + } + } + + composeTestRule + .onNodeWithText("64k / 128k context") + .assertIsDisplayed() + + composeTestRule + .onNodeWithText("50%") + .assertIsDisplayed() + } + + @Test + fun detailSheet_missingBackendOccupancy_doesNotUseCumulativeInputTokensAsNumerator() { + composeTestRule.setContent { + HermesControlTheme { + ContextDetailSheet( + breakdown = + ContextBreakdown( + inputTokens = 12_300L, + outputTokens = 2_000L, + cacheReadTokens = 500L, + cacheWriteTokens = 250L, + reasoningTokens = 100L, + messageCount = 4, + ), + usedTokens = null, + fullTokens = 128_000L, + onDismiss = {}, + ) + } + } + + // The live numerator is unavailable, even though cumulative REST + // input-token accounting exists below in the detail breakdown. + composeTestRule + .onNodeWithText("—") + .assertIsDisplayed() + + composeTestRule + .onNodeWithText("Context window usage unavailable") + .assertIsDisplayed() + + // Cumulative accounting is still preserved as informational detail. + composeTestRule + .onNodeWithText("12.3k") + .assertIsDisplayed() + + // Most important regression guard: null occupancy is not rendered as + // a determinate zero-percent context state. + composeTestRule + .onNodeWithText("0% of context window used") + .assertDoesNotExist() + } +} diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index c0e458e6..83265e23 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -1308,7 +1308,7 @@ الإكمال (المخرجات) مضغوط ×%1$s المطالبة (المدخلات) -تعكس رموز المطالبة إجمالي السياق المستخدم في هذه الجلسة حتى الآن. +أعداد الرموز أدناه تراكمية للجلسة ولا تمثل الاستخدام الحالي لنافذة السياق. نافذة السياق استخدام نافذة السياق غير متوفر تشغيل GIF diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 8fd352db..0c36aea0 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -1275,7 +1275,7 @@ 补全(输出) 已压缩 ×%1$s 提示词(输入) - 提示词 token 反映此会话迄今使用的总上下文。 + 下方 token 数为会话累计用量,不代表当前上下文窗口占用。 上下文窗口 上下文窗口使用量不可用 播放 GIF diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 16e3cc74..c5f01c34 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1435,7 +1435,7 @@ Completion (output) compressed ×%1$s Prompt (input) - Prompt tokens reflect total context used by this session so far. + Token counts below are cumulative session accounting and do not represent current context-window occupancy. Context window Context window usage unavailable Play GIF From 0e1ab9b20474fb48005f3427ba705884d9717dba Mon Sep 17 00:00:00 2001 From: M57 <41645758+Hy4ri@users.noreply.github.com> Date: Mon, 21 Sep 2026 11:43:13 +0300 Subject: [PATCH 3/3] fix(test): opt-in to ExperimentalMaterial3Api in ContextUsageComponentsTest --- .../ui/chat/components/ContextUsageComponentsTest.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/androidTest/java/com/m57/hermescontrol/ui/chat/components/ContextUsageComponentsTest.kt b/app/src/androidTest/java/com/m57/hermescontrol/ui/chat/components/ContextUsageComponentsTest.kt index e1ec3178..2aecdc9d 100644 --- a/app/src/androidTest/java/com/m57/hermescontrol/ui/chat/components/ContextUsageComponentsTest.kt +++ b/app/src/androidTest/java/com/m57/hermescontrol/ui/chat/components/ContextUsageComponentsTest.kt @@ -1,5 +1,6 @@ package com.m57.hermescontrol.ui.chat.components +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithText @@ -63,6 +64,7 @@ class ContextUsageComponentsTest { .assertIsDisplayed() } + @OptIn(ExperimentalMaterial3Api::class) @Test fun detailSheet_missingBackendOccupancy_doesNotUseCumulativeInputTokensAsNumerator() { composeTestRule.setContent {