- Port agentui (MIT, ashish200729/agentui@b2b752b) Message/MessageBubble/MessageScroller, StreamingResponse, ToolResult/ToolApproval, TodoList, ApprovalCard, CodeBlock/FileDiff, Citations, PromptInput into components/agentui; drop motion/shiki/Next, switch colors, type scale, radii, durations and easing to semantic tokens, honour reduced motion - Conversation: streaming cursor, collapsible thinking, tool cards with kind/scope/risk/ idempotency badges and audit idempotency key, prominent server-rejection state for both whitelist rejections and unauthorized errors, compaction markers, budget meters with over-limit state, approval cards (admin session only), two-column final report (model said vs system verified with task/record links), follow-scroll with jump-to-bottom - Composer: Enter send / Shift+Enter newline, stop = abort (confirmed), append, execute/plan_only switch, "execute this plan" from finished plan_only runs - Session list sidebar with status filter and usage; reconnect/error banners with retry - Drawer: route-derived context (entity/site/task), minimize to a live mini dock - Fix stream refetch race when the run switches from loading to finished Co-Authored-By: Claude Opus 5.5 <[email protected]>
100 lines
3.9 KiB
TypeScript
100 lines
3.9 KiB
TypeScript
// Adapted from agentui (https://github.com/ashish200729/agentui), components/agents/file-diff.tsx
|
||
// Copyright (c) 2026 Saurabh Chauhan. MIT License, see ./LICENSE-agentui.md
|
||
// 改造:新增/删除行底色 emerald/rose → success/destructive 洗层(并保留 +/− 符号,不只靠颜色);不做语法高亮(去 shiki);
|
||
// 折叠用 Disclosure;maxHeight 改 Tailwind 类;用于展示路径类变更(如整理记录的入库目标)。
|
||
import { ChevronDown, FileCode2 } from "lucide-react";
|
||
import { type ReactNode, useId, useState } from "react";
|
||
import { cn } from "~/lib/utils.ts";
|
||
import { CHEVRON_CLASS, Disclosure } from "./disclosure.tsx";
|
||
|
||
export type FileDiffLineType = "added" | "removed" | "context";
|
||
|
||
export interface FileDiffLine {
|
||
id: string;
|
||
type?: FileDiffLineType;
|
||
content: ReactNode;
|
||
/** 行尾附注(如状态) */
|
||
note?: ReactNode;
|
||
}
|
||
|
||
function Count({ value, type }: { value: number; type: "added" | "removed" }) {
|
||
if (!value) return null;
|
||
return (
|
||
<span className={cn("font-mono text-meta tabular-nums", type === "added" ? "text-success" : "text-destructive")}>
|
||
{type === "added" ? "+" : "−"}
|
||
{value}
|
||
</span>
|
||
);
|
||
}
|
||
|
||
export function FileDiff({
|
||
file,
|
||
lines,
|
||
defaultOpen = true,
|
||
maxHeightClassName = "max-h-56",
|
||
className,
|
||
}: {
|
||
file: ReactNode;
|
||
lines: FileDiffLine[];
|
||
defaultOpen?: boolean;
|
||
maxHeightClassName?: string;
|
||
className?: string;
|
||
}) {
|
||
const id = useId();
|
||
const [open, setOpen] = useState(defaultOpen);
|
||
const added = lines.filter((l) => l.type === "added").length;
|
||
const removed = lines.filter((l) => l.type === "removed").length;
|
||
return (
|
||
<div className={cn("w-full min-w-0 overflow-hidden rounded-lg border border-border/60 text-sm", className)}>
|
||
<button
|
||
type="button"
|
||
aria-expanded={open}
|
||
aria-controls={id}
|
||
onClick={() => setOpen(!open)}
|
||
className="flex h-8 w-full cursor-pointer items-center gap-2 px-2.5 text-left outline-none transition-colors duration-micro hover:bg-foreground/[0.03] focus-visible:ring-2 focus-visible:ring-ring"
|
||
>
|
||
<FileCode2 aria-hidden className="size-3.5 shrink-0 text-muted-foreground/70" />
|
||
<span className="min-w-0 flex-1 truncate text-meta text-foreground/85">{file}</span>
|
||
<Count value={added} type="added" />
|
||
<Count value={removed} type="removed" />
|
||
<ChevronDown aria-hidden className={cn("size-3.5", CHEVRON_CLASS, open && "rotate-180")} />
|
||
</button>
|
||
<Disclosure id={id} open={open}>
|
||
<div className={cn("overflow-auto border-t border-border/50 py-1", maxHeightClassName)}>
|
||
<ul className="font-mono text-meta leading-5">
|
||
{lines.map((l) => (
|
||
<li
|
||
key={l.id}
|
||
className={cn(
|
||
"grid grid-cols-[1.25rem_minmax(0,1fr)_auto] gap-1 pr-2.5",
|
||
l.type === "added" && "bg-success/[0.08]",
|
||
l.type === "removed" && "bg-destructive/[0.08]",
|
||
)}
|
||
>
|
||
<span
|
||
aria-hidden
|
||
className={cn(
|
||
"text-center select-none",
|
||
l.type === "added"
|
||
? "text-success"
|
||
: l.type === "removed"
|
||
? "text-destructive"
|
||
: "text-muted-foreground/50",
|
||
)}
|
||
>
|
||
{l.type === "added" ? "+" : l.type === "removed" ? "−" : " "}
|
||
</span>
|
||
<span className="min-w-0 break-all text-foreground/85">
|
||
<span className="sr-only">{l.type === "added" ? "新增:" : l.type === "removed" ? "移除:" : ""}</span>
|
||
{l.content}
|
||
</span>
|
||
{l.note ? <span className="shrink-0 font-sans text-muted-foreground">{l.note}</span> : null}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</div>
|
||
</Disclosure>
|
||
</div>
|
||
);
|
||
}
|