Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
25 changes: 25 additions & 0 deletions src/MandoCode.Desktop.Tests/ModelNoticeReplayTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand All @@ -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.")));
}
}
23 changes: 21 additions & 2 deletions src/MandoCode.Desktop/Services/ModelNoticeReplay.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,38 @@
using System.Net;
using System.Text.RegularExpressions;

namespace MandoCode.Desktop.Services;

/// <summary>Old model setup notices describe a prior runtime, not the restored conversation.</summary>
public static class ModelNoticeReplay
{
/// <summary>
/// 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.
/// </summary>
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,
// and errors even when they quote the same wording.
var match = Regex.Match(html,
"\\A<div class=\"notice-card dim\"><span class=\"notice-emoji\">[^<]*</span><span class=\"notice-text\">([^<]*)</span></div>\\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 &#39; 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");
}
}
13 changes: 0 additions & 13 deletions src/MandoCode.Desktop/ViewModels/ChatController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,6 @@ private void SetPlanProgress(int step, int total, bool running)
/// </summary>
public bool DeferModelAnnouncement { get; set; }

private bool _cloudNoticeShown;

/// <summary>The single status line for the live model: state, image capability, where it runs.</summary>
public void AnnounceModelStatus()
Expand Down Expand Up @@ -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)
Expand Down
Loading