Skip to content

feat(site/src/pages/AgentsPage/components): collapse sequential read file events - #25075

Merged
DanielleMaywood merged 13 commits into
mainfrom
codagt-337-collapse-read-file-events
May 26, 2026
Merged

feat(site/src/pages/AgentsPage/components): collapse sequential read file events#25075
DanielleMaywood merged 13 commits into
mainfrom
codagt-337-collapse-read-file-events

Conversation

@DanielleMaywood

Copy link
Copy Markdown
Contributor

Note

🤖 This PR was written by Coder Agent on behalf of Danielle Maywood

Collapses sequential read_file tool activity in the Agents chat timeline into a single UI event while leaving the tool API and persisted transcript shape unchanged.

The grouping handles both multiple tool blocks within one assistant message and the real persisted shape where read_file calls are split across adjacent assistant and tool-result messages. Refs CODAGT-337.

Implementation plan

Collapse Sequential Read File Events Implementation Plan

For agentic workers: use subagent-driven-development for independent
tasks, or executing-plans for inline execution. Track steps with checkbox
syntax.

Goal: Collapse consecutive read_file tool calls into a single conversation timeline UI entry such as Read 3 files while keeping each tool call singular.

Architecture: This is a render-time frontend grouping change. BlockList will derive a grouped view from existing RenderBlock[] and MergedTool[], leaving message parsing, streaming state, persisted transcript shape, and the read_file tool protocol unchanged. A new grouped read-file component will reuse the same file viewer behavior as ReadFileTool so expanded groups still expose every file read.

Tech Stack: React, TypeScript, Vite, Vitest, Storybook interaction tests, Biome formatting, pnpm commands from site/.


Requirements

  • Consecutive rendered tool blocks whose resolved tool name is read_file collapse into one UI event.
  • Single read_file blocks continue to render as the existing single-file ReadFileTool.
  • Non-consecutive read_file blocks do not collapse across text, reasoning, source, file, or other tool blocks.
  • The read_file tool API stays singular. Do not change tool schemas or backend code.
  • Grouped UI shows a summary such as Read N files and can expand to show each file's content.
  • If any grouped read is running, the group uses a running label and spinner. If any grouped read errors, the group shows an error indicator.

File Map

  • Modify: site/src/pages/AgentsPage/components/ChatConversation/blockUtils.ts
    • Add pure render-block grouping helpers.
  • Modify: site/src/pages/AgentsPage/components/ChatConversation/blockUtils.test.ts
    • Add unit tests for grouping behavior.
  • Modify: site/src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.tsx
    • Use grouped blocks in BlockList, account for grouped IDs in remaining-tool filtering, and render grouped read files.
  • Create: site/src/pages/AgentsPage/components/ChatElements/tools/ReadFilesTool.tsx
    • Render grouped read_file calls using ToolCollapsible and file viewers.
  • Modify: site/src/pages/AgentsPage/components/ChatElements/tools/ReadFileTool.tsx
    • Export a reusable file viewer body if needed by ReadFilesTool.
  • Modify: site/src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.stories.tsx
    • Add a story and play assertions for collapsed sequential reads.

No database, generated API, backend, or docs changes are expected.

Implementation Details

Grouping model

Do not add a persisted RenderBlock union member. Keep grouping as a local render derivation so other code that consumes parsed blocks does not need to understand grouped reads.

Add these types and helpers to blockUtils.ts:

export type ToolGroupRenderBlock = {
	type: "tool-group";
	toolName: "read_file";
	ids: string[];
};

export type TimelineRenderBlock = RenderBlock | ToolGroupRenderBlock;

