Files
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

105 lines
3.7 KiB
TypeScript

// Adapted from agentui (https://github.com/ashish200729/agentui), components/agents/message-bubble.tsx
// Copyright (c) 2026 Saurabh Chauhan. MIT License, see ./LICENSE-agentui.md
// 改造:去掉 motion 的 layout 动画与 render 克隆;变体底色换语义 token(soft = foreground 洗层,danger = destructive,
// warning 为新增);折叠遮罩用 mask-image 渐变到 transparent(不写十六进制),箭头旋转用 duration-base。
import { ChevronDown } from "lucide-react";
import { type ComponentPropsWithRef, createContext, useContext, useId, useState } from "react";
import { cn } from "~/lib/utils.ts";
import { CHEVRON_CLASS } from "./disclosure.tsx";
import { MessageSideContext } from "./message.tsx";
export type MessageBubbleVariant = "solid" | "soft" | "outline" | "ghost" | "danger" | "warning";
const BubbleContext = createContext<{ variant: MessageBubbleVariant; align: "start" | "end" }>({
variant: "soft",
align: "start",
});
export function MessageBubble({
variant = "soft",
align,
className,
...props
}: ComponentPropsWithRef<"div"> & { variant?: MessageBubbleVariant; align?: "start" | "end" }) {
const side = useContext(MessageSideContext);
const resolved = align ?? side ?? "start";
return (
<BubbleContext.Provider value={{ variant, align: resolved }}>
<div
data-slot="message-bubble"
data-variant={variant}
className={cn("flex w-full min-w-0 flex-col", resolved === "end" ? "items-end" : "items-start", className)}
{...props}
/>
</BubbleContext.Provider>
);
}
const VARIANT: Record<MessageBubbleVariant, string> = {
solid: "bg-foreground text-background",
soft: "border border-border/60 bg-foreground/[0.05]",
outline: "border border-border/70",
ghost: "w-full max-w-none rounded-none border-0 px-0 py-0",
danger: "border border-destructive/40 bg-destructive/10 text-destructive",
warning: "border border-warning/40 bg-warning/10 text-warning",
};
export function MessageBubbleContent({ className, ...props }: ComponentPropsWithRef<"div">) {
const { variant } = useContext(BubbleContext);
return (
<div
data-slot="message-bubble-content"
className={cn(
"relative min-w-9 max-w-[85%] rounded-2xl px-3.5 py-2.5 text-sm leading-6 break-words text-foreground",
VARIANT[variant],
className,
)}
{...props}
/>
);
}
const CLAMP = {
2: "line-clamp-2",
3: "line-clamp-3",
4: "line-clamp-4",
5: "line-clamp-5",
6: "line-clamp-6",
} as const;
/** 长消息折叠:收起时按行截断并渐隐 */
export function MessageBubbleCollapsible({
collapsedLines = 4,
moreLabel = "展开",
lessLabel = "收起",
className,
children,
...props
}: ComponentPropsWithRef<"div"> & { collapsedLines?: 2 | 3 | 4 | 5 | 6; moreLabel?: string; lessLabel?: string }) {
const id = useId();
const [open, setOpen] = useState(false);
return (
<div data-state={open ? "open" : "closed"} className={cn("w-full", className)} {...props}>
<div
id={id}
className={cn(
!open && CLAMP[collapsedLines],
!open && "[mask-image:linear-gradient(to_bottom,black_68%,transparent)]",
)}
>
{children}
</div>
<button
type="button"
aria-expanded={open}
aria-controls={id}
onClick={() => setOpen(!open)}
className="mt-1.5 inline-flex h-7 cursor-pointer items-center gap-1 rounded-full px-2 text-meta font-medium text-muted-foreground outline-none transition-colors duration-micro hover:bg-foreground/[0.06] hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
>
{open ? lessLabel : moreLabel}
<ChevronDown aria-hidden className={cn("size-3.5", CHEVRON_CLASS, open && "rotate-180")} />
</button>
</div>
);
}