diff --git a/CHANGELOG.md b/CHANGELOG.md
index 949692c..3c1bd5f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,12 @@ submodule.
tool ("let me check the docs…"), that text becomes its own card above the tool call, and the
answer gets a second card. The conversation reads in the order it happened, instead of merging
everything into one card at the end.
+- **A context meter in every tab.** The strip under the message box now shows how full the agent's
+ context window is, for example "Context ▰▰▰▱▱▱ 41% of 32k tokens", with a bar that turns
+ yellow and then red as it fills. It tells you when to `/compact` before the model starts losing the
+ earlier conversation. The colors are tuned to stay readable in every theme, a full window also
+ shows ⚠ so the warning never depends on color alone, and screen readers announce it. Cloud models,
+ which manage their own window, show their token count.
## [0.15.0] — 2026-09-10
diff --git a/src/MandoCode.Desktop.Tests/ContextMeterColorsTests.cs b/src/MandoCode.Desktop.Tests/ContextMeterColorsTests.cs
new file mode 100644
index 0000000..ade9dda
--- /dev/null
+++ b/src/MandoCode.Desktop.Tests/ContextMeterColorsTests.cs
@@ -0,0 +1,44 @@
+using MandoCode.Desktop.Services;
+using MandoCode.Services;
+using Xunit;
+
+namespace MandoCode.Desktop.Tests;
+
+///
+/// The context meter's bar is read by its length, and its percentage is printed in the fill color.
+/// So in every theme the fill must clear the text floor against the page and the non-text floor
+/// against the empty track. A theme added with a faint green, gold, or red fails here instead of
+/// shipping an unreadable percentage, or a bar that looks full, or empty, when it isn't.
+///
+public class ContextMeterColorsTests
+{
+ public static TheoryData ThemesAndLevels()
+ {
+ var data = new TheoryData();
+ foreach (var t in UiTheme.All)
+ foreach (var level in Enum.GetValues())
+ data.Add(t.Name, level);
+ return data;
+ }
+
+ [Theory]
+ [MemberData(nameof(ThemesAndLevels))]
+ public void FillReadsAsTextOnThePage_AndStandsApartFromTheTrack(string name, ContextMeter.Level level)
+ {
+ var theme = UiTheme.All.Single(t => t.Name == name);
+ var fill = ContextMeterColors.Fill(theme, level);
+
+ var onPage = ColorMath.Contrast(fill, theme.Background);
+ var onTrack = ColorMath.Contrast(fill, ContextMeterColors.Track(theme));
+ Assert.True(onPage >= ContextMeterColors.TextFloor, $"{name}/{level}: fill is {onPage:F2}:1 against the page");
+ Assert.True(onTrack >= ContextMeterColors.NonTextFloor, $"{name}/{level}: fill is {onTrack:F2}:1 against the track");
+ }
+
+ [Fact]
+ public void AColorThatAlreadyClearsTheFloorIsLeftAlone()
+ {
+ // Lifting is a last resort: where the palette's own green already works, it's what shows.
+ var theme = UiTheme.All.Single(t => t.Name == "Dracula");
+ Assert.Equal(theme.Green, ContextMeterColors.Fill(theme, ContextMeter.Level.Ok));
+ }
+}
diff --git a/src/MandoCode.Desktop.Tests/MandoCode.Desktop.Tests.csproj b/src/MandoCode.Desktop.Tests/MandoCode.Desktop.Tests.csproj
index b8bbccc..5dff56c 100644
--- a/src/MandoCode.Desktop.Tests/MandoCode.Desktop.Tests.csproj
+++ b/src/MandoCode.Desktop.Tests/MandoCode.Desktop.Tests.csproj
@@ -69,6 +69,7 @@
+
-
+
+
+
+
+
+
+
+
+
+
+