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 @@ + - + + + + + + + + + + + diff --git a/src/MandoCode.Desktop/Controls/ChatTabView.xaml.cs b/src/MandoCode.Desktop/Controls/ChatTabView.xaml.cs index ca57f06..061a057 100644 --- a/src/MandoCode.Desktop/Controls/ChatTabView.xaml.cs +++ b/src/MandoCode.Desktop/Controls/ChatTabView.xaml.cs @@ -334,6 +334,7 @@ public async void ApplyTheme() if (_shutDown) return; var theme = ThemeManager.Current; TranscriptView.DefaultBackgroundColor = ThemeManager.C(theme.Background); + UpdateContextMeter(); // its fill is derived from the theme var core = CanScript ? TranscriptView.CoreWebView2 : null; // captured: see AppendRawAsync if (core == null) return; diff --git a/src/MandoCode.Desktop/Services/ContextMeterColors.cs b/src/MandoCode.Desktop/Services/ContextMeterColors.cs new file mode 100644 index 0000000..04fac66 --- /dev/null +++ b/src/MandoCode.Desktop/Services/ContextMeterColors.cs @@ -0,0 +1,36 @@ +using MandoCode.Services; + +namespace MandoCode.Desktop.Services; + +/// +/// Colors for the context meter in a tab's status strip. The fill takes the level's theme color +/// (green, gold, red) but is lifted toward body text until it clears two floors: the text floor +/// against the page, because the percentage is printed in this same color, and the non-text floor +/// against the empty track, because the bar's length is the reading, so the filled part has to +/// stand apart from the unfilled part. Measured across the shipped themes, the level colors alone +/// missed even 3:1 against the page in a few light and retro palettes, and a dim-colored track sat +/// within 1.0–1.5:1 of the fill in most of them. +/// +public static class ContextMeterColors +{ + /// WCAG 1.4.11: graphical objects need 3:1 against adjacent colors. + public const double NonTextFloor = 3.0; + + /// WCAG 1.4.3: the percentage is small text in the fill color. + public const double TextFloor = ColorMath.AaNormalText; + + /// The empty track: the theme's border color, a quiet outline of the capacity. + public static string Track(UiTheme theme) => theme.Border; + + public static string Fill(UiTheme theme, ContextMeter.Level level) + { + var color = level switch + { + ContextMeter.Level.Full => theme.Red, + ContextMeter.Level.Warm => theme.Gold, + _ => theme.Green, + }; + var onPage = ColorMath.Readable(color, theme.Background, theme.Text, TextFloor); + return ColorMath.Readable(onPage, Track(theme), theme.Text, NonTextFloor); + } +}