feat(web): collapse resolved approval rows
ci / web (push) Failing after 25s
ci / flake (push) Successful in 3m36s
ci / rust (push) Successful in 6m12s

Resolved permission boxes vanish from the transcript instead of
lingering as "answered" cards. When a turn has 2+ tools the resolved
approval is absorbed into the tool group as a compact italic line;
otherwise it is hidden entirely.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Miguel Palhas
2026-09-07 21:20:31 +01:00
co-authored by Claude Opus 4.6
parent bc0fd6ee60
commit 60e57fde31
5 changed files with 141 additions and 57 deletions
+24 -16
View File
@@ -402,13 +402,15 @@ export function createStructuredConnection(
return el;
}
/** Group tool rows (and intermediate agent text between them) by turn.
* An agent_text/agent_thought row is "intermediate" when a tool row follows
* it in the same turn — it gets absorbed into the group so the collapsed
* block preserves chronological order. The *last* agent text with no
* subsequent tool stays standalone (the prominent final answer).
* Returns groups keyed by `turn:<userId>`, each with `toolCount` (for the
* header label) and `rows` (tools + intermediate text, in order). */
/** Group tool rows (and intermediate agent text / resolved approvals between
* them) by turn. An agent_text/agent_thought row is "intermediate" when a
* tool row follows it in the same turn — it gets absorbed into the group so
* the collapsed block preserves chronological order. Resolved approval rows
* (not the currently pending one) are absorbed the same way. The *last*
* agent text with no subsequent tool stays standalone (the prominent final
* answer). Returns groups keyed by `turn:<userId>`, each with `toolCount`
* (for the header label) and `rows` (tools + intermediate text/approvals,
* in order). */
function activityGroupsByTurn(): Map<string, { toolCount: number; rows: TranscriptRow[] }> {
// First: count tools per turn to know which turns qualify for grouping.
const toolCounts = new Map<string, number>();
@@ -422,33 +424,35 @@ export function createStructuredConnection(
// Second: build groups with interleaved text for turns with 2+ tools.
const groups = new Map<string, { toolCount: number; rows: TranscriptRow[] }>();
turnId = "before-first-message";
// Pending text rows that might be intermediate (not yet confirmed by a
// Pending rows that might be intermediate (not yet confirmed by a
// following tool). If no tool follows, they stay standalone.
let pendingText: TranscriptRow[] = [];
let pending: TranscriptRow[] = [];
let currentGid = `turn:${turnId}`;
for (const row of rows) {
if (row.kind === "user") {
turnId = row.id;
currentGid = `turn:${turnId}`;
pendingText = [];
pending = [];
continue;
}
const count = toolCounts.get(currentGid) ?? 0;
if (count < 2) continue;
if (row.kind === "agent_text" || row.kind === "agent_thought") {
pendingText.push(row);
const isIntermediateText = row.kind === "agent_text" || row.kind === "agent_thought";
const isResolvedApproval = row.kind === "approval" && row.id !== pendingApprovalRowId;
if (isIntermediateText || isResolvedApproval) {
pending.push(row);
continue;
}
if (row.kind === "tool_call" || row.kind === "tool_diff") {
const group = groups.get(currentGid) ?? { toolCount: count, rows: [] };
// Flush any pending text — a tool follows, so it's intermediate.
group.rows.push(...pendingText);
pendingText = [];
// Flush any pending rows — a tool follows, so they're intermediate.
group.rows.push(...pending);
pending = [];
group.rows.push(row);
groups.set(currentGid, group);
}
}
// pendingText at end of a turn = final text, stays outside (not flushed).
// pending at end of a turn = final text/approvals, stays outside (not flushed).
return groups;
}
@@ -500,6 +504,10 @@ export function createStructuredConnection(
children.push(groupEl);
continue;
}
// Resolved approvals not absorbed into a tool group are hidden — the
// tool row itself already identifies the action, so a standalone
// "answered" card adds nothing. Pending approvals always render.
if (row.kind === "approval" && row.id !== pendingApprovalRowId) continue;
const rowEl = buildRowElement(row);
if (!rowEl) continue;
rowEls.set(row.id, rowEl);