From fae27dd85f1297784529802dfeadc272b830c7b8 Mon Sep 17 00:00:00 2001 From: DevMando Date: Thu, 10 Sep 2026 21:43:22 -0700 Subject: [PATCH] Fix: stop repeating the cloud subscription notice on model switches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Selecting a cloud model announced that cloud models need an ollama.com subscription. It was guarded to fire once, but the guard was an instance field on ChatController and there is one controller per agent tab — so the fact being stated was app-wide while the memory of having stated it was per-tab, and every agent the user switched announced it again. A second defect ran the other way: the flag was set before the DeferModelAnnouncement check, so a tab restored onto a cloud model marked the notice as shown while displaying nothing, then suppressed it for the rest of that session. Removed rather than repaired. The notice was pre-emptive — fired on selection in case a 403 arrived later — but the model chip already carries "cloud", and ResponseStreamer already reports the actionable version when a request actually fails. It told the user something they could not act on, before anything had gone wrong. ModelNoticeReplay keeps recognising it, and now covers the 0.14.x wording too. Nothing emits it any more, so replay is the only thing keeping it off screen for anyone whose journal already holds one. That older wording contains an apostrophe and is journaled HTML-encoded, so the filter now decodes before matching — comparing the encoded text against a raw literal fails silently, which is exactly how it would have come back on restore. The setup wizard's cloud messaging is untouched: it explains cloud versus local while the user is choosing, and only mentions the subscription when a sign-in check has actually failed. Both are conditional and actionable. --- CHANGELOG.md | 9 +++++++ .../ModelNoticeReplayTests.cs | 25 +++++++++++++++++++ .../Services/ModelNoticeReplay.cs | 23 +++++++++++++++-- .../ViewModels/ChatController.cs | 13 ---------- 4 files changed, 55 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 278995c..9037da6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -246,6 +246,15 @@ for every approved plan. Desktop's version follows the engine generation, so it a sticky header, or the suggestion list a field opens when it is filled. ### Fixed +- **Switching to a cloud model no longer repeats the subscription notice.** Picking a cloud model + announced that cloud models need an ollama.com subscription. It was meant to say so once, but the + "already said it" memory belonged to a single agent rather than the app, so every agent you + switched said it again — and a tab restored onto a cloud model marked the notice as shown without + displaying it, suppressing it for the rest of that session. The notice is gone entirely rather + than repaired: the model chip already marks a model as `cloud`, and if a cloud request actually + fails you now get the only message that was ever actionable — that the account is signed in but + has no active subscription. The setup wizard still explains cloud versus local while you are + choosing, and still offers to sign you in when you are not. - **Deleting a snapshot, past conversation, or note no longer resets the list.** These panels rebuilt themselves after every deletion, so the surviving cards slid back up and expanded project groups collapsed — losing your place in the middle of tidying up. The lists now update in place: diff --git a/src/MandoCode.Desktop.Tests/ModelNoticeReplayTests.cs b/src/MandoCode.Desktop.Tests/ModelNoticeReplayTests.cs index c05e92e..b97bafe 100644 --- a/src/MandoCode.Desktop.Tests/ModelNoticeReplayTests.cs +++ b/src/MandoCode.Desktop.Tests/ModelNoticeReplayTests.cs @@ -20,6 +20,23 @@ public void RepeatedRestoresDropAllHistoricalSetupNoticesButKeepConversation() Assert.Equal(replay, replay.Where(h => !ModelNoticeReplay.IsTransient(h))); } + [Fact] + public void TheRetiredCloudNoticeIsStrippedInBothOfItsWordings() + { + // The notice is no longer produced at all — a model switch says nothing about cloud + // subscriptions now. But it sits in the journals of anyone who switched to a cloud model + // before it was removed, and replay is the ONLY thing keeping it off screen, so both + // shipped wordings have to stay recognised. + Assert.True(ModelNoticeReplay.IsTransient( + Notice("Cloud models run on ollama.com and need an active cloud subscription."))); + + // The 0.14.x wording. Journaled HTML-ENCODED, because it contains an apostrophe — matching + // it against a raw literal silently fails, which is how it would come back on restore. + Assert.True(ModelNoticeReplay.IsTransient( + Notice("Cloud model — runs on ollama.com's servers and needs an account with an " + + "active cloud subscription. Without one, requests return 403 Forbidden."))); + } + [Fact] public void QuotedWordingAndActualWarningsArePreserved() { @@ -28,5 +45,13 @@ public void QuotedWordingAndActualWarningsArePreserved() Assert.False(ModelNoticeReplay.IsTransient(Notice(Sizing, "warn"))); Assert.False(ModelNoticeReplay.IsTransient(Notice("The context window is too small for this request."))); Assert.False(ModelNoticeReplay.IsTransient(Notice("Project root changed to: C:\\project"))); + + // The setup wizard's cloud-vs-local explainer is also a dim notice and mentions the same + // subscription. It is part of a walkthrough the user went through, so it is history and + // must survive — the match is on the exact retired notices, not on the word "cloud". + Assert.False(ModelNoticeReplay.IsTransient(Notice( + "Cloud models run on ollama.com's servers: more capable, no GPU needed, but they " + + "require an ollama.com account with an active cloud subscription. Local models run " + + "privately on your own hardware, free — bigger is smarter but needs more memory."))); } } diff --git a/src/MandoCode.Desktop/Services/ModelNoticeReplay.cs b/src/MandoCode.Desktop/Services/ModelNoticeReplay.cs index c55b7eb..52ee00e 100644 --- a/src/MandoCode.Desktop/Services/ModelNoticeReplay.cs +++ b/src/MandoCode.Desktop/Services/ModelNoticeReplay.cs @@ -1,3 +1,4 @@ +using System.Net; using System.Text.RegularExpressions; namespace MandoCode.Desktop.Services; @@ -5,6 +6,20 @@ namespace MandoCode.Desktop.Services; /// Old model setup notices describe a prior runtime, not the restored conversation. public static class ModelNoticeReplay { + /// + /// The cloud-subscription notice a model switch used to emit. No longer produced — the model + /// chip already marks a model as cloud, and ResponseStreamer says the actionable version if a + /// 403 actually arrives — but it sits in the journals of anyone who switched to a cloud model + /// before it was removed, so replay must still recognise it. Both wordings are listed: the + /// second shipped in 0.14.x and the first replaced it, and a journal can hold either. + /// + private static readonly string[] RetiredCloudNotices = + { + "Cloud models run on ollama.com and need an active cloud subscription.", + "Cloud model — runs on ollama.com's servers and needs an account with an active cloud " + + "subscription. Without one, requests return 403 Forbidden.", + }; + public static bool IsTransient(string html) { // Match only standalone system notices. Keep user messages, assistant explanations, @@ -12,8 +27,12 @@ public static bool IsTransient(string html) var match = Regex.Match(html, "\\A
[^<]*([^<]*)
\\z"); if (!match.Success) return false; - var text = match.Groups[1].Value; - return text == "Cloud models run on ollama.com and need an active cloud subscription." + + // Decoded, because the journal holds the ESCAPED text: the retired wording contains an + // apostrophe, which is written as ' and would never match a raw C# literal. + var text = WebUtility.HtmlDecode(match.Groups[1].Value); + + return RetiredCloudNotices.Contains(text) || Regex.IsMatch(text, @"\AContext window sized to \d+k tokens for this model tier \(applies from your next message\)\.\z"); } } diff --git a/src/MandoCode.Desktop/ViewModels/ChatController.cs b/src/MandoCode.Desktop/ViewModels/ChatController.cs index 42a938a..c0720ed 100644 --- a/src/MandoCode.Desktop/ViewModels/ChatController.cs +++ b/src/MandoCode.Desktop/ViewModels/ChatController.cs @@ -199,7 +199,6 @@ private void SetPlanProgress(int step, int total, bool running) /// public bool DeferModelAnnouncement { get; set; } - private bool _cloudNoticeShown; /// The single status line for the live model: state, image capability, where it runs. public void AnnounceModelStatus() @@ -1809,18 +1808,6 @@ private async Task ApplyModelSwitchAsync(string modelTag) _transcript.Append(_html.StatusChip(modelTag, ModelStatusDetail(modelTag), "ok")); - // Selection-time awareness, not just failure-time: cloud models require an active - // ollama.com cloud subscription — a signed-in account without one gets 403 Forbidden - // on its first message, which reads as the app breaking. Once per session is enough to - // establish that; the chip already marks every cloud model, and ResponseStreamer says - // the actionable version if a 403 actually arrives. - if (MandoCodeConfig.IsCloudModel(modelTag) && !_cloudNoticeShown) - { - _cloudNoticeShown = true; - if (!DeferModelAnnouncement) - _transcript.Append(_html.Dim("Cloud models run on ollama.com and need an active cloud subscription.")); - } - // Only mention the cleared context — and offer a snapshot — when there was actually a // conversation to clear. Switching an empty chat has nothing to salvage, so stay quiet. if (_pending != null)