Skip to content

Commit cfd36a7

Browse files
authored
fix: pair browser screenshots with their exact tool action (#198)
## Summary - fixes the activity model grouping every consecutive tool under one generic `data-tool` type - groups evidence-free runs by their actual tool name - keeps every evidence-bearing tool call standalone so its screenshot expands beneath the exact action that produced it ## Root Cause `collapseToolRuns` compared `part.type`, which is `data-tool` for every tool call. As a result, a browser open followed by a screenshot rendered as one `Opened ... (+1 more)` action even though the screenshot evidence belongs only to the screenshot call. ## Decision Evidence is keyed by `toolCallId` and an evidence-bearing call is never collapsed with another action. Repeated actions without evidence can still collapse to keep the activity timeline compact. ## Verification - `pnpm lint` - `pnpm typecheck` - `pnpm turbo build --force` - `pnpm deadcode` - `pnpm architecture:check` - `pnpm turbo skills:build` - focused runtime assertion covering browser open, multiple screenshots, exact evidence pairing, and repeated evidence-free actions ## Production QA After merge and deployment, re-run the existing authenticated screenshot flow and verify each screenshot appears only inside its own expanded `Captured a screenshot` action, never as a Deliverable.
1 parent f185a94 commit cfd36a7

1 file changed

Lines changed: 29 additions & 8 deletions

File tree

apps/web/src/components/chat/message-activity-model.ts

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -108,28 +108,49 @@ export function buildActivityRows(parts: MessagePart[]): ActivityItem[] {
108108
export function collapseToolRuns(
109109
parts: ToolActivityPart[],
110110
): Array<{ evidence: ToolEvidencePart[]; key: string; parts: ToolPart[] }> {
111-
const evidence = parts.filter(isToolEvidencePart);
111+
const evidenceByToolCallId = groupEvidenceByToolCallId(parts.filter(isToolEvidencePart));
112112
const tools = parts.filter(isToolPart);
113113
const rows: Array<{ evidence: ToolEvidencePart[]; key: string; parts: ToolPart[] }> = [];
114114
let index = 0;
115115
while (index < tools.length) {
116-
const type = tools[index]?.type;
116+
const tool = tools[index];
117+
if (!tool) break;
118+
const evidence = evidenceByToolCallId.get(tool.data.toolCallId) ?? [];
119+
if (evidence.length > 0) {
120+
rows.push({ evidence, key: `${tool.data.toolName}:${index}`, parts: [tool] });
121+
index += 1;
122+
continue;
123+
}
117124
let end = index + 1;
118-
while (end < tools.length && tools[end]?.type === type) {
125+
while (
126+
tools[end]?.data.toolName === tool.data.toolName &&
127+
!evidenceByToolCallId.has(tools[end]?.data.toolCallId ?? "")
128+
) {
119129
end += 1;
120130
}
121-
const toolParts = tools.slice(index, end);
122-
const toolCallIds = new Set(toolParts.map((part) => part.data.toolCallId));
123131
rows.push({
124-
evidence: evidence.filter((part) => toolCallIds.has(part.data.toolCallId)),
125-
key: `${type}:${index}`,
126-
parts: toolParts,
132+
evidence: [],
133+
key: `${tool.data.toolName}:${index}`,
134+
parts: tools.slice(index, end),
127135
});
128136
index = end;
129137
}
130138
return rows;
131139
}
132140

141+
function groupEvidenceByToolCallId(evidence: ToolEvidencePart[]): Map<string, ToolEvidencePart[]> {
142+
const grouped = new Map<string, ToolEvidencePart[]>();
143+
for (const part of evidence) {
144+
const existing = grouped.get(part.data.toolCallId);
145+
if (existing) {
146+
existing.push(part);
147+
} else {
148+
grouped.set(part.data.toolCallId, [part]);
149+
}
150+
}
151+
return grouped;
152+
}
153+
133154
export function buildToolDetailSections(parts: ToolPart[]): ToolDetailSection[] {
134155
const occurrences = new Map<string, number>();
135156
return parts.flatMap((part) => {

0 commit comments

Comments
 (0)