Files
UHD_Media_Tool/apps/web/app/components/agentui/file-diff.tsx
T
rootandClaude Opus 5.5 8352e5e881 feat(web): W4-F rebuild agent conversation and drawer with agentui components
- 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]>
2026-09-27 23:08:17 +08:00

100 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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>
);
}