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]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-27 23:08:17 +08:00
1 parent 29cd10b27e
commit 8352e5e881
26 files changed
+3390 -585

No files matched your search

@@ -0,0 +1,99 @@
// 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>
);
}