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 {