export const groupSequentialReadFileBlocks = (
	blocks: readonly RenderBlock[],
	tools: readonly MergedTool[],
): TimelineRenderBlock[] => {
	const toolByID = new Map(tools.map((tool) => [tool.id, tool]));
	const grouped: TimelineRenderBlock[] = [];
	let currentReadFileIDs: string[] = [];

	const flushReadFileIDs = () => {
		if (currentReadFileIDs.length === 0) {
			return;
		}
		if (currentReadFileIDs.length === 1) {
			grouped.push({ type: "tool", id: currentReadFileIDs[0] });
		} else {
			grouped.push({
				type: "tool-group",
				toolName: "read_file",
				ids: currentReadFileIDs,
			});
		}
		currentReadFileIDs = [];
	};

	for (const block of blocks) {
		if (block.type === "tool") {
			const tool = toolByID.get(block.id);
			if (tool?.name === "read_file") {
				currentReadFileIDs = [...currentReadFileIDs, block.id];
				continue;
			}
		}

		flushReadFileIDs();
		grouped.push(block);
	}

	flushReadFileIDs();
	return grouped;
};

export const getToolIDsForBlock = (
	block: TimelineRenderBlock,
): readonly string[] => {
	if (block.type === "tool") {
		return [block.id];
	}
	if (block.type === "tool-group") {
		return block.ids;
	}
	return [];
};

groupSequentialReadFileBlocks should:

  • Build toolByID = new Map(tools.map((tool) => [tool.id, tool])).
  • Iterate through blocks in order.
  • For each block.type === "tool", resolve the tool from toolByID.
  • Accumulate consecutive tool blocks only when the resolved tool exists and tool.name === "read_file".
  • When a non-read block or unresolved tool block appears, flush the current run.
  • Flush a run as:
    • one original tool block when the run length is 1,
    • one { type: "tool-group", toolName: "read_file", ids } block when the run length is greater than 1.
  • Preserve original block order and object identity for non-grouped blocks where practical.

Grouped component behavior

Create ReadFilesTool.tsx with props:

import type { MergedTool } from "../../ChatConversation/types";

export const ReadFilesTool: React.FC<{
	tools: readonly MergedTool[];
}> = ({ tools }) => {
	// Implementation described in Task 3.
};

Inside ReadFilesTool:

  • Derive each file item from its tool with parseArgs, asRecord, and asString, matching ReadFileRenderer in Tool.tsx.
  • Use path || "file" as the display path fallback.
  • Use asString(rec.content).trim() for content to match existing single-file behavior.
  • Compute status:
    • running if any tool has status === "running",
    • error if no running tools and any tool has isError,
    • completed otherwise.
  • Header label:
    • Reading ${tools.length} files… when running,
    • Read ${tools.length} files otherwise.
  • hasContent is true if any grouped item has non-empty content.
  • Use ToolCollapsible with className="w-full".
  • In expanded content, render a vertical list of file sections. Each section shows the path and a scrollable file viewer body.
  • Preserve per-file errors in the expanded list by showing the error message near that file if present.

To avoid duplicating file viewer setup, extract a small exported component from ReadFileTool.tsx:

export const ReadFileContent: React.FC<{
	path: string;
	content: string;
}> = ({ path, content }) => {
	// Existing ScrollArea plus FileViewer body.
};

Then ReadFileTool renders <ReadFileContent path={path} content={content} />, and ReadFilesTool reuses it for each file.

BlockList integration

In ConversationTimeline.tsx:

  • Import getToolIDsForBlock, groupSequentialReadFileBlocks, and ReadFilesTool.
  • Keep toolByID creation.
  • Compute const displayBlocks = groupSequentialReadFileBlocks(blocks, tools); after toolByID.
  • Build blockToolIDs from displayBlocks.flatMap(getToolIDsForBlock) and retain the existing streaming check behavior for unresolved single tool placeholders.
  • Render displayBlocks.map instead of blocks.map.
  • Add case "tool-group" before case "tool":
case "tool-group": {
	const groupTools = block.ids
		.map((id) => toolByID.get(id))
		.filter((tool): tool is MergedTool => Boolean(tool));
	if (groupTools.length === 0) {
		return null;
	}
	return <ReadFilesTool key={`${keyPrefix}-tool-group-${index}`} tools={groupTools} />;
}
  • Keep the existing single-tool render path unchanged.
  • For lastBlockIsThinking, continue using the original blocks input because grouping only affects tool blocks.

Tasks

