From 7456d67243f095982a65f7d2340969249b00891b Mon Sep 17 00:00:00 2001 From: Armando Fernandez Date: Thu, 24 Sep 2026 15:12:08 -0700 Subject: [PATCH 1/6] Add a context meter to each tab's status strip The strip under the input box now shows how full the agent's context window is, left of the git chip: a bar against the configured context length for local models, and the token count alone for cloud models, which manage their window server-side. "Used" is the last request's prompt tokens. A full window adds a warning glyph, so the state never rests on color alone, and the tooltip points at /compact. The fill takes the level's theme color (green, gold, red), lifted per theme until it clears WCAG's 3:1 non-text floor against both the page and the empty track. Measured across all 25 themes, the raw colors missed 3:1 against the page in W98 - Y2K and Solarized Light, and a dim-colored track sat within 1.0-1.5:1 of the fill in most themes. A test sweeps every theme and level. The git chip now hides its own panel, and the strip shows when either part has something to show. Depends on the engine's ContextMeter (DevMando/MandoCode feature/cli-live-ui); the submodule points at that branch until it merges. Co-Authored-By: Claude Opus 5.5 (1M context) --- MandoCode | 2 +- .../ContextMeterColorsTests.cs | 43 +++++++++++++ .../MandoCode.Desktop.Tests.csproj | 1 + .../Controls/ChatTabView.ContextMeter.cs | 64 +++++++++++++++++++ .../Controls/ChatTabView.Explorer.cs | 8 ++- .../Controls/ChatTabView.Header.cs | 1 + .../Controls/ChatTabView.xaml | 17 ++++- .../Controls/ChatTabView.xaml.cs | 1 + .../Services/ContextMeterColors.cs | 32 ++++++++++ 9 files changed, 163 insertions(+), 6 deletions(-) create mode 100644 src/MandoCode.Desktop.Tests/ContextMeterColorsTests.cs create mode 100644 src/MandoCode.Desktop/Controls/ChatTabView.ContextMeter.cs create mode 100644 src/MandoCode.Desktop/Services/ContextMeterColors.cs diff --git a/MandoCode b/MandoCode index fd623aa..cb45c24 160000 --- a/MandoCode +++ b/MandoCode @@ -1 +1 @@ -Subproject commit fd623aa68189ef49737e6c192b841e64a93bfcb2 +Subproject commit cb45c2438767cdddadd1916294d63012e6fdfd36 diff --git a/src/MandoCode.Desktop.Tests/ContextMeterColorsTests.cs b/src/MandoCode.Desktop.Tests/ContextMeterColorsTests.cs new file mode 100644 index 0000000..48c7dc9 --- /dev/null +++ b/src/MandoCode.Desktop.Tests/ContextMeterColorsTests.cs @@ -0,0 +1,43 @@ +using MandoCode.Desktop.Services; +using MandoCode.Services; +using Xunit; + +namespace MandoCode.Desktop.Tests; + +/// +/// The context meter's bar is read by its length, so in every theme the filled part must clear the +/// non-text floor against both the page and the empty track. A theme added with a faint green, +/// gold, or red fails here instead of shipping 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 FillStandsApartFromThePageAndTheTrack(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.NonTextFloor, $"{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..11120d9 --- /dev/null +++ b/src/MandoCode.Desktop/Services/ContextMeterColors.cs @@ -0,0 +1,32 @@ +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 the WCAG non-text floor against +/// both the page and the empty track: the bar's length is the reading, so the filled part has to +/// stand apart from the unfilled part, not just from the page. Measured across the shipped themes, +/// the level colors alone missed 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; + + /// 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, NonTextFloor); + return ColorMath.Readable(onPage, Track(theme), theme.Text, NonTextFloor); + } +} From a6079f22b0709c692e13ce2722a4859ade2742b9 Mon Sep 17 00:00:00 2001 From: Armando Fernandez Date: Thu, 24 Sep 2026 15:16:52 -0700 Subject: [PATCH 2/6] Show the context meter's percentage in the bar's color The percentage now stands on its own in the fill color, semibold, instead of "(54%)" after the token counts. Because it is text in that color, the fill is lifted to the 4.5:1 text floor against the page (from the 3:1 non-text floor), and the bar and the percentage share the one color. The theme sweep asserts the new floor in every theme and level. The meter also gets an accessible name for screen readers. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../ContextMeterColorsTests.cs | 11 ++++---- .../Controls/ChatTabView.ContextMeter.cs | 26 +++++++++++++++---- .../Controls/ChatTabView.xaml | 4 +++ .../Services/ContextMeterColors.cs | 16 +++++++----- 4 files changed, 41 insertions(+), 16 deletions(-) diff --git a/src/MandoCode.Desktop.Tests/ContextMeterColorsTests.cs b/src/MandoCode.Desktop.Tests/ContextMeterColorsTests.cs index 48c7dc9..ade9dda 100644 --- a/src/MandoCode.Desktop.Tests/ContextMeterColorsTests.cs +++ b/src/MandoCode.Desktop.Tests/ContextMeterColorsTests.cs @@ -5,9 +5,10 @@ namespace MandoCode.Desktop.Tests; /// -/// The context meter's bar is read by its length, so in every theme the filled part must clear the -/// non-text floor against both the page and the empty track. A theme added with a faint green, -/// gold, or red fails here instead of shipping a bar that looks full, or empty, when it isn't. +/// 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 { @@ -22,14 +23,14 @@ public class ContextMeterColorsTests [Theory] [MemberData(nameof(ThemesAndLevels))] - public void FillStandsApartFromThePageAndTheTrack(string name, ContextMeter.Level level) + 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.NonTextFloor, $"{name}/{level}: fill is {onPage:F2}:1 against the page"); + 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"); } diff --git a/src/MandoCode.Desktop/Controls/ChatTabView.ContextMeter.cs b/src/MandoCode.Desktop/Controls/ChatTabView.ContextMeter.cs index 9b5d167..45b436a 100644 --- a/src/MandoCode.Desktop/Controls/ChatTabView.ContextMeter.cs +++ b/src/MandoCode.Desktop/Controls/ChatTabView.ContextMeter.cs @@ -1,6 +1,7 @@ using MandoCode.Desktop.Services; using MandoCode.Services; using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Automation; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media; @@ -34,17 +35,32 @@ private void UpdateContextMeter() var theme = ThemeManager.Current; var hasBar = window > 0; + var used = TokenTrackingService.FormatTokenCount(lastOp.PromptTokens); ContextMeterTrack.Visibility = hasBar ? Visibility.Visible : Visibility.Collapsed; + ContextMeterPercent.Visibility = hasBar ? Visibility.Visible : Visibility.Collapsed; if (hasBar) { - ContextMeterTrack.Background = new SolidColorBrush(ThemeManager.C(ContextMeterColors.Track(theme))); - ContextMeterFill.Background = new SolidColorBrush(ThemeManager.C(ContextMeterColors.Fill(theme, reading.Level))); + var fill = new SolidColorBrush(ThemeManager.C(ContextMeterColors.Fill(theme, reading.Level))); var fraction = Math.Clamp((double)lastOp.PromptTokens / window, 0, 1); + var percent = (int)Math.Round(fraction * 100); + var size = TokenTrackingService.FormatTokenCount(window); + + ContextMeterTrack.Background = new SolidColorBrush(ThemeManager.C(ContextMeterColors.Track(theme))); + ContextMeterFill.Background = fill; ContextMeterFill.Width = ContextMeterTrack.Width * fraction; - } - // A full window also says so in words, so the warning never rests on color alone. - ContextMeterText.Text = reading.Level == ContextMeter.Level.Full ? $"⚠ {reading.Label}" : reading.Label; + // A full window also says so with a glyph, so the warning never rests on color alone. + ContextMeterPercent.Foreground = fill; + ContextMeterPercent.Text = reading.Level == ContextMeter.Level.Full ? $"⚠ {percent}%" : $"{percent}%"; + ContextMeterText.Text = $"{used} / {size}"; + AutomationProperties.SetName(ContextMeterPanel, + $"Context window {percent} percent full, {used} of {size} tokens"); + } + else + { + ContextMeterText.Text = reading.Label; + AutomationProperties.SetName(ContextMeterPanel, $"Context: {reading.Label}"); + } ToolTipService.SetToolTip(ContextMeterPanel, hasBar ? $"Context window: the last request used {reading.Label}. " + "Every request resends the conversation, so this is where the next one starts. " diff --git a/src/MandoCode.Desktop/Controls/ChatTabView.xaml b/src/MandoCode.Desktop/Controls/ChatTabView.xaml index bc985a4..79946b6 100644 --- a/src/MandoCode.Desktop/Controls/ChatTabView.xaml +++ b/src/MandoCode.Desktop/Controls/ChatTabView.xaml @@ -774,6 +774,10 @@ CornerRadius="3"> + + diff --git a/src/MandoCode.Desktop/Services/ContextMeterColors.cs b/src/MandoCode.Desktop/Services/ContextMeterColors.cs index 11120d9..04fac66 100644 --- a/src/MandoCode.Desktop/Services/ContextMeterColors.cs +++ b/src/MandoCode.Desktop/Services/ContextMeterColors.cs @@ -4,17 +4,21 @@ 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 the WCAG non-text floor against -/// both the page and the empty track: the bar's length is the reading, so the filled part has to -/// stand apart from the unfilled part, not just from the page. Measured across the shipped themes, -/// the level colors alone missed 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. +/// (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; @@ -26,7 +30,7 @@ public static string Fill(UiTheme theme, ContextMeter.Level level) ContextMeter.Level.Warm => theme.Gold, _ => theme.Green, }; - var onPage = ColorMath.Readable(color, theme.Background, theme.Text, NonTextFloor); + var onPage = ColorMath.Readable(color, theme.Background, theme.Text, TextFloor); return ColorMath.Readable(onPage, Track(theme), theme.Text, NonTextFloor); } } From 74eb9fdc6fccd7da94d2ebeb3703e273c496e7d7 Mon Sep 17 00:00:00 2001 From: Armando Fernandez Date: Thu, 24 Sep 2026 15:21:23 -0700 Subject: [PATCH 3/6] Label the context meter and say what its numbers count "Context" now names the bar, which alone could read as any progress, and the counts read "13k of 32k tokens" instead of "13k / 32k". Co-Authored-By: Claude Opus 5.5 (1M context) --- src/MandoCode.Desktop/Controls/ChatTabView.ContextMeter.cs | 2 +- src/MandoCode.Desktop/Controls/ChatTabView.xaml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/MandoCode.Desktop/Controls/ChatTabView.ContextMeter.cs b/src/MandoCode.Desktop/Controls/ChatTabView.ContextMeter.cs index 45b436a..3dc825a 100644 --- a/src/MandoCode.Desktop/Controls/ChatTabView.ContextMeter.cs +++ b/src/MandoCode.Desktop/Controls/ChatTabView.ContextMeter.cs @@ -52,7 +52,7 @@ private void UpdateContextMeter() // A full window also says so with a glyph, so the warning never rests on color alone. ContextMeterPercent.Foreground = fill; ContextMeterPercent.Text = reading.Level == ContextMeter.Level.Full ? $"⚠ {percent}%" : $"{percent}%"; - ContextMeterText.Text = $"{used} / {size}"; + ContextMeterText.Text = $"{used} of {size} tokens"; AutomationProperties.SetName(ContextMeterPanel, $"Context window {percent} percent full, {used} of {size} tokens"); } diff --git a/src/MandoCode.Desktop/Controls/ChatTabView.xaml b/src/MandoCode.Desktop/Controls/ChatTabView.xaml index 79946b6..e235ac1 100644 --- a/src/MandoCode.Desktop/Controls/ChatTabView.xaml +++ b/src/MandoCode.Desktop/Controls/ChatTabView.xaml @@ -770,6 +770,8 @@ the only signal. Shown once the agent has made a request. --> + + From a1dc2b9e381b69667d2efccc429b3548ce785697 Mon Sep 17 00:00:00 2001 From: Armando Fernandez Date: Thu, 24 Sep 2026 15:24:02 -0700 Subject: [PATCH 4/6] Read the context meter as "41% of 32k tokens" One phrase, led by the colored percentage. The used count is 41% of the window, and the exact figure stays in the tooltip and accessible name. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/MandoCode.Desktop/Controls/ChatTabView.ContextMeter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MandoCode.Desktop/Controls/ChatTabView.ContextMeter.cs b/src/MandoCode.Desktop/Controls/ChatTabView.ContextMeter.cs index 3dc825a..c7b5ff7 100644 --- a/src/MandoCode.Desktop/Controls/ChatTabView.ContextMeter.cs +++ b/src/MandoCode.Desktop/Controls/ChatTabView.ContextMeter.cs @@ -52,7 +52,7 @@ private void UpdateContextMeter() // A full window also says so with a glyph, so the warning never rests on color alone. ContextMeterPercent.Foreground = fill; ContextMeterPercent.Text = reading.Level == ContextMeter.Level.Full ? $"⚠ {percent}%" : $"{percent}%"; - ContextMeterText.Text = $"{used} of {size} tokens"; + ContextMeterText.Text = $"of {size} tokens"; // the used count is in the tooltip AutomationProperties.SetName(ContextMeterPanel, $"Context window {percent} percent full, {used} of {size} tokens"); } From 200b646745ce51fa0e05c6194a519f0c1628a01f Mon Sep 17 00:00:00 2001 From: Armando Fernandez Date: Thu, 24 Sep 2026 15:29:18 -0700 Subject: [PATCH 5/6] Chore: point the engine at the rebased feature/cli-live-ui The engine branch was rebased onto its changelog entry, so the commit recorded earlier is no longer on any branch. Co-Authored-By: Claude Opus 5.5 (1M context) --- MandoCode | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MandoCode b/MandoCode index cb45c24..1cbcc07 160000 --- a/MandoCode +++ b/MandoCode @@ -1 +1 @@ -Subproject commit cb45c2438767cdddadd1916294d63012e6fdfd36 +Subproject commit 1cbcc0777356762bfb52f5ed1be11bc5b2ea47f2 From d837a6fdd80d64e3dfe92c398c4a621e92299434 Mon Sep 17 00:00:00 2001 From: Armando Fernandez Date: Thu, 24 Sep 2026 15:29:30 -0700 Subject: [PATCH 6/6] Docs: record the context meter in the changelog Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) 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