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
128 changes: 128 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
97 changes: 89 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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** |
Expand All @@ -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
Expand All @@ -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
Expand Down
Loading
Loading