Task 1: Add pure grouping helper tests

Files:

  • Test: site/src/pages/AgentsPage/components/ChatConversation/blockUtils.test.ts

  • Modify later: site/src/pages/AgentsPage/components/ChatConversation/blockUtils.ts

  • Step 1: Add failing tests for grouped read blocks.

Add MergedTool to the type imports and add a new suite after appendTextBlock:

describe("groupSequentialReadFileBlocks", () => {
	const tools: MergedTool[] = [
		{
			id: "read-1",
			name: "read_file",
			args: { path: "a.ts" },
			result: { content: "a" },
			isError: false,
			status: "completed",
		},
		{
			id: "read-2",
			name: "read_file",
			args: { path: "b.ts" },
			result: { content: "b" },
			isError: false,
			status: "completed",
		},
		{
			id: "execute-1",
			name: "execute",
			args: { command: "pwd" },
			result: { output: "/home/coder" },
			isError: false,
			status: "completed",
		},
	];

	it("collapses consecutive read_file tool blocks", () => {
		const result = groupSequentialReadFileBlocks(
			[
				{ type: "tool", id: "read-1" },
				{ type: "tool", id: "read-2" },
			],
			tools,
		);

		expect(result).toEqual([
			{ type: "tool-group", toolName: "read_file", ids: ["read-1", "read-2"] },
		]);
	});

	it("leaves a single read_file tool block ungrouped", () => {
		const result = groupSequentialReadFileBlocks(
			[{ type: "tool", id: "read-1" }],
			tools,
		);

		expect(result).toEqual([{ type: "tool", id: "read-1" }]);
	});

	it("does not collapse read_file blocks across other content", () => {
		const result = groupSequentialReadFileBlocks(
			[
				{ type: "tool", id: "read-1" },
				{ type: "response", text: "middle" },
				{ type: "tool", id: "read-2" },
			],
			tools,
		);

		expect(result).toEqual([
			{ type: "tool", id: "read-1" },
			{ type: "response", text: "middle" },
			{ type: "tool", id: "read-2" },
		]);
	});

	it("does not collapse read_file blocks across another tool", () => {
		const result = groupSequentialReadFileBlocks(
			[
				{ type: "tool", id: "read-1" },
				{ type: "tool", id: "execute-1" },
				{ type: "tool", id: "read-2" },
			],
			tools,
		);

		expect(result).toEqual([
			{ type: "tool", id: "read-1" },
			{ type: "tool", id: "execute-1" },
			{ type: "tool", id: "read-2" },
		]);
	});

	it("keeps unresolved tool blocks ungrouped", () => {
		const result = groupSequentialReadFileBlocks(
			[
				{ type: "tool", id: "read-1" },
				{ type: "tool", id: "missing" },
				{ type: "tool", id: "read-2" },
			],
			tools,
		);

		expect(result).toEqual([
			{ type: "tool", id: "read-1" },
			{ type: "tool", id: "missing" },
			{ type: "tool", id: "read-2" },
		]);
	});
});
  • Step 2: Run the focused test and verify it fails because the helper is missing.
cd site && pnpm test -- src/pages/AgentsPage/components/ChatConversation/blockUtils.test.ts -t groupSequentialReadFileBlocks

Expected: Vitest fails because groupSequentialReadFileBlocks is not exported from blockUtils.ts.

Task 2: Implement grouping helpers

Files:

  • Modify: site/src/pages/AgentsPage/components/ChatConversation/blockUtils.ts

  • Test: site/src/pages/AgentsPage/components/ChatConversation/blockUtils.test.ts

  • Step 1: Add the helper types and implementation.

Update imports:

import type { MergedTool, RenderBlock } from "./types";

Add the types and helpers after appendTextBlock:

export type ToolGroupRenderBlock = {
	type: "tool-group";
	toolName: "read_file";
	ids: string[];
};

export type TimelineRenderBlock = RenderBlock | ToolGroupRenderBlock;

