Skip to content

Commit d5a28c4

Browse files
authored
Merge pull request #47 from DevMando/fix/transcript-work-rollup
Collapse completed work into readable transcript rollups
2 parents b1e0911 + bbe1047 commit d5a28c4

10 files changed

Lines changed: 302 additions & 42 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,27 @@ for every approved plan. Desktop's version follows the engine generation, so it
3838
replacement is shown for review, and execution resumes only after approval.
3939

4040
### Changed
41+
- **Completed turns now keep routine activity out of the conversation flow.** File operations,
42+
tool calls, and routine connection progress remain visible while work is running,
43+
then fold into an expandable Activity section when it completes. Assistant replies, warnings,
44+
errors, pending approvals, plans, update notices, and session-restore notices remain visible.
45+
Restored activity starts collapsed so old operational detail does not look like fresh work.
46+
- **Long completed changes now have one Work completed rollup.** Related tool activity, diffs,
47+
commands, and approval outcomes collapse together after a turn ends, with the number of files
48+
changed, line totals, distinct approval states, and commands shown in the summary. Expanding it
49+
preserves the original sequence and individual controls. Auto-approved deletions and MCP tool
50+
requests stay part of that routine sequence; destructive warnings remain visible only while a
51+
manual decision is needed.
52+
- **Completed approval notices collapse into a compact state card.** Successful approvals and
53+
auto-approval notices keep their full existing appearance while work is active, then summarize
54+
each distinct state once (for example, `✅ ⚠️`) with the original cards available on expand.
55+
Approval and tool-activity summaries share the same compact card and rotating chevron. Pending
56+
prompts, denials, and approval errors remain visible.
57+
- **Restored Desktop sessions no longer repeat the success message for conversation memory.** The
58+
restored transcript is already visible, so full memory recovery is silent. Desktop calls out only
59+
reduced-memory restores and unavailable conversation memory.
60+
- **`@` directory references now render as a normal List operation.** Missing references use a
61+
clear warning instead of raw `[Directory]` or `[Not found]` parser-style labels.
4162
- **Automatic plans now start for the work that actually benefits from them.** Desktop recognizes
4263
explicit checklists, cross-cutting changes, and multiple deliverables instead of treating a long
4364
message as complex. Questions, research, explanations, and narrow edits stay conversational, and

‎src/MandoCode.Desktop/Assets/web/transcript/transcript.css‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,32 @@
515515
details.op-group summary { color: var(--dim); font-size: 12px; cursor: pointer; user-select: none; }
516516
details.op-group summary:hover { color: var(--fg); }
517517
details.op-group > .op { margin-left: 16px; }
518+
details.activity-group, details.approval-activity-group, details.work-group { margin: 4px 0; }
519+
details.activity-group:not([open]), details.approval-activity-group:not([open]),
520+
details.work-group:not([open]) { width: fit-content; }
521+
details.activity-group > summary, details.approval-activity-group > summary, details.work-group > summary {
522+
display: flex; align-items: center; gap: 6px; width: fit-content; list-style: none;
523+
padding: 4px 8px; color: var(--fg); background: var(--panel); border: 1px solid var(--border);
524+
border-radius: 7px; cursor: pointer; user-select: none; font-size: 12px; line-height: 18px; }
525+
details.activity-group > summary::-webkit-details-marker,
526+
details.approval-activity-group > summary::-webkit-details-marker,
527+
details.work-group > summary::-webkit-details-marker { display: none; }
528+
details.activity-group > summary::before, details.approval-activity-group > summary::before,
529+
details.work-group > summary::before {
530+
content: '›'; color: var(--dim); font-size: 17px; line-height: 14px;
531+
transform-origin: center; transition: transform 120ms ease; }
532+
details.activity-group[open] > summary::before,
533+
details.approval-activity-group[open] > summary::before,
534+
details.work-group[open] > summary::before { transform: rotate(90deg); }
535+
details.activity-group > summary:hover, details.approval-activity-group > summary:hover,
536+
details.work-group > summary:hover {
537+
border-color: var(--accent); }
538+
details.activity-group[open] > summary, details.approval-activity-group[open] > summary {
539+
margin-bottom: 4px; }
540+
details.work-group[open] > summary { margin-bottom: 6px; }
541+
details.work-group > summary { font-weight: 600; border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); }
542+
details.approval-activity-group > summary {
543+
font-size: 14px; font-family: "Segoe UI Emoji", "Segoe UI", sans-serif; }
518544

