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
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,27 @@ for every approved plan. Desktop's version follows the engine generation, so it
replacement is shown for review, and execution resumes only after approval.

### Changed
- **Completed turns now keep routine activity out of the conversation flow.** File operations,
tool calls, and routine connection progress remain visible while work is running,
then fold into an expandable Activity section when it completes. Assistant replies, warnings,
errors, pending approvals, plans, update notices, and session-restore notices remain visible.
Restored activity starts collapsed so old operational detail does not look like fresh work.
- **Long completed changes now have one Work completed rollup.** Related tool activity, diffs,
commands, and approval outcomes collapse together after a turn ends, with the number of files
changed, line totals, distinct approval states, and commands shown in the summary. Expanding it
preserves the original sequence and individual controls. Auto-approved deletions and MCP tool
requests stay part of that routine sequence; destructive warnings remain visible only while a
manual decision is needed.
- **Completed approval notices collapse into a compact state card.** Successful approvals and
auto-approval notices keep their full existing appearance while work is active, then summarize
each distinct state once (for example, `✅ ⚠️`) with the original cards available on expand.
Approval and tool-activity summaries share the same compact card and rotating chevron. Pending
prompts, denials, and approval errors remain visible.
- **Restored Desktop sessions no longer repeat the success message for conversation memory.** The
restored transcript is already visible, so full memory recovery is silent. Desktop calls out only
reduced-memory restores and unavailable conversation memory.
- **`@` directory references now render as a normal List operation.** Missing references use a
clear warning instead of raw `[Directory]` or `[Not found]` parser-style labels.
- **Automatic plans now start for the work that actually benefits from them.** Desktop recognizes
explicit checklists, cross-cutting changes, and multiple deliverables instead of treating a long
message as complex. Questions, research, explanations, and narrow edits stay conversational, and
Expand Down
26 changes: 26 additions & 0 deletions src/MandoCode.Desktop/Assets/web/transcript/transcript.css
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,32 @@
details.op-group summary { color: var(--dim); font-size: 12px; cursor: pointer; user-select: none; }
details.op-group summary:hover { color: var(--fg); }
details.op-group > .op { margin-left: 16px; }
details.activity-group, details.approval-activity-group, details.work-group { margin: 4px 0; }
details.activity-group:not([open]), details.approval-activity-group:not([open]),
details.work-group:not([open]) { width: fit-content; }
details.activity-group > summary, details.approval-activity-group > summary, details.work-group > summary {
display: flex; align-items: center; gap: 6px; width: fit-content; list-style: none;
padding: 4px 8px; color: var(--fg); background: var(--panel); border: 1px solid var(--border);
border-radius: 7px; cursor: pointer; user-select: none; font-size: 12px; line-height: 18px; }
details.activity-group > summary::-webkit-details-marker,
details.approval-activity-group > summary::-webkit-details-marker,
details.work-group > summary::-webkit-details-marker { display: none; }
details.activity-group > summary::before, details.approval-activity-group > summary::before,
details.work-group > summary::before {
content: '›'; color: var(--dim); font-size: 17px; line-height: 14px;
transform-origin: center; transition: transform 120ms ease; }
details.activity-group[open] > summary::before,
details.approval-activity-group[open] > summary::before,
details.work-group[open] > summary::before { transform: rotate(90deg); }
details.activity-group > summary:hover, details.approval-activity-group > summary:hover,
details.work-group > summary:hover {
border-color: var(--accent); }
details.activity-group[open] > summary, details.approval-activity-group[open] > summary {
margin-bottom: 4px; }
details.work-group[open] > summary { margin-bottom: 6px; }
details.work-group > summary { font-weight: 600; border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); }
details.approval-activity-group > summary {
font-size: 14px; font-family: "Segoe UI Emoji", "Segoe UI", sans-serif; }