export const groupSequentialReadFileBlocks = (
	blocks: readonly RenderBlock[],
	tools: readonly MergedTool[],
): TimelineRenderBlock[] => {
	const toolByID = new Map(tools.map((tool) => [tool.id, tool]));
	const grouped: TimelineRenderBlock[] = [];
	let currentReadFileIDs: string[] = [];

	const flushReadFileIDs = () => {
		if (currentReadFileIDs.length === 0) {
			return;
		}
		if (currentReadFileIDs.length === 1) {
			grouped.push({ type: "tool", id: currentReadFileIDs[0] });
		} else {
			grouped.push({
				type: "tool-group",
				toolName: "read_file",
				ids: currentReadFileIDs,
			});
		}
		currentReadFileIDs = [];
	};

	for (const block of blocks) {
		if (block.type === "tool") {
			const tool = toolByID.get(block.id);
			if (tool?.name === "read_file") {
				currentReadFileIDs = [...currentReadFileIDs, block.id];
				continue;
			}
		}

		flushReadFileIDs();
		grouped.push(block);
	}

	flushReadFileIDs();
	return grouped;
};

export const getToolIDsForBlock = (
	block: TimelineRenderBlock,
): readonly string[] => {
	if (block.type === "tool") {
		return [block.id];
	}
	if (block.type === "tool-group") {
		return block.ids;
	}
	return [];
};
  • Step 2: Import the helper in the test file.

Update the blockUtils.test.ts import to include groupSequentialReadFileBlocks and import MergedTool:

import {
	appendTextBlock,
	asNonEmptyString,
	groupSequentialReadFileBlocks,
} from "./blockUtils";
import type { MergedTool, RenderBlock } from "./types";
  • Step 3: Run the focused helper tests and verify they pass.
cd site && pnpm test -- src/pages/AgentsPage/components/ChatConversation/blockUtils.test.ts -t groupSequentialReadFileBlocks

Expected: the new grouping tests pass.

  • Step 4: Run all block utility tests.
cd site && pnpm test -- src/pages/AgentsPage/components/ChatConversation/blockUtils.test.ts

Expected: all blockUtils.test.ts tests pass.

Task 3: Add grouped read-file component with reusable file viewer body

Files:

  • Modify: site/src/pages/AgentsPage/components/ChatElements/tools/ReadFileTool.tsx

  • Create: site/src/pages/AgentsPage/components/ChatElements/tools/ReadFilesTool.tsx

  • Step 1: Extract ReadFileContent from ReadFileTool.tsx.

Add this exported component before ReadFileTool:

export const ReadFileContent: React.FC<{
	path: string;
	content: string;
}> = ({ path, content }) => {
	const theme = useTheme();
	const isDark = theme.palette.mode === "dark";

	return (
		<ScrollArea
			className="mt-1.5 rounded-md border border-solid border-border-default text-2xs"
			viewportClassName="max-h-64"
			scrollBarClassName="w-1.5"
		>
			<FileViewer
				file={{
					name: path,
					contents: content,
				}}
				options={getFileViewerOptionsMinimal(isDark)}
				style={DIFFS_FONT_STYLE}
			/>
		</ScrollArea>
	);
};

Then replace the existing inline ScrollArea in ReadFileTool with:

<ReadFileContent path={path} content={content} />
  • Step 2: Create ReadFilesTool.tsx.

Use this shape:

import { LoaderIcon, TriangleAlertIcon } from "lucide-react";
import type React from "react";
import {
	Tooltip,
	TooltipContent,
	TooltipTrigger,
} from "#/components/Tooltip/Tooltip";
import type { MergedTool } from "../../ChatConversation/types";
import { asRecord, asString } from "../runtimeTypeUtils";
import { ReadFileContent } from "./ReadFileTool";
import { ToolCollapsible } from "./ToolCollapsible";
import { parseArgs } from "./utils";

type ReadFileItem = {
	id: string;
	path: string;
	content: string;
	isError: boolean;
	errorMessage?: string;
};