519545
/* Jump-to-bottom pill — shows when scrolled away from the live end of the chat. */
520546
#jump-pill { position: fixed; bottom: 14px; left: 50%; transform: translateX(-50%);

‎src/MandoCode.Desktop/Assets/web/transcript/transcript.js‎

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,135 @@
66
const n = d.querySelectorAll(':scope > .op').length;
77
d.querySelector('summary').textContent = '⚙ ' + n + ' operation' + (n === 1 ? '' : 's');
88
}
9+
// Activity renders through the original append path while work is running. Only the explicit
10+
// completion signal wraps finished activity, so live operation cards look and behave exactly as
11+
// they did before completed-turn collapsing existed.
12+
function activitySummary(d) {
13+
const ops = d.querySelectorAll('.op').length;
14+
const tools = d.querySelectorAll('.tool-pill').length;
15+
const status = d.querySelectorAll('.notice-card.activity-item').length;
16+
const parts = [];
17+
if (ops) parts.push(ops + ' operation' + (ops === 1 ? '' : 's'));
18+
if (tools) parts.push(tools + ' tool call' + (tools === 1 ? '' : 's'));
19+
if (status) parts.push(status + ' update' + (status === 1 ? '' : 's'));
20+
d.querySelector('summary').textContent = 'Activity' + (parts.length ? ' · ' + parts.join(' · ') : '');
21+
}
22+
function completeActivity() {
23+
const items = Array.from(log.querySelectorAll('.activity-item:not([data-activity-completed])'));
24+
const tops = [];
25+
items.forEach(function (item) {
26+
item.setAttribute('data-activity-completed', '1');
27+
let top = item;
28+
while (top.parentElement && top.parentElement !== log) top = top.parentElement;
29+
if (top.parentElement === log && tops.indexOf(top) < 0) tops.push(top);
30+
});
31+
32+
let run = [];
33+
function flush() {
34+
if (!run.length) return;
35+
const d = document.createElement('details');
36+
d.className = 'activity-group';
37+
d.appendChild(document.createElement('summary'));
38+
log.insertBefore(d, run[0]);
39+
run.forEach(function (node) { d.appendChild(node); });
40+
activitySummary(d);
41+
run = [];
42+
}
43+
44+
tops.forEach(function (top) {
45+
if (run.length && run[run.length - 1].nextElementSibling !== top) flush();
46+
run.push(top);
47+
});
48+
flush();
49+
}
50+
function approvalActivitySummary(d) {
51+
const icons = [];
52+
d.querySelectorAll('.approval-activity-item[data-activity-icon]').forEach(function (item) {
53+
const icon = item.getAttribute('data-activity-icon');
54+
if (icon && icons.indexOf(icon) < 0) icons.push(icon);
55+
});
56+
const summary = d.querySelector('summary');
57+
summary.textContent = icons.join(' ');
58+
summary.title = 'Approval activity';
59+
summary.setAttribute('aria-label', 'Approval activity: ' + icons.join(' '));
60+
}
61+
function completeApprovalActivity() {
62+
const items = Array.from(log.querySelectorAll(
63+
'.approval-activity-item:not([data-approval-activity-completed])'));
64+
const tops = [];
65+
items.forEach(function (item) {
66+
item.setAttribute('data-approval-activity-completed', '1');
67+
let top = item;
68+
while (top.parentElement && top.parentElement !== log) top = top.parentElement;
69+
if (top.parentElement === log && tops.indexOf(top) < 0) tops.push(top);
70+
});
71+
72+
let run = [];
73+
function flush() {
74+
if (!run.length) return;
75+
const d = document.createElement('details');
76+
d.className = 'approval-activity-group';
77+
d.appendChild(document.createElement('summary'));
78+
log.insertBefore(d, run[0]);
79+
run.forEach(function (node) { d.appendChild(node); });
80+
approvalActivitySummary(d);
81+
run = [];
82+
}
83+
84+
tops.forEach(function (top) {
85+
if (run.length && run[run.length - 1].nextElementSibling !== top) flush();
86+
run.push(top);
87+
});
88+
flush();
89+
}
90+
// Finished work commonly alternates between a tool/activity card, its resulting diff or
91+
// command, and an approval result. Keep that complete sequence together once the turn is
92+
// over, without changing the individual cards or how they stream while the work is active.
93+
function isCompletedWork(node) {
94+
return !node.hasAttribute('data-work-completed') &&
95+
(node.matches('details.activity-group, details.approval-activity-group') ||
96+
node.matches('.panel[data-work-kind]'));
97+
}
98+
function workSummary(d) {
99+
const files = d.querySelectorAll('.panel[data-work-kind="diff"]').length;
100+
const additions = d.querySelectorAll('.d-add').length;
101+
const deletions = d.querySelectorAll('.d-rem').length;
102+
const commands = d.querySelectorAll('.panel[data-work-kind="command"]').length;
103+
const icons = [];
104+
d.querySelectorAll('.approval-activity-item[data-activity-icon]').forEach(function (item) {
105+
const icon = item.getAttribute('data-activity-icon');
106+
if (icon && icons.indexOf(icon) < 0) icons.push(icon);
107+
});
108+
109+
const parts = [];
110+
if (files) parts.push(files + ' file' + (files === 1 ? '' : 's') + ' changed');
111+
if (additions || deletions) parts.push('+' + additions + ' / −' + deletions);
112+
if (icons.length) parts.push(icons.join(' '));
113+
if (commands) parts.push(commands + ' command' + (commands === 1 ? '' : 's'));
114+
d.querySelector('summary').textContent = 'Work completed' +
115+
(parts.length ? ' · ' + parts.join(' · ') : '');
116+
}
117+
function completeWorkRollups() {
118+
let run = [];
119+
function flush() {
120+
if (run.length < 2) return run = [];
121+
const d = document.createElement('details');
122+
d.className = 'work-group';
123+
d.appendChild(document.createElement('summary'));
124+
log.insertBefore(d, run[0]);
125+
run.forEach(function (node) {
126+
node.setAttribute('data-work-completed', '1');
127+
d.appendChild(node);
128+
});
129+
workSummary(d);
130+
run = [];
131+
}
132+
Array.from(log.children).forEach(function (node) {
133+
if (isCompletedWork(node)) run.push(node);
134+
else flush();
135+
});
136+
flush();
137+
}
9138
function placeChild(c) {
10139
if (c.nodeType !== 1) { log.appendChild(c); return; }
11140
if (c.classList.contains('op')) {
@@ -387,6 +516,11 @@
387516
if (nearBottom) window.scrollTo(0, document.body.scrollHeight);
388517
updatePill();
389518
};
519+
window.__completeActivity = function () {
520+
completeActivity();
521+
completeApprovalActivity();
522+
completeWorkRollups();
523+
};
390524
window.__clear = function () { log.innerHTML = ''; updatePill(); };
391525

392526
document.addEventListener('click', function (e) {

‎src/MandoCode.Desktop/Controls/ChatTabView.Transcript.cs‎

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public sealed partial class ChatTabView
2121
// ============================================================
2222

2323
private bool _journalRestored;
24+
private bool _pendingActivityCompletion;
2425

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

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

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

191+
/// <summary>Collapses the current group of routine tool/status output without touching visible
192+
/// messages that need attention. If a turn ends before WebView initialization, apply it after
193+
/// the queued blocks have reached the document.</summary>
194+
private void CompleteTranscriptActivity()
195+
{
196+
if (!CanScript)
197+
{
198+
_pendingActivityCompletion = true;
199+
return;
200+
}
201+
_ = CompleteTranscriptActivityAsync();
202+
}
203+
204+
private async Task CompleteTranscriptActivityAsync()
205+
{
206+
// Journal replay deliberately runs before _webViewReady is set, but it still has a live
207+
// CoreWebView2 and must be able to close restored activity groups.
208+
var core = _shutDown ? null : TranscriptView.CoreWebView2;
209+
if (core == null) return;
210+
try { await core.ExecuteScriptAsync("window.__completeActivity && window.__completeActivity()"); }
211+
catch { /* transient during navigation/teardown */ }
212+
}
213+
183214
private async void ClearTranscript()
184215
{
185216
var core = CanScript ? TranscriptView.CoreWebView2 : null;

‎src/MandoCode.Desktop/Controls/ChatTabView.xaml.cs‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ public ChatTabView(Window owner, AgentSession session, TranscriptHtmlBuilder htm
110110
// drives them into a WebView2 that no longer has a CoreWebView2.
111111
_transcript.BlockAdded += OnTranscriptBlock;
112112
_transcript.Cleared += OnTranscriptCleared;
113+
_transcript.ActivityCompleted += OnTranscriptActivityCompleted;
113114
Session.Busy.Changed += OnBusyChanged;
114115
Session.TitleChanged += OnAgentTitleChanged;
115116

@@ -128,6 +129,7 @@ public ChatTabView(Window owner, AgentSession session, TranscriptHtmlBuilder htm
128129
// Harness events arrive on background threads; each hop marshals to the UI thread.
129130
private void OnTranscriptBlock(string html) => OnUi(() => AppendHtml(html));
130131
private void OnTranscriptCleared() => OnUi(ClearTranscript);
132+
private void OnTranscriptActivityCompleted() => OnUi(CompleteTranscriptActivity);
131133
private void OnBusyChanged(bool busy, string? activity) => OnUi(() => UpdateBusy(busy, activity));
132134
private void OnAgentTitleChanged(string _) => OnUi(UpdateHeader);
133135
private void OnControllerStateChanged() => OnUi(UpdateHeader);
@@ -182,6 +184,11 @@ public async Task InitializeAsync()
182184
await RestoreJournaledTranscriptAsync();
183185
_webViewReady = true;
184186
while (_pendingHtml.Count > 0) AppendHtml(_pendingHtml.Dequeue());
187+
if (_pendingActivityCompletion)
188+
{
189+
_pendingActivityCompletion = false;
190+
CompleteTranscriptActivity();
191+
}
185192
};
186193

187194
// The WebView hosts only the transcript document. Any link click opens in the
@@ -333,6 +340,7 @@ public void Shutdown()
333340
// WebView2 whose CoreWebView2 is about to be null.
334341
_transcript.BlockAdded -= OnTranscriptBlock;
335342
_transcript.Cleared -= OnTranscriptCleared;
343+
_transcript.ActivityCompleted -= OnTranscriptActivityCompleted;
336344
Session.Busy.Changed -= OnBusyChanged;
337345
Session.TitleChanged -= OnAgentTitleChanged;
338346
_controller.StateChanged -= OnControllerStateChanged;

‎src/MandoCode.Desktop/MainWindow.Mcp.cs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,7 @@ private async void McpEditorSave_Click(object sender, RoutedEventArgs e)
457457

458458
var originalName = _mcpEditOriginalName;
459459
var (_, message) = await Task.Run(() => _controller.SaveMcpServerAsync(originalName, name, server));
460+
_controller.CompleteTranscriptActivity();
460461
if (!string.IsNullOrWhiteSpace(originalName)) _itemTags.RenameItem(TagScope.Mcps, originalName, name);
461462
McpPageStatus.Text = message;
462463
await RefreshMcpListAsync();

0 commit comments

Comments
 (0)