/* Jump-to-bottom pill — shows when scrolled away from the live end of the chat. */
#jump-pill { position: fixed; bottom: 14px; left: 50%; transform: translateX(-50%);
Expand Down
134 changes: 134 additions & 0 deletions src/MandoCode.Desktop/Assets/web/transcript/transcript.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,135 @@
const n = d.querySelectorAll(':scope > .op').length;
d.querySelector('summary').textContent = '⚙ ' + n + ' operation' + (n === 1 ? '' : 's');
}
// Activity renders through the original append path while work is running. Only the explicit
// completion signal wraps finished activity, so live operation cards look and behave exactly as
// they did before completed-turn collapsing existed.
function activitySummary(d) {
const ops = d.querySelectorAll('.op').length;
const tools = d.querySelectorAll('.tool-pill').length;
const status = d.querySelectorAll('.notice-card.activity-item').length;
const parts = [];
if (ops) parts.push(ops + ' operation' + (ops === 1 ? '' : 's'));
if (tools) parts.push(tools + ' tool call' + (tools === 1 ? '' : 's'));
if (status) parts.push(status + ' update' + (status === 1 ? '' : 's'));
d.querySelector('summary').textContent = 'Activity' + (parts.length ? ' · ' + parts.join(' · ') : '');
}
function completeActivity() {
const items = Array.from(log.querySelectorAll('.activity-item:not([data-activity-completed])'));
const tops = [];
items.forEach(function (item) {
item.setAttribute('data-activity-completed', '1');
let top = item;
while (top.parentElement && top.parentElement !== log) top = top.parentElement;
if (top.parentElement === log && tops.indexOf(top) < 0) tops.push(top);
});

let run = [];
function flush() {
if (!run.length) return;
const d = document.createElement('details');
d.className = 'activity-group';
d.appendChild(document.createElement('summary'));
log.insertBefore(d, run[0]);
run.forEach(function (node) { d.appendChild(node); });
activitySummary(d);
run = [];
}

tops.forEach(function (top) {
if (run.length && run[run.length - 1].nextElementSibling !== top) flush();
run.push(top);
});
flush();
}
function approvalActivitySummary(d) {
const icons = [];
d.querySelectorAll('.approval-activity-item[data-activity-icon]').forEach(function (item) {
const icon = item.getAttribute('data-activity-icon');
if (icon && icons.indexOf(icon) < 0) icons.push(icon);
});
const summary = d.querySelector('summary');
summary.textContent = icons.join(' ');
summary.title = 'Approval activity';
summary.setAttribute('aria-label', 'Approval activity: ' + icons.join(' '));
}
function completeApprovalActivity() {
const items = Array.from(log.querySelectorAll(
'.approval-activity-item:not([data-approval-activity-completed])'));
const tops = [];
items.forEach(function (item) {
item.setAttribute('data-approval-activity-completed', '1');
let top = item;
while (top.parentElement && top.parentElement !== log) top = top.parentElement;
if (top.parentElement === log && tops.indexOf(top) < 0) tops.push(top);
});

let run = [];
function flush() {
if (!run.length) return;
const d = document.createElement('details');
d.className = 'approval-activity-group';
d.appendChild(document.createElement('summary'));
log.insertBefore(d, run[0]);
run.forEach(function (node) { d.appendChild(node); });
approvalActivitySummary(d);
run = [];
}

tops.forEach(function (top) {
if (run.length && run[run.length - 1].nextElementSibling !== top) flush();
run.push(top);
});
flush();
}
// Finished work commonly alternates between a tool/activity card, its resulting diff or
// command, and an approval result. Keep that complete sequence together once the turn is
// over, without changing the individual cards or how they stream while the work is active.
function isCompletedWork(node) {
return !node.hasAttribute('data-work-completed') &&
(node.matches('details.activity-group, details.approval-activity-group') ||
node.matches('.panel[data-work-kind]'));
}
function workSummary(d) {
const files = d.querySelectorAll('.panel[data-work-kind="diff"]').length;
const additions = d.querySelectorAll('.d-add').length;
const deletions = d.querySelectorAll('.d-rem').length;
const commands = d.querySelectorAll('.panel[data-work-kind="command"]').length;
const icons = [];
d.querySelectorAll('.approval-activity-item[data-activity-icon]').forEach(function (item) {
const icon = item.getAttribute('data-activity-icon');
if (icon && icons.indexOf(icon) < 0) icons.push(icon);
});

const parts = [];
if (files) parts.push(files + ' file' + (files === 1 ? '' : 's') + ' changed');
if (additions || deletions) parts.push('+' + additions + ' / −' + deletions);
if (icons.length) parts.push(icons.join(' '));
if (commands) parts.push(commands + ' command' + (commands === 1 ? '' : 's'));
d.querySelector('summary').textContent = 'Work completed' +
(parts.length ? ' · ' + parts.join(' · ') : '');
}
function completeWorkRollups() {
let run = [];
function flush() {
if (run.length < 2) return run = [];
const d = document.createElement('details');
d.className = 'work-group';
d.appendChild(document.createElement('summary'));
log.insertBefore(d, run[0]);
run.forEach(function (node) {
node.setAttribute('data-work-completed', '1');
d.appendChild(node);
});
workSummary(d);
run = [];
}
Array.from(log.children).forEach(function (node) {
if (isCompletedWork(node)) run.push(node);
else flush();
});
flush();
}
function placeChild(c) {
if (c.nodeType !== 1) { log.appendChild(c); return; }
if (c.classList.contains('op')) {
Expand Down Expand Up @@ -387,6 +516,11 @@
if (nearBottom) window.scrollTo(0, document.body.scrollHeight);
updatePill();
};
window.__completeActivity = function () {
completeActivity();
completeApprovalActivity();
completeWorkRollups();
};
window.__clear = function () { log.innerHTML = ''; updatePill(); };

document.addEventListener('click', function (e) {
Expand Down
43 changes: 37 additions & 6 deletions src/MandoCode.Desktop/Controls/ChatTabView.Transcript.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public sealed partial class ChatTabView
// ============================================================

private bool _journalRestored;
private bool _pendingActivityCompletion;

/// <summary>Replays this session's journaled transcript into the fresh WebView — via
/// ExecuteScript directly, NOT through TranscriptWriter (that would re-journal every
Expand All @@ -47,6 +48,7 @@ private async Task RestoreJournaledTranscriptAsync()
}
}
if (chunk.Length > 0) await AppendRawAsync(chunk.ToString());
await CompleteTranscriptActivityAsync(); // Restored work is history, so it starts collapsed.

// Divider goes through AppendRawAsync too — journaling it would stack one
// divider per relaunch. Memory restore happens LATER (RestoreConversationMemoryAsync,
Expand Down Expand Up @@ -82,10 +84,8 @@ public async Task RestoreConversationMemoryAsync()
var restored = await Task.Run(() => Session.Ai.TryRestoreHistoryJson(historyJson));
if (restored > 0)
{
await AppendRawAsync(_html.StatusCard(
"Conversation memory restored",
$"The agent remembers this session ({restored} messages).",
"success"));
// The preceding “Previous session restored” card is enough on Desktop: the
// replay is already visible, and a second success card only repeats it.
return;
}
}
Expand Down Expand Up @@ -114,17 +114,25 @@ await AppendRawAsync(_html.StatusCard(
_controller.ArmRestoredConversation(
"From \"your previous session in this tab\" (verbatim excerpt, not a recap):\n" +
sb.ToString().TrimEnd());
await AppendRawAsync(_html.Dim(
"Context re-armed — the agent will be briefed on this conversation with your next message."));
await AppendRawAsync(_html.StatusCard(
"Conversation context will be re-briefed",
"The agent will receive recent context with your next message.",
"warning"));
return;
}

// 3) Transcript was replayed but no memory of any kind exists — say so to the model.
if (_replayedBlockCount > 0)
{
_controller.NoteWorkspaceEvent(
"This tab was restored from a previous session. The transcript the user sees above is a replay " +
"for their benefit; it is NOT in your context and you have no memory of it. If the user refers " +
"to earlier work, say so honestly and re-read files instead of guessing.");
await AppendRawAsync(_html.StatusCard(
"Conversation memory unavailable",
"The transcript was restored, but the agent cannot recall it.",
"warning"));
}
}
catch { /* memory restore is best-effort; a fresh conversation always works */ }
}
Expand Down Expand Up @@ -180,6 +188,29 @@ private async void AppendHtml(string html)
}
}

