mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-05-24 10:32:45 +00:00
fix(webui): avoid misleading file edit counters
This commit is contained in:
parent
9b2f452b6e
commit
effc1efd92
@ -27,6 +27,7 @@ interface ActivityCounts {
|
|||||||
fileCount: number;
|
fileCount: number;
|
||||||
added: number;
|
added: number;
|
||||||
deleted: number;
|
deleted: number;
|
||||||
|
hasDiffStats: boolean;
|
||||||
hasEditingFiles: boolean;
|
hasEditingFiles: boolean;
|
||||||
hasFailedFiles: boolean;
|
hasFailedFiles: boolean;
|
||||||
primaryFilePath?: string;
|
primaryFilePath?: string;
|
||||||
@ -61,6 +62,7 @@ function countActivity(messages: UIMessage[], fileEdits: FileEditSummary[]): Act
|
|||||||
}
|
}
|
||||||
let added = 0;
|
let added = 0;
|
||||||
let deleted = 0;
|
let deleted = 0;
|
||||||
|
let hasDiffStats = false;
|
||||||
let hasEditingFiles = false;
|
let hasEditingFiles = false;
|
||||||
let failedFileCount = 0;
|
let failedFileCount = 0;
|
||||||
let primaryFilePath: string | undefined;
|
let primaryFilePath: string | undefined;
|
||||||
@ -77,6 +79,10 @@ function countActivity(messages: UIMessage[], fileEdits: FileEditSummary[]): Act
|
|||||||
if (edit.status === "error" || edit.binary) {
|
if (edit.status === "error" || edit.binary) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (!hasVisibleDiffStats(edit)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
hasDiffStats = true;
|
||||||
added += edit.added;
|
added += edit.added;
|
||||||
deleted += edit.deleted;
|
deleted += edit.deleted;
|
||||||
}
|
}
|
||||||
@ -86,6 +92,7 @@ function countActivity(messages: UIMessage[], fileEdits: FileEditSummary[]): Act
|
|||||||
fileCount: fileEdits.length,
|
fileCount: fileEdits.length,
|
||||||
added,
|
added,
|
||||||
deleted,
|
deleted,
|
||||||
|
hasDiffStats,
|
||||||
hasEditingFiles,
|
hasEditingFiles,
|
||||||
hasFailedFiles: fileEdits.length > 0 && failedFileCount === fileEdits.length,
|
hasFailedFiles: fileEdits.length > 0 && failedFileCount === fileEdits.length,
|
||||||
primaryFilePath,
|
primaryFilePath,
|
||||||
@ -120,6 +127,7 @@ export function AgentActivityCluster({
|
|||||||
fileCount,
|
fileCount,
|
||||||
added,
|
added,
|
||||||
deleted,
|
deleted,
|
||||||
|
hasDiffStats,
|
||||||
hasEditingFiles,
|
hasEditingFiles,
|
||||||
hasFailedFiles,
|
hasFailedFiles,
|
||||||
primaryFilePath,
|
primaryFilePath,
|
||||||
@ -140,6 +148,7 @@ export function AgentActivityCluster({
|
|||||||
const headerBusy = fileCount > 0 ? hasEditingFiles : isTurnStreaming;
|
const headerBusy = fileCount > 0 ? hasEditingFiles : isTurnStreaming;
|
||||||
const singleFilePath = fileCount === 1 ? primaryFilePath : undefined;
|
const singleFilePath = fileCount === 1 ? primaryFilePath : undefined;
|
||||||
const singleFileTooltipPath = fileCount === 1 ? primaryFileTooltipPath : undefined;
|
const singleFileTooltipPath = fileCount === 1 ? primaryFileTooltipPath : undefined;
|
||||||
|
const hasVisibleActivity = reasoningSteps > 0 || toolCalls > 0 || fileCount > 0;
|
||||||
|
|
||||||
const fileActivitySummary = fileCount > 0
|
const fileActivitySummary = fileCount > 0
|
||||||
? hasPendingFileEdit && !singleFilePath
|
? hasPendingFileEdit && !singleFilePath
|
||||||
@ -243,6 +252,8 @@ export function AgentActivityCluster({
|
|||||||
autoFollowActivityRef.current = distance < ACTIVITY_SCROLL_NEAR_BOTTOM_PX;
|
autoFollowActivityRef.current = distance < ACTIVITY_SCROLL_NEAR_BOTTOM_PX;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
if (!hasVisibleActivity) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("w-full", hasBodyBelow && "mb-2")}>
|
<div className={cn("w-full", hasBodyBelow && "mb-2")}>
|
||||||
<button
|
<button
|
||||||
@ -282,7 +293,7 @@ export function AgentActivityCluster({
|
|||||||
{summary}
|
{summary}
|
||||||
</StreamingLabelSheen>
|
</StreamingLabelSheen>
|
||||||
)}
|
)}
|
||||||
{fileCount > 0 && (
|
{fileCount > 0 && hasDiffStats && (
|
||||||
<span className="inline-flex min-w-0 items-center gap-1 text-muted-foreground/85">
|
<span className="inline-flex min-w-0 items-center gap-1 text-muted-foreground/85">
|
||||||
<DiffPair added={added} deleted={deleted} />
|
<DiffPair added={added} deleted={deleted} />
|
||||||
</span>
|
</span>
|
||||||
@ -435,6 +446,17 @@ function summarizeFileEdits(edits: UIFileEdit[], active: boolean): FileEditSumma
|
|||||||
summary.absolute_path = edit.absolute_path;
|
summary.absolute_path = edit.absolute_path;
|
||||||
}
|
}
|
||||||
summary.pending = summary.pending || !!edit.pending || !edit.path;
|
summary.pending = summary.pending || !!edit.pending || !edit.path;
|
||||||
|
if (!edit.path && edit.pending) {
|
||||||
|
if (active && edit.status === "editing") {
|
||||||
|
summary.hasActiveEditing = true;
|
||||||
|
summary.approximate = summary.approximate || !!edit.approximate;
|
||||||
|
if (!edit.binary) {
|
||||||
|
summary.added += edit.added;
|
||||||
|
summary.deleted += edit.deleted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (active && edit.status === "editing") {
|
if (active && edit.status === "editing") {
|
||||||
summary.hasActiveEditing = true;
|
summary.hasActiveEditing = true;
|
||||||
summary.binary = summary.binary || !!edit.binary;
|
summary.binary = summary.binary || !!edit.binary;
|
||||||
@ -461,8 +483,16 @@ function summarizeFileEdits(edits: UIFileEdit[], active: boolean): FileEditSumma
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return order.map((key) => {
|
return order.flatMap((key) => {
|
||||||
const summary = byPath.get(key)!;
|
const summary = byPath.get(key)!;
|
||||||
|
if (
|
||||||
|
!summary.path
|
||||||
|
&& !summary.hasActiveEditing
|
||||||
|
&& !summary.hasSuccessfulChange
|
||||||
|
&& !summary.hasFailed
|
||||||
|
) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
const status: UIFileEdit["status"] = summary.hasActiveEditing
|
const status: UIFileEdit["status"] = summary.hasActiveEditing
|
||||||
? "editing"
|
? "editing"
|
||||||
: summary.hasSuccessfulChange
|
: summary.hasSuccessfulChange
|
||||||
@ -470,7 +500,7 @@ function summarizeFileEdits(edits: UIFileEdit[], active: boolean): FileEditSumma
|
|||||||
: summary.hasFailed
|
: summary.hasFailed
|
||||||
? "error"
|
? "error"
|
||||||
: "done";
|
: "done";
|
||||||
return {
|
return [{
|
||||||
key: summary.key,
|
key: summary.key,
|
||||||
path: summary.path,
|
path: summary.path,
|
||||||
absolute_path: summary.absolute_path,
|
absolute_path: summary.absolute_path,
|
||||||
@ -481,10 +511,14 @@ function summarizeFileEdits(edits: UIFileEdit[], active: boolean): FileEditSumma
|
|||||||
status,
|
status,
|
||||||
pending: summary.pending && !summary.path,
|
pending: summary.pending && !summary.path,
|
||||||
error: summary.error,
|
error: summary.error,
|
||||||
};
|
}];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasVisibleDiffStats(edit: Pick<FileEditSummary, "added" | "deleted">): boolean {
|
||||||
|
return edit.added > 0 || edit.deleted > 0;
|
||||||
|
}
|
||||||
|
|
||||||
function FileEditGroup({ edits }: { edits: FileEditSummary[] }) {
|
function FileEditGroup({ edits }: { edits: FileEditSummary[] }) {
|
||||||
if (edits.length === 0) return null;
|
if (edits.length === 0) return null;
|
||||||
return (
|
return (
|
||||||
@ -500,7 +534,7 @@ function FileEditRow({ edit }: { edit: FileEditSummary }) {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const editing = edit.status === "editing";
|
const editing = edit.status === "editing";
|
||||||
const failed = edit.status === "error";
|
const failed = edit.status === "error";
|
||||||
const hasCountedDiff = !failed && !edit.binary;
|
const hasCountedDiff = !failed && !edit.binary && hasVisibleDiffStats(edit);
|
||||||
return (
|
return (
|
||||||
<li className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-3 rounded-md px-2 py-1.5 text-xs">
|
<li className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-3 rounded-md px-2 py-1.5 text-xs">
|
||||||
<div className="flex min-w-0 items-center gap-2">
|
<div className="flex min-w-0 items-center gap-2">
|
||||||
|
|||||||
@ -271,6 +271,69 @@ describe("AgentActivityCluster", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not render zero diff counters for completed edits", () => {
|
||||||
|
render(
|
||||||
|
<AgentActivityCluster
|
||||||
|
messages={activityMessages("", {
|
||||||
|
id: "t2",
|
||||||
|
role: "tool",
|
||||||
|
kind: "trace",
|
||||||
|
content: "edit_file()",
|
||||||
|
traces: ["edit_file()"],
|
||||||
|
fileEdits: [{
|
||||||
|
call_id: "call-edit",
|
||||||
|
tool: "edit_file",
|
||||||
|
path: "src/app.tsx",
|
||||||
|
phase: "end",
|
||||||
|
added: 0,
|
||||||
|
deleted: 0,
|
||||||
|
approximate: false,
|
||||||
|
status: "done",
|
||||||
|
}],
|
||||||
|
createdAt: 3,
|
||||||
|
})}
|
||||||
|
isTurnStreaming={false}
|
||||||
|
hasBodyBelow={false}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByRole("button", { name: /edited app\.tsx/i })).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("+0")).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("-0")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("drops stale pathless pending edits after the turn completes", () => {
|
||||||
|
render(
|
||||||
|
<AgentActivityCluster
|
||||||
|
messages={[{
|
||||||
|
id: "t1",
|
||||||
|
role: "tool",
|
||||||
|
kind: "trace",
|
||||||
|
content: "",
|
||||||
|
traces: [],
|
||||||
|
fileEdits: [{
|
||||||
|
call_id: "call-edit",
|
||||||
|
tool: "edit_file",
|
||||||
|
path: "",
|
||||||
|
phase: "start",
|
||||||
|
added: 98,
|
||||||
|
deleted: 0,
|
||||||
|
approximate: true,
|
||||||
|
status: "editing",
|
||||||
|
pending: true,
|
||||||
|
}],
|
||||||
|
createdAt: 1,
|
||||||
|
}]}
|
||||||
|
isTurnStreaming={false}
|
||||||
|
hasBodyBelow={false}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByRole("button", { name: /preparing edit/i })).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("+98")).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("0 tool calls")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it("renders pending file edit placeholders before the path is known", () => {
|
it("renders pending file edit placeholders before the path is known", () => {
|
||||||
render(
|
render(
|
||||||
<AgentActivityCluster
|
<AgentActivityCluster
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user