From 13b44ca9271fbffc746da0b297281ba496cea367 Mon Sep 17 00:00:00 2001 From: DevMando Date: Mon, 27 Jul 2026 10:05:34 -0700 Subject: [PATCH 1/2] Add agent callsigns and show the version in the window title A Settings > Behavior toggle (app-wide, persisted in panel-state.json) names new agents from a curated 500+ callsign pool - construct-crew, phreak, and cypher-energy handles, no real people or properties - dealt from a shuffled deck that never repeats until exhausted, skips names open tabs are wearing, and falls back to numbers if all are taken. Tests pin the pool contract: 500+ unique well-formed names, cycle without repeats, collision skip, numbered fallback. The window title now reads MandoCode Desktop v{version}, sourced from the assembly version the update checker already compares. --- .../AgentCallsignsTests.cs | 65 +++++++++ .../MandoCode.Desktop.Tests.csproj | 1 + src/MandoCode.Desktop/MainWindow.Settings.cs | 10 ++ src/MandoCode.Desktop/MainWindow.Snapshots.cs | 3 +- src/MandoCode.Desktop/MainWindow.xaml | 6 + src/MandoCode.Desktop/MainWindow.xaml.cs | 3 +- .../Services/AgentCallsigns.cs | 137 ++++++++++++++++++ src/MandoCode.Desktop/Services/PanelState.cs | 6 +- 8 files changed, 227 insertions(+), 4 deletions(-) create mode 100644 src/MandoCode.Desktop.Tests/AgentCallsignsTests.cs create mode 100644 src/MandoCode.Desktop/Services/AgentCallsigns.cs diff --git a/src/MandoCode.Desktop.Tests/AgentCallsignsTests.cs b/src/MandoCode.Desktop.Tests/AgentCallsignsTests.cs new file mode 100644 index 0000000..f068b09 --- /dev/null +++ b/src/MandoCode.Desktop.Tests/AgentCallsignsTests.cs @@ -0,0 +1,65 @@ +using MandoCode.Desktop.Services; +using Xunit; + +namespace MandoCode.Desktop.Tests; + +public class AgentCallsignsTests +{ + [Fact] + public void Pool_HasAtLeast500Names() + => Assert.True(AgentCallsigns.Pool.Count >= 500, + $"Pool has {AgentCallsigns.Pool.Count} names; the feature promises 500."); + + [Fact] + public void Pool_NamesAreUniqueCaseInsensitive() + { + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + var dupes = AgentCallsigns.Pool.Where(n => !seen.Add(n)).ToList(); + Assert.True(dupes.Count == 0, "Duplicate callsigns: " + string.Join(", ", dupes)); + } + + [Fact] + public void Pool_NamesAreWellFormed() + { + foreach (var name in AgentCallsigns.Pool) + { + Assert.False(string.IsNullOrWhiteSpace(name)); + Assert.Equal(name, name.Trim()); + // "Agent N" is the numbered scheme's namespace; a callsign colliding with it + // would make AgentNaming reuse-the-lowest-free-number logic misfire. + Assert.DoesNotContain("Agent ", name, StringComparison.OrdinalIgnoreCase); + } + } + + [Fact] + public void Next_DoesNotRepeatWithinOneFullCycle() + { + AgentCallsigns.ResetDeck(); + var dealt = new List(); + for (var i = 0; i < AgentCallsigns.Pool.Count; i++) + dealt.Add(AgentCallsigns.Next(Array.Empty())); + + Assert.Equal(AgentCallsigns.Pool.Count, dealt.Distinct(StringComparer.OrdinalIgnoreCase).Count()); + } + + [Fact] + public void Next_SkipsNamesWornByOpenTabs() + { + AgentCallsigns.ResetDeck(); + var taken = AgentCallsigns.Pool.Take(50).ToArray(); + for (var i = 0; i < 100; i++) + { + var name = AgentCallsigns.Next(taken); + Assert.DoesNotContain(name, taken, StringComparer.OrdinalIgnoreCase); + } + } + + [Fact] + public void Next_FallsBackToNumbersWhenEveryCallsignIsTaken() + { + AgentCallsigns.ResetDeck(); + var everything = AgentCallsigns.Pool.ToList(); + var name = AgentCallsigns.Next(everything); + Assert.Equal("Agent 1", name); + } +} diff --git a/src/MandoCode.Desktop.Tests/MandoCode.Desktop.Tests.csproj b/src/MandoCode.Desktop.Tests/MandoCode.Desktop.Tests.csproj index 5753791..609e37e 100644 --- a/src/MandoCode.Desktop.Tests/MandoCode.Desktop.Tests.csproj +++ b/src/MandoCode.Desktop.Tests/MandoCode.Desktop.Tests.csproj @@ -60,6 +60,7 @@ + diff --git a/src/MandoCode.Desktop/MainWindow.Settings.cs b/src/MandoCode.Desktop/MainWindow.Settings.cs index 9b62515..7ad8c17 100644 --- a/src/MandoCode.Desktop/MainWindow.Settings.cs +++ b/src/MandoCode.Desktop/MainWindow.Settings.cs @@ -46,6 +46,7 @@ private void LoadSettings() S_TemperatureLabel.Text = cfg.Temperature.ToString("0.##"); S_MaxTokens.Value = cfg.MaxTokens; S_Streaming.SelectedItem = cfg.ResponseStreaming; + S_AgentCallsigns.IsOn = AgentCallsigns.Enabled; // app-wide, not from the agent's config S_TaskPlanning.IsOn = cfg.EnableTaskPlanning; S_DiffApprovals.IsOn = cfg.EnableDiffApprovals; S_AutoContinue.IsOn = cfg.EnableAutoContinuation; @@ -69,6 +70,15 @@ private void LoadSettings() } } + /// App-wide naming style for new agents — applies immediately (like Appearance), + /// not through the agent config / Make Default flow the rest of the page uses. + private void AgentCallsigns_Toggled(object sender, RoutedEventArgs e) + { + if (_loadingSettings) return; + AgentCallsigns.Enabled = S_AgentCallsigns.IsOn; + SavePanelState(); + } + /// Runs the guided /setup wizard in the active agent's chat — the same flow that /// fires on first launch. Routed through SubmitAsync so it gets the standard command echo /// and the is-processing guard. diff --git a/src/MandoCode.Desktop/MainWindow.Snapshots.cs b/src/MandoCode.Desktop/MainWindow.Snapshots.cs index 45c62ab..0596e8e 100644 --- a/src/MandoCode.Desktop/MainWindow.Snapshots.cs +++ b/src/MandoCode.Desktop/MainWindow.Snapshots.cs @@ -141,7 +141,8 @@ private void MarkSnapshotsSeen() private void SavePanelState() => PanelState.Save(new PanelStateShape( _collapsedSnapshotGroups.ToList(), _collapsedHistoryGroups.ToList(), _snapshotsSeenAt, _historySeenAt, - _collapsedNoteGroups.ToList(), _lastNotePath, _noteModel)); + _collapsedNoteGroups.ToList(), _lastNotePath, _noteModel, + AgentCallsigns.Enabled)); // The group object is kept in sync (not just the set) so that when the ListView recycles a // container on scroll, the OneTime IsExpanded x:Bind re-reads the correct, current state. diff --git a/src/MandoCode.Desktop/MainWindow.xaml b/src/MandoCode.Desktop/MainWindow.xaml index 27e8b04..92d9ca1 100644 --- a/src/MandoCode.Desktop/MainWindow.xaml +++ b/src/MandoCode.Desktop/MainWindow.xaml @@ -1032,6 +1032,12 @@ HorizontalAlignment="Left" Padding="0,4,0,8"> + + diff --git a/src/MandoCode.Desktop/MainWindow.xaml.cs b/src/MandoCode.Desktop/MainWindow.xaml.cs index 0bfc0bc..339059e 100644 --- a/src/MandoCode.Desktop/MainWindow.xaml.cs +++ b/src/MandoCode.Desktop/MainWindow.xaml.cs @@ -60,7 +60,7 @@ private enum LeftPanel { None, Snapshots, History, Notes } public MainWindow() { InitializeComponent(); - Title = "MandoCode Desktop"; + Title = $"MandoCode Desktop v{UiUpdateCheckService.CurrentVersion}"; ThemeManager.Initialize(Root); // ONE window-level subscription to the static ThemeChanged event. Chat tabs must not @@ -102,6 +102,7 @@ public MainWindow() _historySeenAt = panelState.HistorySeenAt; _lastNotePath = panelState.LastNotePath; _noteModel = panelState.NoteModel; + AgentCallsigns.Enabled = panelState.AgentCallsigns ?? false; // The editor writes note content; the panel only lists. One store, handed over once. NoteEditor.Store = _notes; WireNotesPanel(); diff --git a/src/MandoCode.Desktop/Services/AgentCallsigns.cs b/src/MandoCode.Desktop/Services/AgentCallsigns.cs new file mode 100644 index 0000000..c40dde2 --- /dev/null +++ b/src/MandoCode.Desktop/Services/AgentCallsigns.cs @@ -0,0 +1,137 @@ +namespace MandoCode.Desktop.Services; + +/// +/// The callsign pool for agent naming — street-energy handles in the spirit of breaking +/// cyphers, phone phreaks, and construct crews, without naming any real person or property: +/// distinctive real-world handles are riffed rather than copied (the "Morphy" rule), and +/// everything else is ordinary words that just sound like they earned a spot in a cypher. +/// +/// Selection is a shuffled deck: no name repeats until the whole pool has been dealt, then it +/// reshuffles. The deck is app-session state — a restart reshuffles. Names already worn by an +/// open tab are skipped; in the (theoretical) case that every callsign is simultaneously in +/// use, naming falls back to numbers. +/// +public static class AgentCallsigns +{ + /// App-wide naming style for NEW agents — persisted in panel-state.json, toggled + /// from Settings → Behavior. Existing tabs keep whatever name they have. + public static bool Enabled { get; set; } + + private static readonly Random Rng = new(); + private static readonly List Deck = new(); + + public static string Next(IEnumerable takenTitles) + { + var titles = takenTitles.ToList(); + var taken = new HashSet(titles.Where(t => !string.IsNullOrEmpty(t))!, + StringComparer.OrdinalIgnoreCase); + + // Deal until a name lands that no open tab is wearing — reshuffling at most once per + // call, so a pool with nothing free degrades to numbers instead of spinning. + var reshuffled = false; + while (true) + { + if (Deck.Count == 0) + { + if (reshuffled) return AgentNaming.NextFreeName(titles); + Deck.AddRange(Pool); + for (var i = Deck.Count - 1; i > 0; i--) // Fisher–Yates + { + var j = Rng.Next(i + 1); + (Deck[i], Deck[j]) = (Deck[j], Deck[i]); + } + reshuffled = true; + } + + var name = Deck[^1]; + Deck.RemoveAt(Deck.Count - 1); + if (!taken.Contains(name)) return name; + } + } + + /// Forgets the current deal so the next draw starts a fresh shuffled cycle. + /// Exists for tests, which need cycle behavior to be observable from a known state. + public static void ResetDeck() => Deck.Clear(); + + public static readonly IReadOnlyList Pool = new[] + { + "Morphy", "Neo", "Trin", "Cypher", "Oracle", "Tank", "Dozer", "Mouse", + "Switch", "Link", "Seraph", "Keysmith", "Construct", "Sentinel", "Nebu", "Merov", + "Niobe", "Sati", "Smitty", "Rabbit", "Deja", "Anomaly", "Redshift", + // Hand-picked additions: + "Phteven", "Mandox", "Sequoia", "Tule", "Merrill", "Parker", "Rey", "Warlock", + "Kaweah", "Wichita", "KC", "Dodge", "Doppler", "Nimbus", "Cirus", + "Dorth", "Chiki", "Lulu", "Cripto", "Mandeezy", "Finity", "Runner", "ExJay", + "Wrangler", "Zonik", "M-117", "Loggic", "Blazor", "Michelle", "Han", "Twister", + "Cuamatzi", + "Condor", "Crunch", "Phreak", "Acid", "Burn", "Crash", "Override", "Zed", + "Razor", "Blade", "Falkon", "Root", "Sudo", "Kernel", + "Shell", "Grep", "Proxy", "Hex", "Null", "Void", + "Segfault", "Packet", "Socket", "Ping", "Tracer", "Payload", "Cache", + "Regex", "Lambda", "Quine", "Enigma", "Morse", "Opcode", "Byte", + "Chip", "Circuit", "Diode", "Neon", "Laser", "Photon", "Prism", + "Firewall", "Mainframe", "Codec", "Modem", "Terminal", + "Cloud", "Storm", "Wing", "Freeze", "Flare", "Halo", "Cyclone", + "Vortex", "Torque", "Kinetik", "Jetik", "Physix", "Rukus", "Havok", "Kaos", + "Vertigo", "Spinz", "Mills", "Boogie", "Flava", "Breaker", "Ghost", "Banshee", + "Funk", "Groove", "Riddim", "Tempo", "Beatz", "Blaze", "Ember", + "Inferno", "Frost", "Glacier", "Tundra", "Quake", "Tremor", "Rumble", "Thunder", + "Bolt", "Volt", "Surge", "Static", "Spark", "Flux", "Pulse", "Eclipse", + "Comet", "Meteor", "Nova", "Quasar", "Pulsar", "Nebula", "Orbit", "Zenith", + "Apex", "Vertex", "Kryptik", "Mystik", "Majik", "Logik", "Tekniq", "Uniq", + "Freq", "Sonik", "Kosmik", "Atomik", "Elektrik", "Dynamik", "Klassik", "Fanatik", + "Mekanik", "Organik", "Volkanik", "Galaktik", "Robotik", "Poetik", "Akrobat", "Hypnotik", + "Toxik", "Seismik", "Optik", "Grafik", "Frantik", "Drastik", "Tactik", + "Drift", "Skid", "Dash", "Vault", "Flip", + "Ollie", + "Sway", + "Strobe", "Flash", + "Ray", "Beam", "Lumen", "Lux", "Aurora", + "Titan", "Atlas", "Orion", "Vega", "Sirius", "Rigel", "Lyra", "Draco", + "Nyx", "Erebus", "Chronos", "Hyperion", "Icarus", "Nemesis", "Janus", "Juno", + "Ceres", "Vesta", "Io", "Callisto", "Europa", "Andromeda", + "Phoenix", "Gryphon", "Hydra", "Kraken", "Wyvern", "Basilisk", "Chimera", "Sphinx", + "Cobra", "Viper", "Mamba", "Python", "Adder", "Raptor", "Osprey", "Talon", + "Panther", "Lynx", "Ocelot", "Cheetah", "Wolf", "Lobo", "Coyote", "Vixen", + "Mantis", "Hornet", "Wasp", "Firefly", "Dragonfly", + "Rhino", "Bison", "Grizzly", "Kodiak", "Husky", "Akita", "Dingo", + "Mongoose", "Badger", "Serval", "Caracal", "Jackal", "Fennec", + "Ronin", "Shogun", "Sensei", "Ninja", "Shinobi", "Kunai", "Shuriken", "Katana", + "Sabre", "Rapier", "Dagger", "Kris", "Scimitar", "Falchion", "Cutlass", "Claymore", + "Bishop", "Rook", "Gambit", "Checkm8", "Blitz", "Bullet", + "Dice", "Domino", "Ace", "Deuce", "Wildcard", "Maverick", + "Rebel", "Rogue", "Bandit", "Outlaw", "Renegade", "Drifter", "Nomad", "Vagabond", + "Recon", "Stealth", "Decoy", "Smoke", "Mirage", "Cloak", "Veil", "Whisper", + "Scout", "Ranger", "Warden", "Sentry", "Vanguard", "Bastion", "Citadel", "Aegis", + "Paladin", "Ricochet", "Ballistix", "Zigzag", "Riddle", "Karma", "Mantra", + "Zen", "Halcyon", "Ozone", "Headrush", "Adrenalin", + "Specter", "Phantom", "Wraith", "Shade", "Shadow", "Eidolon", "Revenant", "Umbra", + "Onyx", "Obsidian", "Jade", "Cobalt", "Crimson", "Scarlet", "Indigo", "Violet", + "Slate", "Graphite", "Steel", "Iron", "Mercury", "Platinum", + "Titanium", "Tungsten", "Granite", "Basalt", "Flint", "Quartz", "Topaz", "Garnet", + "Argon", "Xenon", "Krypton", "Plasma", "Ion", "Isotope", "Quark", + "Proton", "Neutron", "Electron", "Fusion", "Fission", "Reactor", "Dynamo", "Turbine", + "Piston", "Throttle", "Clutch", "Nitro", "Turbo", "Redline", "Burnout", "Slipstream", + "Rocket", "Thruster", "Igniter", "Apogee", + "Radar", "Sonar", "Lidar", "Beacon", "Signal", "Uplink", + "Relay", "Conduit", "Transistor", "Amp", "Waveform", + "Echo", "Reverb", "Tremolo", "Crescendo", "Staccato", + "Allegro", "Forte", "Presto", "Octave", "Cadence", + "Scratch", "Fader", "Vinyl", "Needle", "Breakbeat", "Beatbox", + "Remix", "Dub", "Bassline", "Subz", "Snare", + "Monsoon", "Typhoon", "Sirocco", "Mistral", "Zephyr", "Gale", "Squall", "Tempest", + "Avalanche", "Icicle", "Polaris", "Boreal", "Arctic", "Chill", "Coldsnap", + "Solstice", "Equinox", "Midnight", "Dusk", "Dawn", "Nightfall", "Daybreak", + "Horizon", "Meridian", + "Canyon", "Mesa", "Dune", "Oasis", "Sahara", "Savanna", "Delta", "Ridge", + "Summit", "Crag", "Bluff", "Cliff", "Fjord", "Reef", + "Rapids", "Cascade", "Torrent", "Riptide", "Undertow", "Geyser", "Wake", + "Stencil", "Tag", "Aerosol", "FatKap", "Wildstyle", "Throwie", "Burner", + "Flexx", "Twista", "Spida", "Casper", "Primo", "Nollie", "Fakie", + "Hurricane", "Tornado", "Loki", "Odin", "Freya", "Fenrir", "Valkyrie", + "Zeus", "Hermes", "Apollo", "Artemis", "Athena", "Ares", "Helios", "Selene", + "Wisp", "Cinder", "Ash", + "Pixel", "Sprite", "Voxel", "Shader", "Raster", "Vector", + "Render", "Framez", "Raycast", "Skybox", "Bloom", + }; +} diff --git a/src/MandoCode.Desktop/Services/PanelState.cs b/src/MandoCode.Desktop/Services/PanelState.cs index 6b88421..b39693a 100644 --- a/src/MandoCode.Desktop/Services/PanelState.cs +++ b/src/MandoCode.Desktop/Services/PanelState.cs @@ -16,7 +16,8 @@ public sealed record PanelStateShape( DateTimeOffset? HistorySeenAt = null, List? CollapsedNoteGroups = null, string? LastNotePath = null, - string? NoteModel = null); + string? NoteModel = null, + bool? AgentCallsigns = null); /// /// Persists per-panel UI preference — the fold state of the Snapshots and History project groups — @@ -45,7 +46,8 @@ public static PanelStateShape Load() shape.HistorySeenAt, shape.CollapsedNoteGroups ?? new(), shape.LastNotePath, - shape.NoteModel); + shape.NoteModel, + shape.AgentCallsigns); } } catch { /* corrupt/unreadable — start with everything expanded */ } From 170c32f7b355bd6686d9a21937c93a3e65bde347 Mon Sep 17 00:00:00 2001 From: DevMando Date: Mon, 27 Jul 2026 10:05:37 -0700 Subject: [PATCH 2/2] Give agents their name as a spoken identity The tab's name now reaches the model and the transcript. AgentSession stamps Config.AgentName (new in the engine, null-safe for the CLI) via its Title setter BEFORE building AIService, so the baked system prompt introduces the agent as '{name}, running on MandoCode' from the first message. Renaming a tab notifies the live conversation through the workspace-note channel. Reply cards are labeled with the agent's name instead of a hardcoded MandoCode - the platform stays the stage, the agent is the actor. Rolls the harness pin for AgentName support. --- CHANGELOG.md | 11 +++++++++++ MandoCode | 2 +- .../ResponseStreamerTests.cs | 2 +- src/MandoCode.Desktop/MainWindow.History.cs | 6 ++++++ .../Services/AgentSession.cs | 19 +++++++++++++++---- .../Services/ITranscriptHtml.cs | 2 +- .../Services/SessionManager.cs | 9 ++++++--- .../Services/TranscriptHtmlBuilder.cs | 7 +++++-- .../ViewModels/ChatController.cs | 2 +- .../ViewModels/ResponseStreamer.cs | 2 +- 10 files changed, 48 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1071215..428a7e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,17 @@ recorded by the `MandoCode` submodule. ## [Unreleased] ### Added +- **Agent callsigns.** A Settings → Behavior toggle (app-wide) names new agents from a + curated 500+ pool of handles — construct-crew, phreak, and cypher energy ("Morphy", + "Crunch", "Blazor", "Kaos") — drawn from a shuffled deck that doesn't repeat until it runs + dry. Off (the default) keeps "Agent 1, Agent 2, …"; renaming a tab works either way. +- **Agents know their own name.** The tab's name is the agent's spoken identity: reply cards + are labeled with it, and the system prompt introduces the model as "{name}, a local AI + coding assistant running on MandoCode" — so saying "hi" to Blazor gets Blazor, not a + confused MandoCode. Renaming a tab tells the live conversation. (Engine support is + null-safe: the CLI keeps its classic MandoCode identity untouched.) +- **Version in the title bar.** The window title reads "MandoCode Desktop v{version}", + sourced from the same assembly version the update checker compares against releases. - **Music player.** A music icon on the left rail opens a compact player: play/pause, next, stop, volume, and a playlist picker. While music plays the rail icon becomes an animated gold equalizer, and hovering it names the current track. **Add playlist** points at any diff --git a/MandoCode b/MandoCode index 279ccb6..9d2bde3 160000 --- a/MandoCode +++ b/MandoCode @@ -1 +1 @@ -Subproject commit 279ccb667cc1e86e8e08e4eeded9889a94272b33 +Subproject commit 9d2bde3f44ac6fa6880bf01bcb4b5f6e04f6926b diff --git a/src/MandoCode.Desktop.Tests/ResponseStreamerTests.cs b/src/MandoCode.Desktop.Tests/ResponseStreamerTests.cs index a3ed002..4e1be7d 100644 --- a/src/MandoCode.Desktop.Tests/ResponseStreamerTests.cs +++ b/src/MandoCode.Desktop.Tests/ResponseStreamerTests.cs @@ -19,7 +19,7 @@ public sealed class ResponseStreamerTests // Tags each fragment type so a test can assert which builder method produced a transcript block. private sealed class TagHtml : ITranscriptHtml { - public string AssistantCard(string markdown) => $"CARD:{markdown}"; + public string AssistantCard(string markdown, string? speaker = null) => $"CARD:{markdown}"; public string Warn(string text) => $"WARN:{text}"; public string Error(string text) => $"ERR:{text}"; public string Dim(string text) => $"DIM:{text}"; diff --git a/src/MandoCode.Desktop/MainWindow.History.cs b/src/MandoCode.Desktop/MainWindow.History.cs index 15b3a52..4c7ebc4 100644 --- a/src/MandoCode.Desktop/MainWindow.History.cs +++ b/src/MandoCode.Desktop/MainWindow.History.cs @@ -554,6 +554,12 @@ private async Task RenameTabAsync(ChatTabEntry entry) entry.View.Session.Title = name; entry.Label.Text = name; RefreshTabStrip(); + + // The system prompt was baked at session start, so a live conversation learns the new + // name the way it learns other outside-the-conversation facts; the next fresh session + // bakes it properly via the Title setter's Config.AgentName stamp. + entry.View.Session.Controller.NoteWorkspaceEvent( + $"The user renamed you — your name is now “{name}”."); } /// Selecting an agent also returns you to the chat page — the Settings you were diff --git a/src/MandoCode.Desktop/Services/AgentSession.cs b/src/MandoCode.Desktop/Services/AgentSession.cs index afa2f55..f30d3be 100644 --- a/src/MandoCode.Desktop/Services/AgentSession.cs +++ b/src/MandoCode.Desktop/Services/AgentSession.cs @@ -34,8 +34,16 @@ public sealed class AgentSession public int Id { get; } - /// Tab-strip label. Defaults to the project folder's leaf name. - public string Title { get; set; } + /// Tab-strip label AND the agent's spoken identity: setting it also stamps + /// on this session's config clone, so the system + /// prompt introduces the agent by this name on the next prompt rebuild (construction, + /// settings refresh, or model switch). Defaults to the project folder's leaf name. + public string Title + { + get => _title; + set { _title = value; Config.AgentName = value; } + } + private string _title = ""; /// Durable identity across app launches (unlike , a process-local /// counter). Names this session's transcript journal on disk; a restored tab passes its @@ -67,7 +75,8 @@ public AgentSession( ConfigCoordinator configs, McpCoordinator mcp, string projectRoot, - string? persistKey = null) + string? persistKey = null, + string? title = null) { Id = Interlocked.Increment(ref _nextId); PersistKey = string.IsNullOrWhiteSpace(persistKey) ? Guid.NewGuid().ToString("N") : persistKey; @@ -83,7 +92,9 @@ public AgentSession( Config = configs.CreateClone(); ProjectRoot = new ProjectRootAccessor(projectRoot); - Title = FolderLabel(projectRoot); + // Before AIService below: its constructor bakes the system prompt, and the agent's + // spoken identity (Config.AgentName, stamped by the Title setter) must be in it. + Title = title ?? FolderLabel(projectRoot); Tokens = new TokenTrackingService(); PlanHandoff = new PlanHandoff(); diff --git a/src/MandoCode.Desktop/Services/ITranscriptHtml.cs b/src/MandoCode.Desktop/Services/ITranscriptHtml.cs index d4cd4a1..15e123d 100644 --- a/src/MandoCode.Desktop/Services/ITranscriptHtml.cs +++ b/src/MandoCode.Desktop/Services/ITranscriptHtml.cs @@ -8,7 +8,7 @@ namespace MandoCode.Desktop.Services; /// public interface ITranscriptHtml { - string AssistantCard(string markdown); + string AssistantCard(string markdown, string? speaker = null); string Warn(string text); string Error(string text); string Dim(string text); diff --git a/src/MandoCode.Desktop/Services/SessionManager.cs b/src/MandoCode.Desktop/Services/SessionManager.cs index a85fd97..f31421a 100644 --- a/src/MandoCode.Desktop/Services/SessionManager.cs +++ b/src/MandoCode.Desktop/Services/SessionManager.cs @@ -50,8 +50,9 @@ public SessionManager( public AgentSession CreateSession(string? projectRoot = null, string? persistKey = null) { var root = projectRoot ?? Active?.ProjectRoot.ProjectRoot ?? _initialProjectRoot; - var session = new AgentSession(_globals, _configs, _mcp, root, persistKey); - session.Title = NextAgentName(); + // Named at construction, not after: AIService bakes the system prompt in its ctor, + // and the callsign must be the identity in it from the first message. + var session = new AgentSession(_globals, _configs, _mcp, root, persistKey, NextAgentName()); _sessions.Add(session); Activate(session); @@ -63,7 +64,9 @@ public AgentSession CreateSession(string? projectRoot = null, string? persistKey /// label just distinguishes agents; the user can rename it. Reuses the lowest free number so /// closing "Agent 2" then opening a new one gives "Agent 2" again, not an ever-climbing count. /// - private string NextAgentName() => AgentNaming.NextFreeName(_sessions.Select(s => s.Title)); + private string NextAgentName() => AgentCallsigns.Enabled + ? AgentCallsigns.Next(_sessions.Select(s => s.Title)) + : AgentNaming.NextFreeName(_sessions.Select(s => s.Title)); public void Activate(AgentSession session) { diff --git a/src/MandoCode.Desktop/Services/TranscriptHtmlBuilder.cs b/src/MandoCode.Desktop/Services/TranscriptHtmlBuilder.cs index 907832e..292b5bb 100644 --- a/src/MandoCode.Desktop/Services/TranscriptHtmlBuilder.cs +++ b/src/MandoCode.Desktop/Services/TranscriptHtmlBuilder.cs @@ -51,8 +51,11 @@ public string FromMarkdown(string markdown) public string UserEcho(string text) => $"
> {E(text)}
"; - public string AssistantCard(string markdown) => - $"
MandoCode
{FromMarkdown(markdown)}
"; + /// Null speaker keeps the classic MandoCode label — used by surfaces with no + /// agent (the appearance preview). Per-agent callers pass the tab's name so the card + /// agrees with what the system prompt told the model it's called. + public string AssistantCard(string markdown, string? speaker = null) => + $"
{E(speaker ?? "MandoCode")}
{FromMarkdown(markdown)}
"; public string Info(string text) => $"
{E(text)}
"; public string Success(string text) => $"
{E(text)}
"; diff --git a/src/MandoCode.Desktop/ViewModels/ChatController.cs b/src/MandoCode.Desktop/ViewModels/ChatController.cs index 1405fce..d5bd06c 100644 --- a/src/MandoCode.Desktop/ViewModels/ChatController.cs +++ b/src/MandoCode.Desktop/ViewModels/ChatController.cs @@ -767,7 +767,7 @@ private async Task HandleProgressEventAsync(TaskProgressEvent progressEvent, Tas PlanProgressChanged?.Invoke(progressEvent.CurrentStep, progressEvent.TotalSteps, true); if (!string.IsNullOrEmpty(progressEvent.Message)) { - _transcript.Append(_html.AssistantCard(progressEvent.Message)); + _transcript.Append(_html.AssistantCard(progressEvent.Message, _config.AgentName)); ConversationLogger?.Invoke("a", progressEvent.Message!); } _transcript.Append(_html.Success($"Step {progressEvent.CurrentStep} completed.")); diff --git a/src/MandoCode.Desktop/ViewModels/ResponseStreamer.cs b/src/MandoCode.Desktop/ViewModels/ResponseStreamer.cs index 569264b..75a2ef1 100644 --- a/src/MandoCode.Desktop/ViewModels/ResponseStreamer.cs +++ b/src/MandoCode.Desktop/ViewModels/ResponseStreamer.cs @@ -77,7 +77,7 @@ public async Task StreamAsync(string input, CancellationToken token) if (segment.Length > 0) { segments.Add(segment); - _transcript.Append(_html.AssistantCard(segment)); + _transcript.Append(_html.AssistantCard(segment, _config.AgentName)); ConversationLogger?.Invoke("a", segment); } } while (await enumerator.MoveNextAsync());