/// <summary>Collapses the current group of routine tool/status output without touching visible
/// messages that need attention. If a turn ends before WebView initialization, apply it after
/// the queued blocks have reached the document.</summary>
private void CompleteTranscriptActivity()
{
if (!CanScript)
{
_pendingActivityCompletion = true;
return;
}
_ = CompleteTranscriptActivityAsync();
}

private async Task CompleteTranscriptActivityAsync()
{
// Journal replay deliberately runs before _webViewReady is set, but it still has a live
// CoreWebView2 and must be able to close restored activity groups.
var core = _shutDown ? null : TranscriptView.CoreWebView2;
if (core == null) return;
try { await core.ExecuteScriptAsync("window.__completeActivity && window.__completeActivity()"); }
catch { /* transient during navigation/teardown */ }
}

private async void ClearTranscript()
{
var core = CanScript ? TranscriptView.CoreWebView2 : null;
Expand Down
8 changes: 8 additions & 0 deletions src/MandoCode.Desktop/Controls/ChatTabView.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ public ChatTabView(Window owner, AgentSession session, TranscriptHtmlBuilder htm
// drives them into a WebView2 that no longer has a CoreWebView2.
_transcript.BlockAdded += OnTranscriptBlock;
_transcript.Cleared += OnTranscriptCleared;
_transcript.ActivityCompleted += OnTranscriptActivityCompleted;
Session.Busy.Changed += OnBusyChanged;
Session.TitleChanged += OnAgentTitleChanged;

Expand All @@ -128,6 +129,7 @@ public ChatTabView(Window owner, AgentSession session, TranscriptHtmlBuilder htm
// Harness events arrive on background threads; each hop marshals to the UI thread.
private void OnTranscriptBlock(string html) => OnUi(() => AppendHtml(html));
private void OnTranscriptCleared() => OnUi(ClearTranscript);
private void OnTranscriptActivityCompleted() => OnUi(CompleteTranscriptActivity);
private void OnBusyChanged(bool busy, string? activity) => OnUi(() => UpdateBusy(busy, activity));
private void OnAgentTitleChanged(string _) => OnUi(UpdateHeader);
private void OnControllerStateChanged() => OnUi(UpdateHeader);
Expand Down Expand Up @@ -182,6 +184,11 @@ public async Task InitializeAsync()
await RestoreJournaledTranscriptAsync();
_webViewReady = true;
while (_pendingHtml.Count > 0) AppendHtml(_pendingHtml.Dequeue());
if (_pendingActivityCompletion)
{
_pendingActivityCompletion = false;
CompleteTranscriptActivity();
}
};

// The WebView hosts only the transcript document. Any link click opens in the
Expand Down Expand Up @@ -333,6 +340,7 @@ public void Shutdown()
// WebView2 whose CoreWebView2 is about to be null.
_transcript.BlockAdded -= OnTranscriptBlock;
_transcript.Cleared -= OnTranscriptCleared;
_transcript.ActivityCompleted -= OnTranscriptActivityCompleted;
Session.Busy.Changed -= OnBusyChanged;
Session.TitleChanged -= OnAgentTitleChanged;
_controller.StateChanged -= OnControllerStateChanged;
Expand Down
1 change: 1 addition & 0 deletions src/MandoCode.Desktop/MainWindow.Mcp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,7 @@ private async void McpEditorSave_Click(object sender, RoutedEventArgs e)

var originalName = _mcpEditOriginalName;
var (_, message) = await Task.Run(() => _controller.SaveMcpServerAsync(originalName, name, server));
_controller.CompleteTranscriptActivity();
if (!string.IsNullOrWhiteSpace(originalName)) _itemTags.RenameItem(TagScope.Mcps, originalName, name);
McpPageStatus.Text = message;
await RefreshMcpListAsync();
Expand Down
Loading
Loading