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..2aecdc9d
--- /dev/null
+++ b/app/src/androidTest/java/com/m57/hermescontrol/ui/chat/components/ContextUsageComponentsTest.kt
@@ -0,0 +1,110 @@
+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
+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()
+ }
+
+ @OptIn(ExperimentalMaterial3Api::class)
+ @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/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..83265e23 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -1308,8 +1308,9 @@
الإكمال (المخرجات)
مضغوط ×%1$s
المطالبة (المدخلات)
-تعكس رموز المطالبة إجمالي السياق المستخدم في هذه الجلسة حتى الآن.
+أعداد الرموز أدناه تراكمية للجلسة ولا تمثل الاستخدام الحالي لنافذة السياق.
نافذة السياق
+استخدام نافذة السياق غير متوفر
تشغيل GIF
تصفية المهام حسب العنوان أو الحالة أو المسؤول...
سجلات Hermes
diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml
index 7fcf777c..0c36aea0 100644
--- a/app/src/main/res/values-zh/strings.xml
+++ b/app/src/main/res/values-zh/strings.xml
@@ -1275,8 +1275,9 @@
补全(输出)
已压缩 ×%1$s
提示词(输入)
- 提示词 token 反映此会话迄今使用的总上下文。
+ 下方 token 数为会话累计用量,不代表当前上下文窗口占用。
上下文窗口
+ 上下文窗口使用量不可用
播放 GIF
按标题、状态或负责人筛选任务…
Hermes 日志
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index a7da130e..c5f01c34 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1435,8 +1435,9 @@
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
Filter tasks by title, status, or assignee...
Hermes Logs