const getReadFileItem = (tool: MergedTool): ReadFileItem => {
	const parsedArgs = parseArgs(tool.args);
	const path = parsedArgs ? asString(parsedArgs.path).trim() : "";
	const rec = asRecord(tool.result);
	return {
		id: tool.id,
		path: path || "file",
		content: rec ? asString(rec.content).trim() : "",
		isError: tool.isError,
		errorMessage: rec ? asString(rec.error || rec.message) : undefined,
	};
};

export const ReadFilesTool: React.FC<{
	tools: readonly MergedTool[];
}> = ({ tools }) => {
	const items = tools.map(getReadFileItem);
	const isRunning = tools.some((tool) => tool.status === "running");
	const isError = tools.some((tool) => tool.isError);
	const hasContent = items.some((item) => item.content.length > 0);
	const label = isRunning
		? `Reading ${tools.length} files…`
		: `Read ${tools.length} files`;
	const errorMessage = items.find((item) => item.errorMessage)?.errorMessage;

	return (
		<ToolCollapsible
			className="w-full"
			hasContent={hasContent}
			header={
				<>
					<span className="text-[13px]">{label}</span>
					{isError && (
						<Tooltip>
							<TooltipTrigger asChild>
								<TriangleAlertIcon className="h-3.5 w-3.5 shrink-0 text-current" />
							</TooltipTrigger>
							<TooltipContent>
								{errorMessage || "Failed to read one or more files"}
							</TooltipContent>
						</Tooltip>
					)}
					{isRunning && (
						<LoaderIcon className="h-3.5 w-3.5 shrink-0 animate-spin motion-reduce:animate-none text-current" />
					)}
				</>
			}
		>
			<div className="space-y-3">
				{items.map((item) => (
					<section key={item.id} className="min-w-0">
						<div className="mt-2 truncate text-xs text-content-secondary">
							{item.path}
						</div>
						{item.isError && (
							<div className="mt-1 text-xs text-content-destructive">
								{item.errorMessage || "Failed to read file"}
							</div>
						)}
						{item.content.length > 0 && (
							<ReadFileContent path={item.path} content={item.content} />
						)}
					</section>
				))}
			</div>
		</ToolCollapsible>
	);
};
  • Step 3: Run TypeScript check for immediate errors.
cd site && pnpm check

Expected: no TypeScript errors from the new component and extracted viewer body.

Task 4: Integrate grouped blocks into BlockList

Files:

  • Modify: site/src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.tsx

  • Step 1: Import the helper and grouped component.

Add ReadFilesTool near the existing tool imports:

import { ReadFilesTool } from "../ChatElements/tools/ReadFilesTool";

Update the block utility import:

import {
	getToolIDsForBlock,
	groupSequentialReadFileBlocks,
} from "./blockUtils";

If ConversationTimeline.tsx already imports from blockUtils, merge these names into that import instead of adding a duplicate.

  • Step 2: Derive displayBlocks and update remaining-tool filtering.

In BlockList, after toolByID:

const displayBlocks = groupSequentialReadFileBlocks(blocks, tools);

Replace the existing blockToolIDs computation with:

const blockToolIDs = new Set(
	displayBlocks.flatMap((block) => {
		if (
			block.type === "tool" &&
			!(toolByID.has(block.id) || isStreaming)
		) {
			return [];
		}
		return [...getToolIDsForBlock(block)];
	}),
);

This keeps unresolved streaming placeholders eligible for single-tool rendering while also excluding grouped tool IDs from remainingTools.

  • Step 3: Render displayBlocks and add the grouped case.

Change:

{blocks.map((block, index) => {

to:

{displayBlocks.map((block, index) => {

Add this switch case before case "tool":

case "tool-group": {
	const groupTools = block.ids
		.map((id) => toolByID.get(id))
		.filter((tool): tool is MergedTool => Boolean(tool));
	if (groupTools.length === 0) {
		return null;
	}
	return (
		<ReadFilesTool
			key={`${keyPrefix}-tool-group-${index}`}
			tools={groupTools}
		/>
	);
}
  • Step 4: Run focused tests and typecheck.
cd site && pnpm test -- src/pages/AgentsPage/components/ChatConversation/blockUtils.test.ts
cd site && pnpm check

Expected: tests pass and TypeScript accepts the new TimelineRenderBlock switch case.

Task 5: Add Storybook coverage for sequential reads

Files:

  • Modify: site/src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.stories.tsx

  • Step 1: Add a story with play assertions.

Add a story near the other tool/rendering regression stories:

export const SequentialReadFilesCollapsed: Story = {
	args: {
		...defaultArgs,
		parsedMessages: buildMessages([
			{
				...baseMessage,
				id: 1,
				role: "assistant",
				content: [
					{ type: "text", text: "I'll inspect the relevant files." },
					{
						type: "tool-call",
						tool_call_id: "read-1",
						tool_name: "read_file",
						args: { path: "site/src/a.ts" },
					},
					{
						type: "tool-result",
						tool_call_id: "read-1",
						tool_name: "read_file",
						result: { content: "export const a = 1;" },
					},
					{
						type: "tool-call",
						tool_call_id: "read-2",
						tool_name: "read_file",
						args: { path: "site/src/b.ts" },
					},
					{
						type: "tool-result",
						tool_call_id: "read-2",
						tool_name: "read_file",
						result: { content: "export const b = 2;" },
					},
					{
						type: "tool-call",
						tool_call_id: "read-3",
						tool_name: "read_file",
						args: { path: "site/src/c.ts" },
					},
					{
						type: "tool-result",
						tool_call_id: "read-3",
						tool_name: "read_file",
						result: { content: "export const c = 3;" },
					},
				],
			},
		]),
	},
	play: async ({ canvasElement }) => {
		const canvas = within(canvasElement);
		const groupButton = canvas.getByRole("button", { name: /read 3 files/i });
		expect(groupButton).toBeInTheDocument();
		expect(
			canvas.queryByRole("button", { name: /read a\.ts/i }),
		).not.toBeInTheDocument();
		await userEvent.click(groupButton);
		await waitFor(() => {
			expect(canvas.getByText("site/src/a.ts")).toBeVisible();
			expect(canvas.getByText("site/src/b.ts")).toBeVisible();
			expect(canvas.getByText("site/src/c.ts")).toBeVisible();
		});
	},
};

The exact text queries for code contents may need adjustment because @pierre/diffs/react may split code into decorated spans. Prefer asserting visible file paths and the group button.

  • Step 2: Not applicable because Task 4 was already completed before the story test run.

If Task 4 is complete, this should pass. If running this before Task 4, expected failure is that Read 3 files is not found.

cd site && pnpm test:storybook src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.stories.tsx -- --grep SequentialReadFilesCollapsed
  • Step 3: Run the story test after integration.
cd site && pnpm test:storybook src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.stories.tsx -- --grep SequentialReadFilesCollapsed

Expected: the story passes and confirms one grouped UI event.

Task 6: Format and verify the frontend change

Files:

  • All modified frontend files from previous tasks.

  • Step 1: Format frontend files.

cd site && pnpm format

Expected: Biome formats changed files.

  • Step 2: Run focused unit tests.
cd site && pnpm test -- src/pages/AgentsPage/components/ChatConversation/blockUtils.test.ts

Expected: all block utility tests pass.

  • Step 3: Run the focused Storybook interaction test.
cd site && pnpm test:storybook src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.stories.tsx -- --grep SequentialReadFilesCollapsed

Expected: the sequential read-file story passes.

  • Step 4: Run TypeScript check.
cd site && pnpm check

Expected: no TypeScript errors.

  • Step 5: Inspect the diff for unrelated changes.
git diff -- site/src/pages/AgentsPage/components/ChatConversation/blockUtils.ts site/src/pages/AgentsPage/components/ChatConversation/blockUtils.test.ts site/src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.tsx site/src/pages/AgentsPage/components/ChatElements/tools/ReadFileTool.tsx site/src/pages/AgentsPage/components/ChatElements/tools/ReadFilesTool.tsx site/src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.stories.tsx

Expected: the diff only contains grouping helpers, grouped UI, integration, and tests for CODAGT-337.

Follow-up: Real transcript shape fix

After manual testing showed the initial implementation did not collapse reads in
real chats, the reproduced transcript shape was updated to match persisted chat
messages: each read_file tool call can appear in its own assistant message,
with a hidden tool role result message between calls. The original
block-level grouping only handled multiple tool blocks inside one assistant
message, so those real sequential reads stayed separate.

Additional changes:

  • Added groupSequentialReadFileMessages in messageHelpers.ts to collapse
    adjacent read-file-only assistant messages before rendering the timeline.
  • Hidden tool-result-only messages are transparent for grouping and remain
    omitted from the rendered timeline.
  • Visible assistant text, reasoning, files, sources, user messages, and other
    visible tools still break a read-file group.
  • Updated SequentialReadFilesCollapsed to use the real multi-message shape.
  • Added messageHelpers.test.ts coverage for cross-message grouping.

@DanielleMaywood

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean, well-scoped PR. Two-pass grouping (message-level then block-level) is the right architecture for the persisted transcript shape, and the approach to keep grouping as a render-time derivation rather than a new block type was a good call.

Nice detail: the data-tool-call wrapper on ReadFilesTool preserving inter-block spacing, and the self-correction in the plan where the agent discovered block-level grouping alone was insufficient for real transcripts.

"Shall I show you how that breaks? ♥" (Hisoka, on the message/parsed divergence)

Severity breakdown: 2 P2, 6 P3, 1 P4, 4 Nit, 1 Note.

The two P2s are a real UI bug (error groups are locked shut) and a structural invariant violation (merged entry's raw message disagrees with parsed data). The P3s cover streaming UX, test coverage gaps, naming honesty, and a subtle behavioral change in how user messages are filtered.

🤖 This review was automatically generated with Coder Agents.

Comment thread site/src/pages/AgentsPage/components/ChatElements/tools/ReadFilesTool.tsx Outdated
Comment thread site/src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.tsx Outdated
Comment thread site/src/pages/AgentsPage/components/ChatConversation/blockUtils.ts
Comment thread site/src/pages/AgentsPage/components/ChatConversation/blockUtils.ts Outdated
Comment thread site/src/pages/AgentsPage/components/ChatElements/tools/ReadFilesTool.tsx Outdated

Copy link
Copy Markdown
Contributor Author

🤖 This comment was written by Coder Agent on behalf of Danielle Maywood 🤖

Thanks for the review. I will address the actionable comments inline and keep the scope focused on the read_file grouping UI.

@DanielleMaywood

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

R1 findings addressed thoroughly: 10 of 14 fixed in 2402bf0, 3 contested findings resolved by the panel, 1 acknowledged.

The DEREM-3 fix (expansion state persistence across single-to-group streaming transition) is well-designed. Lifting expansion state into ReadFileTimelineBlock with stable keying by the first tool ID is the right React pattern. The SequentialReadFilesExpansionPersistsAcrossGrouping story validates the transition end-to-end.

Contested findings closed: DEREM-5 (6/6 panel accept: old code already rendered hidden user messages as empty DOM), DEREM-8 (defense valid at N=2; N=3 tracked as DEREM-17), DEREM-10 (7/7 accept: inference sufficient).

Severity breakdown for new R2 findings: 4 P3, 1 P4.

DEREM-17 (third copy of read_file extraction) is the main open item. The author's R1 defense ("premature abstraction") was reasonable at N=2. The fix commit itself created N=3, which several panel members noted weakens the defense. A ~10-line shared helper would eliminate all three copies.

"Self-awareness without behavior change: the agent articulated why duplication is acceptable at N=2, then created N=3 in its own follow-up without self-correcting." (Mafu-san)

🤖 This review was automatically generated with Coder Agents.

Comment thread site/src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.tsx Outdated
Comment thread site/src/pages/AgentsPage/components/ChatElements/tools/ReadFilesTool.tsx Outdated

Copy link
Copy Markdown
Contributor Author

🤖 This comment was written by Coder Agent on behalf of Danielle Maywood 🤖

Thanks for the follow-up review. I will address the new R2 items inline.

@DanielleMaywood

Copy link
Copy Markdown
Contributor Author

Hopefully this comment gets in coder-agents-review[bot]'s context... I've attempted to "trim the fat" on this PR as the diff got too large.

@DanielleMaywood

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All P2 and P3 findings resolved across 3 rounds (17 fixed, 3 contested and panel-closed, 1 acknowledged, 3 dropped). Two minor P4s remain.

R2 fixes verified clean by Netero and panel: getReadFileToolData eliminated the extraction duplication (DEREM-17), guard branch tests close coverage gaps (DEREM-18), all four merged arrays asserted (DEREM-19), redundant shouldHide removed from computeLastInChainFlags (DEREM-20), empty-file groups expandable (DEREM-21).

The design is solid: two-layer grouping (message then block), stable keying via first tool ID for streaming transitions, shared getReadFileToolData as single extraction point, and controlled/uncontrolled expansion via ReadFileTimelineBlock. Test coverage is thorough across both unit tests and interactive Storybook stories.

🤖 This review was automatically generated with Coder Agents.

Copy link
Copy Markdown
Contributor Author

🤖 This comment was written by Coder Agent on behalf of Danielle Maywood 🤖

Thanks for the final pass. I addressed the remaining low-risk test coverage note in 74eb7f1 and kept the change scoped to the existing message grouping tests.

@DanielleMaywood

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DEREM-23 addressed in 74eb7f1 (user message breaking a read_file group test added). DEREM-24 (P4, running-state story removed in trim commit 348599c) has no author response. The finding was posted as a reply to the DEREM-7 thread, which GitHub shows as [Resolved, Outdated], so it may have been missed.

DEREM-24 is low-risk (the rendering is isRunning ? "Reading N files..." : "Read N files" plus a spinner icon). A brief acknowledgment, a re-added story, or an explicit "leaving as-is" would close it.

Further review is paused until the author responds to or addresses DEREM-24.

🤖 This review was automatically generated with Coder Agents.

@DanielleMaywood
DanielleMaywood force-pushed the codagt-337-collapse-read-file-events branch from 74eb7f1 to b99960f Compare May 14, 2026 09:57

Copy link
Copy Markdown
Contributor Author

🤖 This comment was written by Coder Agent on behalf of Danielle Maywood 🤖

Rebased on main and addressed DEREM-24 by re-adding the running-state Storybook story in b99960fb3.

@DanielleMaywood

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All 24 findings resolved across 5 rounds. Zero open items.

19 fixed by the author, 3 contested and panel-closed, 1 acknowledged, 3 dropped by orchestrator. The author addressed every finding substantively, including the R4 BLOCKED round where DEREM-24 (running-state story) was restored after being trimmed.

The final codebase is clean: shared getReadFileToolData extraction, two-layer grouping with stable keying, comprehensive test coverage (unit tests with parameterized guard branches, Storybook stories with play functions covering happy path, error states, running state, empty files, and expansion persistence across streaming transitions).

"The code fought well." (Hisoka)

🤖 This review was automatically generated with Coder Agents.

@DanielleMaywood
DanielleMaywood force-pushed the codagt-337-collapse-read-file-events branch from b99960f to ff13eb5 Compare May 15, 2026 10:22
@DanielleMaywood
DanielleMaywood requested a review from jaaydenh May 15, 2026 10:49
@DanielleMaywood
DanielleMaywood force-pushed the codagt-337-collapse-read-file-events branch from e6ece2e to 83d2ee4 Compare May 22, 2026 13:50
@DanielleMaywood
DanielleMaywood marked this pull request as ready for review May 26, 2026 14:41
@DanielleMaywood
DanielleMaywood merged commit c56af60 into main May 26, 2026
45 of 51 checks passed
@DanielleMaywood
DanielleMaywood deleted the codagt-337-collapse-read-file-events branch May 26, 2026 15:19
@github-actions github-actions Bot locked and limited conversation to collaborators May 26, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants