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
45 changes: 43 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,48 @@ are visible until you actually open a second tab.
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.
- **History panel — reopen a closed conversation.** A new rail icon (with a count badge) opens a
docked panel, sharing the Snapshots column, that lists every conversation you've closed — title,
project, model, when, turn count, and the first thing you said. **Open** brings one back as a
fresh tab through the existing restore cascade: the transcript replays and, when the model can
take it, the full memory rehydrates. **Delete** forgets one for good. Search filters by title,
project, model, or that first message. The archive is app-wide, persisted, and capped at the
newest 60 — evicting an old row deletes its journals so the on-disk stores stay bounded.
- **Snapshots panel — grouping, search, and a cleaner import.** Snapshot cards now group by the
project they were taken in (freshest project first), a search box filters by title/recap/model/
project, and Import closes the panel and focuses the chat so the "context armed" confirmation is
the thing you see.
- **Collapsible project groups, in both panels.** Each project group in Snapshots and History is an
`Expander` you can fold — the answer to "10–100 projects." Which groups you've collapsed is
remembered across launches (`PanelState` → `panel-state.json`).
- **Compare view — two agents side by side.** A **Split** button pairs two agents into a resizable
side-by-side view. The pair is an explicit, remembered choice (set by the button or the compare
bar's pickers, never by clicking a tab): clicking a paired agent's tab shows the split, clicking
any other agent shows it normally while the pair waits. The panes are ordinary agent views moved
between grid columns via `Grid.SetColumn` — never re-parented — so both WebViews and their live
transcripts survive the switch.
- **AI-named snapshots.** Saving a snapshot without a name now asks the summarizer for a short,
descriptive title from the recap; uniqueness against existing titles is then guaranteed in code
(`SnapshotNaming`), so two snapshots can't share a name.
- **Unread badges.** The History and Snapshots rail badges are now unread counts — items newer than
the last time you opened that panel — and clear when you open it, rather than showing a running
total. The "last seen" marks persist across launches.
- **Branded app icon** across the exe, taskbar, and window title bar, plus a lightweight
unhandled-exception logger (`crash.log`) to speed up diagnosing native/COM failures.

### Changed
- **Closing the last agent is allowed.** The app no longer forces at least one agent open — closing
the final one leaves an empty state (with the chat background) and a one-click New agent. Settings,
MCP, and snapshot Import disable while no agent is open and re-enable when one exists.
- **"Take snapshot" goes straight to the picker.** The manual capture (tab `⋯` menu) skips the
"snapshot available?" notification bar and opens the name + summarizer-model picker directly — a
model switch keeps the bar, since snapshotting isn't a foregone conclusion there.
- **Closing a tab archives it; `/clear` still forgets.** Closing used to delete a conversation's
journals outright ("closed tab = conversation gone"). Now it files the conversation into the
History archive instead, so it can be reopened later; only `/clear` (and eviction past the
archive cap) deletes the files. A session that never had a real turn is still dropped on close —
there's nothing to reopen. "Cleared means cleared" is unchanged; only *closing* softens from
"gone" to "recoverable."
- **`/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.
Expand Down Expand Up @@ -131,8 +171,9 @@ are visible until you actually open a second tab.
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).
- **Summarize-at-restore.** The tail-brief restore fallback still excerpts the stored dialogue
verbatim rather than running `HistorySummarizer` over it — better coverage of long sessions is a
follow-up, at the cost of one LLM call on restore.

## [0.1.0] — 2026-07-07

Expand Down
82 changes: 61 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,26 @@ graph; `SessionManager` owns the set of them. The split matters:
| `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` |
| `WinUiApprovalService`, `ApprovalPromptGate`, `McpApprovalGate` | `ConfigCoordinator`, `McpCoordinator`, `SessionManager`, `SnapshotStore`, `SessionArchiveStore` |

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.
carries Rename, Take snapshot, Export transcript, and Close. The model in each header opens a
quick-switch dropdown (cloud first, `cloud`/`local` badges) rather than a full-screen picker.

Closing the **last** agent is allowed: it leaves a clean empty state (showing the chat background)
with a one-click way to start a new agent. Actions that need an agent to act on — the Settings and
MCP pages, and snapshot Import — disable while none is open, then re-enable when you open one.

### Compare view (two agents side by side)

The **Split** button pairs two agents into a resizable side-by-side view for comparing what each is
producing. The pair is an explicit, remembered choice — set only by the Split button and the
compare-bar pickers, never by clicking a tab. Clicking a paired agent's tab shows the split;
clicking any other agent shows it normally while the pair waits. The two panes are ordinary agent
views moved between grid columns with `Grid.SetColumn` — **never re-parented**, so both WebViews (and
their live transcripts) survive the switch, which is the whole reason the tab surface is built the
way it is (see below).

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
Expand Down Expand Up @@ -117,18 +130,33 @@ 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.
A snapshot is a portable, AI-written recap of a conversation — save the gist of one agent's
context and carry it into another model or a fresh agent. Snapshots are offered when switching a
model would clear the conversation, and on demand via `Take snapshot` (tab `⋯` menu), which goes
straight to the save step. The recap is generated by `SnapshotEnhancer` (a bare, tool-less Ollama
kernel, map-reduce over the full history so nothing is truncated) using a summarizer model you
pick; a snapshot is therefore always born with a real recap — there is no "light"/un-enhanced state.
Leave the name blank and the summarizer proposes a short title, which is then made unique against
existing titles in code (`SnapshotNaming`) — an LLM can't be trusted to guarantee that itself.

The **Snapshots** rail icon opens a global panel (the `SnapshotStore` is app-wide, one list for
every tab, **persisted** to `snapshots.json`). Cards **group by project** and are **searchable**,
and each project group is a collapsible `Expander` whose fold state is remembered
(`PanelState` → `panel-state.json`). **Import** arms a snapshot's recap to ride along, invisibly,
with the active agent's next message — carrying context into any model. The rail badge is an
**unread count** (snapshots captured since you last opened the panel), not a running total, and
clears when you open it.

### Session history (reopen closed conversations)

Closing an agent no longer discards its conversation — it **archives** it. `SessionArchiveStore`
keeps an app-wide index (`sessions.json`) of closed conversations; the transcript, model memory, and
conversation-log journals stay on disk (see [docs/session-persistence.md](docs/session-persistence.md)).
The **History** rail panel lists them (grouped by project, searchable, collapsible), and **Open**
reopens one as a fresh tab on its original persist-key so the normal restore cascade replays the
transcript and — when the model supports it — rehydrates the full memory. `/clear` still forgets a
conversation for good; only *closing* softened from "gone" to "recoverable." The archive is capped
(newest 60); evicting a row deletes its journals so the on-disk stores stay bounded.

### Why the tab strip isn't a `TabView`

Expand Down Expand Up @@ -158,11 +186,22 @@ within 24 hours.
- 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
Rename, Take snapshot, Export transcript, Close. The header model opens a
quick-switch dropdown (cloud first, `cloud`/`local` badges). Closing the last
agent is allowed and leaves an empty state that shows the chat background
- Compare view — the **Split** button shows two agents side by side in a resizable
split for comparing their output; the compared pair is a remembered, explicit
choice, so clicking other tabs navigates without disturbing it
- Session history — closing an agent archives its conversation instead of deleting
it; the **History** panel reopens any past conversation as a new tab (with its
transcript, and full memory when the model supports it), grouped by project and
searchable. `/clear` still forgets for good
- Context snapshots — save an AI-written recap of a conversation (summarized by a
model you pick) and Import it into another model or a fresh agent; a global
left-rail panel lists them, **persisted**, grouped by project, searchable, with
collapsible groups. Unnamed snapshots get an auto-generated, unique title
- Rail badges on History and Snapshots are unread counts that clear when you open
the panel (persisted), not running totals
- 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 Appearance/Connection/Generation/Behavior/Limits/Integrations); every
Expand All @@ -176,6 +215,7 @@ within 24 hours.
progress, model picker, cloud-auth check + sign-in walkthrough
- `/model`, `/force-skill`, `/music-playlist` — pickers
- 401 auto-recovery — a cloud 401 offers the `ollama signin` walkthrough inline
- Branded application icon across the exe, taskbar, and window title bar
- Update check against this repo's GitHub Releases (24h throttle, fail-silent)

Not ported (yet): matrix easter eggs, terminal theme service (N/A).
Expand Down
45 changes: 36 additions & 9 deletions docs/session-persistence.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ Each tier ships independently and degrades gracefully into the one below it.
|------|---------------|-------|-----------|
| 1 | Workspace shape: tabs, titles, folders, models, active tab | `workspace.json` | Saved on every structural change + close; restored at launch |
| 1 | Snapshots | `snapshots.json` | Rewritten on add/remove; loaded at construction |
| 1 | Closed-conversation index (History) | `sessions.json` | Rewritten on close/reopen/delete; points at the retained per-key journals below |
| 1 | Panel UI prefs: collapsed groups + per-panel "last seen" unread marks | `panel-state.json` | Rewritten on fold/unfold and when a panel is opened |
| 2 | The visible transcript | `transcripts/<key>.jsonl` | Append-on-write journal of every HTML block; replayed into the WebView on restore |
| 3 | The model's memory | `histories/<key>.json` | `AIService.ExportHistoryJson()` at every turn end (write-then-rename); `TryRestoreHistoryJson()` on restore |
| 3 fallback | A plain-text tail of the dialogue | `conversations/<key>.jsonl` | Armed as imported background on the next send when full fidelity can't apply |
Expand Down Expand Up @@ -63,18 +65,43 @@ both sides of the concept line:
was cleared, there is nothing to carry). If the verbatim import fails, the offer stays up and
the snapshot path remains as salvage.

## Where snapshots are left off (future building)
## The History archive — reopening closed conversations

Snapshots persist across launches now, they record their project root, and IDs survive — but
the panel hasn't caught up: **no grouping by project, no search, and the import UX is
unchanged.** Those are polish items waiting for the snapshot library to grow now that it's
durable. Nothing broken, just room.
The per-key journals turned out to support more than restoring the tabs open at close: they back a
**History panel** that reopens *any* conversation you've closed. This required one deliberate change
to the retention model.

Other known headroom, in rough order of value:
Closing a tab used to delete its journals outright — "closed tab = conversation gone." That made
the memory/knowledge split lopsided: the only way context survived was to still be open at launch.
Now closing **archives** instead:

- `SessionArchiveStore` keeps an app-wide index (`sessions.json`) of closed conversations — the
cheap metadata (title, project, model, closed-at, turn count, first message), not the heavy
parts. The transcript/log/history journals it points at are the same per-key stores a live tab
uses; they simply aren't deleted on close anymore.
- **Reopen** recreates a tab on the archived persist-key and lets the normal restore cascade run —
so a reopened conversation replays its transcript and, when the model can take it, rehydrates its
full memory. The row leaves the archive (it's live again) and re-files itself on the next close.
- The archive is capped at the newest 60; evicting a row deletes its journals, so the on-disk
stores stay bounded even for someone who never runs `/clear`.
- The startup orphan sweep now keeps *archived* keys alongside *open* ones — only genuinely
orphaned journals (crash leftovers, pruned folders) are swept.

The design rule held: `/clear` still forgets (deletes the files, never archives). Only the meaning
of *closing* softened from "gone" to "recoverable." A session that never had a real turn is dropped
on close regardless — there's nothing worth reopening.

Both panels grew the same shape at the same time: cards **group by project**, a **search** box
filters them, and each project group is a **collapsible** `Expander` whose fold state persists
(`panel-state.json`). Their rail badges became **unread counts** — items newer than the last time
you opened that panel — which clear on open and whose "last seen" marks also persist. Snapshots
gained **AI-generated titles** (unique-checked in code) when saved unnamed, and Import now gets out
of the way so the chat's "context armed" confirmation is what you see.

## Future building

In rough order of value:

- **Session history browser** — the per-key journals already on disk would support a
"reopen any past conversation as a new tab" picker (Claude Code's `/resume` equivalent),
not just restoring the tabs that were open at close.
- **Summarize-at-restore upgrade** — the tail-brief fallback could run `HistorySummarizer`
over the stored dialogue instead of excerpting it, trading an LLM call for better coverage
of long sessions.
Expand Down
10 changes: 10 additions & 0 deletions src/MandoCode.Desktop.Tests/MandoCode.Desktop.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@
<Compile Include="..\MandoCode.Desktop\Services\GitQuickStatus.cs" Link="src\GitQuickStatus.cs" />
<Compile Include="..\MandoCode.Desktop\Services\WorkspaceDeltaTracker.cs" Link="src\WorkspaceDeltaTracker.cs" />
<Compile Include="..\..\MandoCode\src\MandoCode\Models\DiffModels.cs" Link="src\DiffModels.cs" />

<!-- Session-archive/snapshot display derivations are pure logic (path parsing, placeholders);
the store classes come along because the entry types live beside them, but no test
instantiates a store (they hit fixed LocalAppData paths). -->
<Compile Include="..\MandoCode.Desktop\Services\ContextSnapshot.cs" Link="src\ContextSnapshot.cs" />
<Compile Include="..\MandoCode.Desktop\Services\SnapshotNaming.cs" Link="src\SnapshotNaming.cs" />
<Compile Include="..\MandoCode.Desktop\Services\SessionArchiveStore.cs" Link="src\SessionArchiveStore.cs" />
<Compile Include="..\MandoCode.Desktop\Services\TranscriptJournal.cs" Link="src\TranscriptJournal.cs" />
<Compile Include="..\MandoCode.Desktop\Services\ConversationLog.cs" Link="src\ConversationLog.cs" />
<Compile Include="..\MandoCode.Desktop\Services\SessionHistoryStore.cs" Link="src\SessionHistoryStore.cs" />
</ItemGroup>

</Project>
Loading
Loading