From f004a2c36e8653bbd7b45b3554fb83762edac02a Mon Sep 17 00:00:00 2001 From: Armando Fernandez Date: Fri, 10 Jul 2026 14:39:23 -0700 Subject: [PATCH] Add multi-agent tabs, context snapshots, and tab UX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Multiple concurrent agents, one per tab — each with its own conversation, project folder, model, and settings. The on-disk config becomes the defaults a new agent starts on; "Make Default for New Agents" is the only path that writes it. - Per-agent object graph (AgentSession) mixing app-wide singletons with fresh per-tab instances; SessionManager owns the set. The three approval services are per-agent for correctness: the "don't ask again" bypass set, the SemaphoreSlim(1,1) prompt gate, and the single-assignment approval delegates each corrupt other tabs when shared. - ConfigCoordinator is the sole caller of MandoCodeConfig.Save(), enforced by the MANDO001 build guard; config clones are a JSON round-trip followed by a mandatory ValidateAndClamp() (rebuilds McpServers with the case-insensitive comparer System.Text.Json drops). - McpCoordinator owns the single McpClientManager on a host config with MCP always enabled; the per-agent flag controls tool attachment; /mcp-reload fans out to every agent's kernel. - Context snapshots: capture the conversation the instant before a model switch clears it (and on demand via a tab's options menu), managed from a global left-rail panel; Import arms the active agent's next message. HistorySummarizer ports the harness's private compaction summary via the public GetHistoryAsync(); full history is stored for a future LLM-enhanced recap. - Tabs named Agent 1/2/...; per-tab options menu (rename / snapshot / export / close, close greyed on the last agent); header model quick-switch dropdown; session status and events render as themed status chips (green = healthy, grey = event, gold = soft warning). Fixes over the single-agent baseline: cross-agent approval isolation, WebView2 disposal on tab/window close, NRE on tab open/close (symmetric event handling), and accessibility names on icon-only buttons. Docs: CHANGELOG [Unreleased] and README updated. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 128 ++ README.md | 97 +- src/MandoCode.Desktop/App.xaml.cs | 85 +- .../Controls/ChatTabView.xaml | 256 ++++ .../Controls/ChatTabView.xaml.cs | 935 +++++++++++++++ src/MandoCode.Desktop/MainWindow.xaml | 396 ++++--- src/MandoCode.Desktop/MainWindow.xaml.cs | 1028 ++++++----------- .../MandoCode.Desktop.csproj | 18 + .../Services/AgentSession.cs | 121 ++ .../Services/ConfigCoordinator.cs | 129 +++ .../Services/ContextSnapshot.cs | 46 + .../Services/HistorySummarizer.cs | 93 ++ .../Services/McpCoordinator.cs | 112 ++ .../Services/SessionManager.cs | 99 ++ .../Services/SnapshotStore.cs | 59 + .../Services/TranscriptHtmlBuilder.cs | 31 + .../ViewModels/ChatController.Wizards.cs | 14 +- .../ViewModels/ChatController.cs | 298 ++++- 18 files changed, 2952 insertions(+), 993 deletions(-) create mode 100644 src/MandoCode.Desktop/Controls/ChatTabView.xaml create mode 100644 src/MandoCode.Desktop/Controls/ChatTabView.xaml.cs create mode 100644 src/MandoCode.Desktop/Services/AgentSession.cs create mode 100644 src/MandoCode.Desktop/Services/ConfigCoordinator.cs create mode 100644 src/MandoCode.Desktop/Services/ContextSnapshot.cs create mode 100644 src/MandoCode.Desktop/Services/HistorySummarizer.cs create mode 100644 src/MandoCode.Desktop/Services/McpCoordinator.cs create mode 100644 src/MandoCode.Desktop/Services/SessionManager.cs create mode 100644 src/MandoCode.Desktop/Services/SnapshotStore.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 33c691c..daeec30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,134 @@ recorded by the `MandoCode` submodule. ## [Unreleased] +Multiple agents, one window. Each tab is an independent agent with its own conversation, +project folder, model, and settings — and the config file stops being "the current settings" +and becomes "the defaults a new agent starts on." + +### Why this matters + +One conversation at a time is the wrong shape for real work. You want a cloud model planning +in one tab while a local one grinds through a refactor in another, each pointed at a different +folder. Everything below exists to make that safe rather than merely possible: three of the +fixes are for bugs that would have silently corrupted one agent from another, and none of them +are visible until you actually open a second tab. + +### Added +- **Agent tabs.** A `+` button opens another agent — its own conversation, project folder, + model, and settings. Each tab carries its own header: connection dot, model switcher, token + count, project folder path, save-transcript and open-folder buttons. Closing the last agent + is refused (Settings and MCP have no agent to act on without one). +- **Per-agent settings.** The Settings page acts on the selected agent and says whose settings + you're editing. Changes apply live to that agent alone, for that session. Nothing is written + to disk until you press **Make Default for New Agents**, which snapshots that agent's settings + into `~/.mandocode/config.json`. Agents already open keep their own. +- **Cross-agent approval routing.** An approval raised in a background agent badges its tab in + gold and the toast names the agent (`Click to review in "MandoCode 2"`) — with several agents + running, "an approval is waiting" is useless without saying where. +- **Per-agent MCP.** Servers are one app-wide set (they're OS processes), but each agent decides + whether to attach their tools. Enabling MCP for an agent starts the shared servers if they + aren't running yet. +- **Context snapshots (history points).** Switching a model clears the conversation, so the instant + before it clears, the outgoing conversation is captured as a snapshot — origin model, timestamp, + and a compact recap. A **Snapshots** icon on the left rail (with a count badge) opens a global + management panel — docked left at ~37% width so the active chat stays visible — listing every + tab's snapshots. **Import** arms a snapshot so its recap rides along, invisibly, with the *active* + agent's next message, carrying the context into any model. The store is app-wide, so a snapshot + taken in one tab imports into a brand-new tab on a capable model. **Take snapshot** (tab options + menu) captures on demand without switching. The recap is a deterministic port of the harness's own + compaction summary (`HistorySummarizer`), fed by the public `AIService.GetHistoryAsync()` — no + submodule change. The full history is stored alongside each snapshot so a richer LLM summary can + be generated later without the original conversation still being live. +- **Per-tab options menu.** The tab's `⋯` menu carries Rename, Take snapshot, Export transcript, + and Close. It replaces the bare close button — which, on the last remaining agent, was an `X` + you were not allowed to use; Close is now simply greyed out there. +- **Model quick-switch dropdown.** Clicking the model in a tab's header drops a list anchored to the + button (cloud models first, `cloud`/`local` badges, current one preselected) instead of a + full-screen modal. It opens instantly with a loading spinner while the model list is fetched, and + shows connection/empty-list errors inline. The typed `/model` command still uses the overlay wizard. + +### Changed +- **`/model` is an agent-local switch** and no longer writes to disk; the model button in each + agent's header opens the same picker. `/setup` and the Settings page still set the app-wide + default, because they configure the app rather than one agent. +- **`enableDiffApprovals` applies live, per agent.** The CLI marks it "restart required" because + it wires the approval delegates once at startup against a shared `AIService`; each agent now + owns its own, so the toggle attaches and detaches them on the spot. +- **`/exit` no longer disposes the music player.** Shared resources belong to the window: closing + it (by any route) now disposes the audio device and every agent's WebView2. +- **Chat moved out of `MainWindow`** into a `ChatTabView` user control (`MainWindow.xaml.cs`: + 1368 → 930 lines; the chat surface plus tab plumbing is now its own 867-line control). It + implements `IApprovalUi` against its *own* overlay, which is what makes concurrent approvals + safe rather than a race. +- Settings and MCP stay full-screen sidebar pages, not tabs; selecting an agent returns to chat. +- Two settings are now labelled app-wide, because they are: **Appearance** (a property of the + window, stored outside the shared config) and **Context window** (applied as + `OLLAMA_CONTEXT_LENGTH` when MandoCode starts the daemon — one daemon, one context window). +- **Agents are named `Agent 1`, `Agent 2`, …** by default, not the folder's leaf name (the folder + shows in the header already). Numbers fill the lowest free slot, so closing `Agent 2` and opening + a new tab gives `Agent 2` again rather than an ever-climbing count. Renaming a tab (options menu) + or changing its folder no longer overwrites the label — it persists across both. +- **Session status and events render as chips.** Startup, model-switch, MCP-connected, context + cleared, context imported, and snapshot-saved lines are now status **chips** — a themed CSS status + dot instead of an emoji, so they recolor with the theme and render identically everywhere. The dot + carries meaning: **green** = healthy/ready (connected, ready, now active, snapshot saved), **grey** + = an informational event (context cleared, context imported), **gold** = a soft warning. A switch + clears the live context but leaves the visible transcript, so the `Context cleared` chip is what + makes the reset explicit rather than silent. + +### Fixed +- **"Approve — don't ask again" leaked across agents.** `WinUiApprovalService` held the bypass set + and approved-file list as singleton state, so a blanket approval in one chat silently + auto-approved writes in every other. It is now per-agent. +- **An unanswered approval in one agent blocked every other agent's approval from rendering.** + `ApprovalPromptGate` is a `SemaphoreSlim(1,1)` built to serialize prompts on one console; shared + across agents, tab B simply looked hung. It is now per-agent. +- **The last agent constructed stole every approval.** `ChatController` assigns (not `+=`) five + handlers — `PlanHandoff.OnPlanRequested`, `AIService.On{Write,Delete,Command}ApprovalRequested`, + `McpApprovalGate.OnApprovalRequested`. Single-assignment delegates on shared services mean last + writer wins. Each agent now owns those services, so there is exactly one writer per graph. +- **MCP could never start if the saved default had it off.** `McpClientManager` gates + `StartAllAsync` on `EnableMcp`, which is now a per-agent setting — so `"enableMcp": false` in + the defaults starved every agent that turned MCP on, with no error. `McpCoordinator` owns the + manager and runs it on a host config that always has MCP enabled; the per-agent flag controls + only whether that agent attaches the tools. +- **`/mcp-reload` only refreshed the agent that ran it.** Other agents kept stale tool handles. + Reload now resets each agent's MCP session approvals, restarts the shared servers once, and + re-registers tools on every agent's kernel (history preserved). +- **`NullReferenceException` opening and closing tabs.** `ChatTabView` subscribed to nine harness + events and unsubscribed from none, and `Shutdown()` closed the `CoreWebView2` while leaving the + ready flag set — so a closing agent's unwinding turn drove transcript writes into a null + `CoreWebView2`. On open, `EnsureCoreWebView2Async()` ran before the control was `Loaded`, + leaving `CoreWebView2` null. Subscriptions are now symmetric, initialization waits for `Loaded` + and null-checks, every script path is guarded, and a tab is shut down before it is unparented. +- **WebView2 was never disposed.** Closing an agent left its browser processes running for the + rest of the session, and closing the window left them orphaned. Both now reap. +- **Screen readers saw unnamed buttons.** The tab close buttons and the MCP page's action buttons + wrap an icon in a panel, exposing no accessible name. All are named now. + +### Guardrails +- `ConfigCoordinator` is the only code in the app that calls `MandoCodeConfig.Save()`. That can't + be enforced by the type system — `Save()` is public and non-virtual on a type in the read-only + harness submodule — so a build target (`MANDO001`) fails the build if `ChatController` ever + calls `_config.Save()` on its per-agent clone, which would publish one agent's model as + everybody's default. +- Cloning the config is a JSON round-trip followed by a mandatory `ValidateAndClamp()`. + `System.Text.Json` rebuilds `McpServers` with the default case-sensitive comparer; without the + clamp, every MCP lookup in the clone silently misses on a casing difference. + +### Not done +- Each agent holds a live WebView2 (tens of MB). A retained transcript log would let background + agents defer creating one until first shown. +- Agent settings are session-scoped by design and are not restored on launch. +- **LLM-enhanced snapshots.** The snapshot data model reserves an AI recap (`AiRecap`; the `Tag` + flips `Light`→`AI`, and `BestRecap` prefers it), but there is no "Enhance" action yet. A clean + LLM summary needs a small public seam on `AIService` — a no-tools completion on a side history — + added at the next submodule pin roll; the existing side-channels either fire tools + (`ExecutePlanStepAsync`) or would corrupt the live conversation. The panel is already built, so + it's a button plus one method once the seam lands. +- **Snapshots are session-scoped**, in memory only — they vanish on app close. Persisting them to + disk is a possible follow-up (it would need a store to name and garbage-collect). + ## [0.1.0] — 2026-07-07 The first MandoCode Desktop — the MandoCode AI coding agent with a native diff --git a/README.md b/README.md index ef97db4..4822b6e 100644 --- a/README.md +++ b/README.md @@ -50,13 +50,17 @@ compile independently of the CLI's originals): | `Services/WinUiApprovalService.cs` | `Services/Approval/DiffApprovalHandler.cs` | | `Services/TranscriptHtmlBuilder.cs` | `MarkdownHtmlRenderer` / `OperationDisplayRenderer` | +The harness is safe to instantiate once per agent — its statics are pure functions and readonly +`Regex`, and `AIService` takes every collaborator by constructor. Rolling the pin forward, watch +for that changing. + ## Architecture | Layer | CLI (MandoCode) | Desktop (this app) | |---|---|---| -| Orchestrator | `Components/App.razor` interactive loop | `ViewModels/ChatController.cs` (faithful port) | -| Approvals | `DiffApprovalHandler` (Spectre panels) | `Services/WinUiApprovalService.cs` + XAML overlay (same labels, bypass state, `DiffApprovalResult` contract) | -| Transcript | ANSI scrollback + Spectre renderables | WebView2 + `TranscriptHtmlBuilder` (Markdig HTML, dark theme) | +| Orchestrator | `Components/App.razor` interactive loop | `ViewModels/ChatController.cs` (faithful port), one per agent | +| Approvals | `DiffApprovalHandler` (Spectre panels) | `Services/WinUiApprovalService.cs` + each agent's own XAML overlay (same labels, bypass state, `DiffApprovalResult` contract) | +| Transcript | ANSI scrollback + Spectre renderables | WebView2 + `TranscriptHtmlBuilder` (Markdig HTML, themed) | | Busy/spinner | `SpinnerService` (ANSI) | `BusyStateService` → ProgressRing | | Onboarding | `OnboardingFlow` terminal prompts | `/setup` wizard + Settings page | | Everything else | `Services/`, `Plugins/`, `Models/` | **reused verbatim via project reference** | @@ -66,6 +70,73 @@ Key seams the harness already provided (unchanged): `AIService.ChatStreamAsync`, `McpApprovalGate.OnApprovalRequested`, `TaskPlannerService.ExecutePlanAsync` progress events, `DiffService` diff models. +## Agents + +Each tab is an independent agent. `Services/AgentSession.cs` hand-constructs one agent's object +graph; `SessionManager` owns the set of them. The split matters: + +| Per agent | App-wide | +|---|---| +| `AIService` (its conversation, its model), `ChatController`, `TaskPlannerService` | The `MandoCodeConfig` on disk — the **defaults** a new agent starts on | +| `MandoCodeConfig` clone, `ProjectRootAccessor`, `SkillLoader`, `FileAutocompleteProvider` | `McpClientManager` (one set of server processes) | +| `TokenTrackingService`, `PlanHandoff`, `TranscriptWriter`, `BusyStateService`, `ShellRunner` | `MusicPlayerService`, `ThemeManager`, `TranscriptHtmlBuilder` | +| `WinUiApprovalService`, `ApprovalPromptGate`, `McpApprovalGate` | `ConfigCoordinator`, `McpCoordinator`, `SessionManager`, `SnapshotStore` | + +Tabs default to `Agent 1`, `Agent 2`, … (the folder shows in the header); the number reuses the +lowest free slot, and a rename or folder change never overwrites it. Each tab's `⋯` options menu +carries Rename, Take snapshot, Export transcript, and Close — Close is greyed on the last remaining +agent (Settings and MCP need one to act on). The model in each header opens a quick-switch dropdown +(cloud first, `cloud`/`local` badges) rather than a full-screen picker. + +The three approval services are per-agent for **correctness**, not tidiness. Shared, they break +in ways that are invisible until a second tab exists: `WinUiApprovalService` holds the +"don't ask again" bypass set, so one agent's blanket approval would auto-approve writes in every +other; `ApprovalPromptGate` is a `SemaphoreSlim(1,1)` built for one console, so an unanswered +approval in one agent would stop another's from ever rendering; and `ChatController` **assigns** +(not `+=`) five approval delegates, so on shared services the last agent constructed silently +steals every approval. + +Two settings can't be per-agent and are labelled app-wide in the UI: **Appearance** is a property +of the window (and lives outside the shared config), and **Context window** is applied as +`OLLAMA_CONTEXT_LENGTH` when MandoCode starts the Ollama daemon — one daemon, one context window. + +### Settings and the config file + +`~/.mandocode/config.json` is not "the current settings". It is the **defaults a new agent starts +on**. Editing Settings changes the selected agent, live, for that session; **Make Default for New +Agents** is the only action that writes the file (plus corrections like a healed endpoint URL, the +onboarding flag, and the app-wide MCP server list). + +`ConfigCoordinator` is the only code that calls `MandoCodeConfig.Save()`. The rule can't be +enforced by the type system — `Save()` is public and non-virtual on a harness type — so the +`MANDO001` build target fails the build if `ChatController` calls `_config.Save()` on its clone, +which would publish one agent's model as everybody's default. Cloning is a JSON round-trip +**followed by `ValidateAndClamp()`**: `System.Text.Json` rebuilds `McpServers` with a +case-sensitive comparer, and without the clamp every MCP lookup in the clone silently misses on +a casing difference. + +### Context snapshots + +Switching a model clears the conversation (a different model mid-history is a different +conversation). The instant before it clears, the outgoing conversation is captured as a +`ContextSnapshot` — origin model, timestamp, a deterministic recap, and the full history. The +recap comes from `HistorySummarizer`, a port of the harness's own (private) compaction summary fed +by the public `AIService.GetHistoryAsync()` — so no submodule change. The **Snapshots** rail icon +opens a global panel (the `SnapshotStore` is app-wide, one list for every tab); **Import** arms a +snapshot's recap to ride along, invisibly, with the active agent's next message — carrying context +into any model. `Take snapshot` on a tab's `⋯` menu captures on demand without switching. + +The snapshot keeps the full history so a richer LLM summary can be generated later (the model +reserves `AiRecap`, the `Tag` flips `Light`→`AI`); that "Enhance" action waits on a small no-tools +completion seam added at the next harness pin roll. Snapshots are session-scoped and in memory only. + +### Why the tab strip isn't a `TabView` + +WinUI's `TabView` hosts only the selected item's content, which detaches the previous tab and +closes its `CoreWebView2`. `TranscriptWriter` retains nothing — the WebView2 DOM is the only copy +of a conversation — so that would destroy the transcript on every tab switch. The strip carries +headers only; content lives in `Visibility`-toggled sibling panels and is never re-parented. + ## Releasing Push a tag (`git tag v0.2.0 && git push --tags`) and the Release workflow @@ -83,13 +154,23 @@ within 24 hours. - Slash commands with autocomplete: /help /clear /model /config /retry /learn /copy /copy-code /skills /force-skill /mcp /mcp tools /mcp remove /mcp-reload /music* /command /exit — plus `!cmd` shell escape and `@file` references -- Token tracking in the status bar + per-response summaries -- Sidebar navigation: Chat, Settings, and MCP pages +- Token tracking + per-response summaries, per agent +- Agent tabs — `+` opens another agent (`Agent 1`, `Agent 2`, …) with its own + conversation, project folder, model, and settings; an approval waiting in a + background agent badges its tab and the toast names it. Each tab's `⋯` menu: + Rename, Take snapshot, Export transcript, Close (greyed on the last agent). The + header model opens a quick-switch dropdown (cloud first, `cloud`/`local` badges) +- Context snapshots — the conversation is captured the instant a model switch would + clear it (and on demand via `⋯` → Take snapshot); a global left-rail panel lists + every tab's snapshots and Import carries one into the active agent's next message +- Sidebar: Settings and MCP as full-screen pages, acting on the selected agent - Settings — the whole config as a native form (toggles, sliders, number boxes, - grouped Connection/Generation/Behavior/Limits/Integrations); every change is - validated and applied through the shared ConfigKeySetter, same as the CLI + grouped Appearance/Connection/Generation/Behavior/Limits/Integrations); every + change is validated through the shared ConfigKeySetter, same as the CLI, and + applies to that agent alone. "Make Default for New Agents" saves it to disk - MCP — live server list with status/tool counts; add/edit servers in a single - form modal with a Test button (isolated connection check + tool table preview) + form modal with a Test button (isolated connection check + tool table preview). + Servers are one app-wide set; each agent chooses whether to attach their tools - Guided wizards, built on the approval-overlay select + text primitives: - `/setup` — probe/start Ollama, change endpoint, pull a starter model with live progress, model picker, cloud-auth check + sign-in walkthrough diff --git a/src/MandoCode.Desktop/App.xaml.cs b/src/MandoCode.Desktop/App.xaml.cs index 885b23d..a808f1a 100644 --- a/src/MandoCode.Desktop/App.xaml.cs +++ b/src/MandoCode.Desktop/App.xaml.cs @@ -51,74 +51,49 @@ private static ServiceProvider BuildServices() services.AddSingleton(config); - // Project root: first command-line arg, else current directory. The UI also - // has an "Open Folder" action that mutates ProjectRootAccessor at runtime. + // Project root the FIRST tab opens on: first command-line arg, else current directory. + // Every tab owns its own ProjectRootAccessor thereafter (see AgentSession), so the + // folder button retargets one tab without disturbing the others. var cmdArgs = Environment.GetCommandLineArgs().Skip(1).ToArray(); var projectRoot = cmdArgs.Length > 0 && System.IO.Directory.Exists(cmdArgs[0]) ? System.IO.Path.GetFullPath(cmdArgs[0]) : Environment.CurrentDirectory; - services.AddSingleton(new ProjectRootAccessor(projectRoot)); - // ---- Harness services, reused verbatim ---- - // SpinnerService writes ANSI to Console, which is a harmless no-op in a - // windowed app (no console attached). It's still registered because AIService - // takes it as a constructor dependency; the real busy UI is BusyStateService. + // ---- App-global singletons ---- + // Everything that carries per-conversation state (AIService, ChatController, the three + // approval gates, TranscriptWriter, BusyStateService, TokenTrackingService, …) is built + // per tab by AgentSession and is deliberately NOT registered here. + // + // SpinnerService writes ANSI to Console, a harmless no-op in a windowed app (no console + // attached). It's still registered because AIService takes it; the real busy UI is the + // per-tab BusyStateService. services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - services.AddSingleton(provider => - { - var cfg = provider.GetRequiredService(); - var root = provider.GetRequiredService(); - return new SkillLoader(cfg, root); - }); - - services.AddSingleton(provider => new McpClientManager(provider.GetRequiredService())); - services.AddSingleton(provider => new McpApprovalGate(provider.GetRequiredService())); - - services.AddSingleton(provider => - { - var cfg = provider.GetRequiredService(); - var tokenTracker = provider.GetRequiredService(); - var root = provider.GetRequiredService(); - var planHandoff = provider.GetRequiredService(); - var skillLoader = provider.GetRequiredService(); - var mcpManager = provider.GetRequiredService(); - var mcpGate = provider.GetRequiredService(); - var spinner = provider.GetRequiredService(); - return new AIService(root, cfg, tokenTracker, planHandoff, skillLoader, mcpManager, mcpGate, spinner); - }); - - services.AddSingleton(provider => - { - var ai = provider.GetRequiredService(); - var cfg = provider.GetRequiredService(); - return new TaskPlannerService(ai, cfg); - }); - services.AddSingleton(provider => new MusicPlayerService(provider.GetRequiredService())); + // The single McpClientManager is created and owned by McpCoordinator, not registered here: + // it must run on a config that always has MCP enabled, because EnableMcp is a per-agent + // setting in this app. See the note on McpCoordinator._hostConfig. - services.AddSingleton(provider => - { - var cfg = provider.GetRequiredService(); - var ignoreDirs = new HashSet(MandoCodeConfig.DefaultIgnoreDirectories); - foreach (var dir in cfg.IgnoreDirectories) ignoreDirs.Add(dir); - var root = provider.GetRequiredService(); - return new FileAutocompleteProvider(root, ignoreDirs); - }); + // One audio device. + services.AddSingleton(provider => new MusicPlayerService(provider.GetRequiredService())); // ---- WinUI-side services (replace the terminal rendering layer) ---- // Update checks hit MandoCode.Desktop's own GitHub releases, not the CLI's NuGet // package — the CLI's UpdateCheckService is deliberately NOT registered. services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); // stateless formatter + + // App-wide store of context snapshots taken when a tab switches its model, so any tab can + // re-import a conversation captured by another. Session-scoped, not persisted. + services.AddSingleton(); + + // ---- Coordinators + session registry ---- + services.AddSingleton(provider => new ConfigCoordinator(provider.GetRequiredService())); + services.AddSingleton(provider => new McpCoordinator(provider.GetRequiredService())); + services.AddSingleton(provider => new SessionManager( + provider, + provider.GetRequiredService(), + provider.GetRequiredService(), + projectRoot)); return services.BuildServiceProvider(); } diff --git a/src/MandoCode.Desktop/Controls/ChatTabView.xaml b/src/MandoCode.Desktop/Controls/ChatTabView.xaml new file mode 100644 index 0000000..6026c5b --- /dev/null +++ b/src/MandoCode.Desktop/Controls/ChatTabView.xaml @@ -0,0 +1,256 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - - - - - - - - - - + @@ -429,11 +399,23 @@ - - + + + + + + + + + + + + - - - - + - + -