fix(chat): count a subagent's edits in the turn's Edited N files card (#86)
A skill that hands its work to subagents (vp-auto) showed "Edited 1 file" for a turn that changed 4: a subagent's Write/Edit calls are in its own agent-<id>.jsonl, so the card only saw the parent's. - server/lib/agent-edits.js reads a subagent's transcript (readLines, a member's only as theirs) for the files it wrote or edited with their +/- lines, nested subagents folded in (3 levels), cached by size + mtime. - The transcript route puts them on each Claude Agent/Task result as `edits`; the card adds them to the turn's own edits. A file only a subagent wrote is listed, never an artifact card (its versions are rebuilt from this transcript's inputs, which a subagent's are not).
This commit is contained in:
@@ -491,6 +491,8 @@ type Item =
|
||||
| { kind: "command"; key: string; name: string | null; output: string | null; error: boolean };
|
||||
|
||||
const EDIT_TOOLS = new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
|
||||
// A subagent's result carries the files IT changed (`edits`): they count too.
|
||||
const AGENT_TOOLS = new Set(["Agent", "Task"]);
|
||||
|
||||
/** A line that is only Claude Code's local-command plumbing — the caveat it
|
||||
* writes first, the command, what the command printed — or null for
|
||||
@@ -592,7 +594,11 @@ export function chatItems(messages: TranscriptMessage[], keys: string[]): Item[]
|
||||
if (msg.usage) output.add(msg.usage);
|
||||
msg.content.forEach((block, b) => {
|
||||
const last = items[items.length - 1];
|
||||
if (block.type === "tool_use" && EDIT_TOOLS.has(block.name ?? "")) edits.push(block);
|
||||
if (
|
||||
block.type === "tool_use" &&
|
||||
(EDIT_TOOLS.has(block.name ?? "") || AGENT_TOOLS.has(block.name ?? ""))
|
||||
)
|
||||
edits.push(block);
|
||||
// A question to the user is a turn of the conversation, not hidden work.
|
||||
if (block.type === "tool_use" && block.name === "AskUserQuestion") {
|
||||
items.push({ kind: "ask", key: `${key}:${b}`, block });
|
||||
@@ -672,19 +678,38 @@ function EditedFilesCard({
|
||||
const files = new Map<string, { added: number; removed: number; counted: boolean }>();
|
||||
// Written whole in this turn (a Write): a component made here is an artifact.
|
||||
const created = new Set<string>();
|
||||
// Changed only by a subagent: listed, never an artifact card — the chat
|
||||
// rebuilds an artifact from this transcript's Write/Edit inputs, which a
|
||||
// subagent's are not.
|
||||
const byAgent = new Set<string>();
|
||||
for (const b of blocks) {
|
||||
const r0 = b.id ? toolResultMap.get(b.id) : undefined;
|
||||
if (AGENT_TOOLS.has(b.name ?? "")) {
|
||||
for (const e of r0?.edits ?? []) {
|
||||
if (!files.has(e.path)) byAgent.add(e.path);
|
||||
const f = files.get(e.path) ?? { added: 0, removed: 0, counted: false };
|
||||
f.added += e.added;
|
||||
f.removed += e.removed;
|
||||
f.counted ||= e.counted;
|
||||
files.set(e.path, f);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const input = (b.input ?? {}) as Record<string, unknown>;
|
||||
const p = input.file_path ?? input.notebook_path;
|
||||
const r = b.id ? toolResultMap.get(b.id) : undefined;
|
||||
if (typeof p !== "string" || !r || r.is_error) continue;
|
||||
if (b.name === "Write") created.add(p);
|
||||
byAgent.delete(p);
|
||||
const f = files.get(p) ?? { added: 0, removed: 0, counted: false };
|
||||
f.added += r.diff?.added ?? 0;
|
||||
f.removed += r.diff?.removed ?? 0;
|
||||
f.counted ||= !!r.diff;
|
||||
files.set(p, f);
|
||||
}
|
||||
const artifacts = [...files.keys()].filter((p) => isArtifact(p, created.has(p)));
|
||||
const artifacts = [...files.keys()].filter(
|
||||
(p) => !byAgent.has(p) && isArtifact(p, created.has(p))
|
||||
);
|
||||
const cards = artifacts.length > 0 && (
|
||||
<div data-testid="artifact-cards" className="flex flex-col gap-2">
|
||||
{artifacts.map((p) => (
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
* Desktop's "Edited N files" card: one row per file with the lines added and
|
||||
* removed summed over the turn, a failed call left out, a row click handed
|
||||
* to the caller, and a row (or the header, every file) dragged as `@path`
|
||||
* attachments for a prompt box.
|
||||
* attachments for a prompt box. Files a subagent changed (its Agent
|
||||
* result's `edits`, read from its own transcript) are counted in the same
|
||||
* card, never as an artifact.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
@@ -127,4 +129,49 @@ describe("the Edited N files card", () => {
|
||||
JSON.parse(all["application/x-agenthub-attach"]!).map((a: { path: string }) => a.path)
|
||||
).toEqual(["@/w/a.ts", "@/w/b.json"]);
|
||||
});
|
||||
|
||||
it("counts the files a subagent changed, beside the turn's own edits", () => {
|
||||
render(
|
||||
<MessageList
|
||||
loading={false}
|
||||
messages={[
|
||||
user("run the task"),
|
||||
call("1", "Edit", "/w/a.go"),
|
||||
result("1", { added: 1, removed: 0 }),
|
||||
{
|
||||
type: "assistant",
|
||||
sender: "assistant",
|
||||
timestamp: at,
|
||||
content: [{ type: "tool_use", id: "2", name: "Agent", input: { prompt: "do it" } }],
|
||||
} as TranscriptMessage,
|
||||
{
|
||||
type: "user",
|
||||
sender: "tool",
|
||||
timestamp: at,
|
||||
content: [
|
||||
{
|
||||
type: "tool_result",
|
||||
id: "2",
|
||||
output: "done",
|
||||
agent: { id: "x1" },
|
||||
edits: [
|
||||
{ path: "/w/a.go", added: 4, removed: 2, counted: true },
|
||||
{ path: "/w/b_test.go", added: 9, removed: 0, counted: true },
|
||||
{ path: "/w/report.html", added: 5, removed: 0, counted: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
} as TranscriptMessage,
|
||||
user("thanks"),
|
||||
]}
|
||||
/>
|
||||
);
|
||||
const card = screen.getByTestId("edited-files");
|
||||
expect(card.textContent).toContain("Edited 3 files");
|
||||
expect(within(card).getByText("b_test.go")).toBeTruthy();
|
||||
expect(within(card).getByTitle("/w/a.go").textContent).toContain("+5"); // a.go: 1 + 4
|
||||
// A page only a subagent wrote is listed, not shown as an artifact card.
|
||||
expect(within(card).getByText("report.html")).toBeTruthy();
|
||||
expect(screen.queryByTestId("artifact-card")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3236,6 +3236,10 @@ export interface TranscriptContent {
|
||||
/** Present on a Write/Edit/MultiEdit "tool_result": the lines it added and
|
||||
* removed (from Claude Code's `structuredPatch`). */
|
||||
diff?: { added: number; removed: number };
|
||||
/** Present on an Agent/Task "tool_result" whose subagent (or one it started)
|
||||
* wrote or edited files: each file with its +/- lines (`counted`: the
|
||||
* results carried them). Read from the subagent's own transcript. */
|
||||
edits?: { path: string; added: number; removed: number; counted: boolean }[];
|
||||
/** Present on an AskUserQuestion "tool_result": question text → the option
|
||||
* the user picked, or what they typed. */
|
||||
answers?: Record<string, string>;
|
||||
|
||||
Reference in New Issue
Block a user