mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-06-15 07:14:08 +00:00
* feat(webui): refine output timeline and composer queue * feat(webui): add provider model picker * fix(webui): polish model settings and heartbeat checks * chore: keep heartbeat changes out of webui pr * refactor(webui): isolate settings routes * fix(providers): align minimax anthropic test * fix(providers): keep minimax anthropic base sdk-compatible * fix(providers): normalize anthropic base urls
115 lines
3.8 KiB
TypeScript
115 lines
3.8 KiB
TypeScript
import { AlertCircle, CheckCircle2, CircleDashed } from "lucide-react";
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
import { FileReferenceChip } from "@/components/FileReferenceChip";
|
|
import type { UIFileEdit } from "@/lib/types";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
import { ActivityStep } from "./ActivityStep";
|
|
import { DiffPair } from "./DiffPair";
|
|
|
|
export interface FileEditSummary {
|
|
key: string;
|
|
path: string;
|
|
absolute_path?: string;
|
|
added: number;
|
|
deleted: number;
|
|
approximate: boolean;
|
|
binary: boolean;
|
|
status: UIFileEdit["status"];
|
|
operation?: UIFileEdit["operation"];
|
|
pending: boolean;
|
|
error?: string;
|
|
}
|
|
|
|
export function FileEditGroup({ edits }: { edits: FileEditSummary[] }) {
|
|
if (edits.length === 0) return null;
|
|
return (
|
|
<ul className="space-y-1">
|
|
{edits.map((edit) => (
|
|
<FileEditRow key={edit.key} edit={edit} />
|
|
))}
|
|
</ul>
|
|
);
|
|
}
|
|
|
|
function FileEditRow({ edit }: { edit: FileEditSummary }) {
|
|
const { t } = useTranslation();
|
|
const editing = edit.status === "editing";
|
|
const failed = edit.status === "error";
|
|
const hasCountedDiff = !failed && !edit.binary && hasVisibleDiffStats(edit);
|
|
const failureDetail = failed
|
|
? formatFileEditError(edit.error)
|
|
|| t("message.fileEditFailedFallback", { defaultValue: "File change was not applied." })
|
|
: "";
|
|
const statusIcon = failed ? (
|
|
<AlertCircle className="h-3 w-3" aria-hidden />
|
|
) : editing ? (
|
|
<CircleDashed className="h-3 w-3 animate-spin" aria-hidden />
|
|
) : (
|
|
<CheckCircle2 className="h-3 w-3" aria-hidden />
|
|
);
|
|
return (
|
|
<ActivityStep
|
|
as="li"
|
|
marker={(
|
|
<span
|
|
className={cn(
|
|
"grid h-3.5 w-3.5 place-items-center rounded-full border bg-background transition-colors",
|
|
failed && "border-destructive/30 text-destructive/78",
|
|
editing && "border-muted-foreground/24 text-muted-foreground/65",
|
|
!failed && !editing && "border-emerald-500/28 text-emerald-500/78",
|
|
)}
|
|
>
|
|
{statusIcon}
|
|
</span>
|
|
)}
|
|
active={editing}
|
|
tone={failed ? "error" : editing ? "active" : "success"}
|
|
className="text-xs"
|
|
contentClassName="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-3"
|
|
title={failureDetail || edit.absolute_path || edit.path}
|
|
label={edit.pending && !edit.path
|
|
? t("message.fileEditPreparing", { defaultValue: "Preparing file edit…" })
|
|
: (
|
|
<FileReferenceChip
|
|
path={edit.path}
|
|
tooltipPath={edit.absolute_path}
|
|
display="path"
|
|
active={editing}
|
|
className="min-w-0"
|
|
textClassName="text-[12px]"
|
|
testId="activity-file-reference"
|
|
/>
|
|
)}
|
|
detail={failed ? (
|
|
<span className="min-w-0 truncate text-[11px] leading-4 text-destructive/75">
|
|
{failureDetail}
|
|
</span>
|
|
) : null}
|
|
aside={hasCountedDiff ? <DiffPair added={edit.added} deleted={edit.deleted} /> : null}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export function hasVisibleDiffStats(edit: Pick<FileEditSummary, "added" | "deleted">): boolean {
|
|
return edit.added > 0 || edit.deleted > 0;
|
|
}
|
|
|
|
function formatFileEditError(error?: string): string {
|
|
const firstLine = (error || "").replace(/\s+/g, " ").trim();
|
|
if (!firstLine) return "";
|
|
const cleaned = firstLine
|
|
.replace(/^Error applying patch:\s*/i, "")
|
|
.replace(/^Error writing file:\s*/i, "")
|
|
.replace(/^Error editing file:\s*/i, "")
|
|
.replace(/^Error:\s*/i, "");
|
|
|
|
return cleaned
|
|
.replace(/^old_text not found in (.+)$/i, "Target text was not found in $1.")
|
|
.replace(/^old_text appears multiple times in (.+)$/i, "Target text matched multiple places in $1.")
|
|
.replace(/^file to (?:update|delete) does not exist: (.+)$/i, "File does not exist: $1.")
|
|
.replace(/^path to (?:update|delete) is not a file: (.+)$/i, "Path is not a file: $1.")
|
|
.slice(0, 180);
|
|
}
|