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:
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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user