fix(webui): align thinking and tool trace affordances

Tool trace groups are supporting details, so default them to collapsed.
Match the Thinking bubble's expanded body to the tool trace affordance by
using the same grouped header and animated fade/slide body treatment.

Update MessageBubble tests to assert tool traces start collapsed and expand
on click.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Xubin Ren 2026-05-13 07:58:24 +00:00
parent 521aaa5ecf
commit c7ec5d3b75
2 changed files with 10 additions and 8 deletions

View File

@ -386,14 +386,14 @@ interface TraceGroupProps {
/** /**
* Collapsible group of tool-call / progress breadcrumbs. Defaults to * Collapsible group of tool-call / progress breadcrumbs. Defaults to
* expanded for discoverability; a single click on the header folds the * collapsed because tool traces are supporting evidence, not the answer.
* group down to a one-line summary so it never dominates the thread. * A single click expands the exact calls when the user wants details.
*/ */
function TraceGroup({ message, animClass }: TraceGroupProps) { function TraceGroup({ message, animClass }: TraceGroupProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const lines = message.traces ?? [message.content]; const lines = message.traces ?? [message.content];
const count = lines.length; const count = lines.length;
const [open, setOpen] = useState(true); const [open, setOpen] = useState(false);
return ( return (
<div className={cn("w-full", animClass)}> <div className={cn("w-full", animClass)}>
<button <button
@ -471,7 +471,7 @@ function ReasoningBubble({ text, streaming }: ReasoningBubbleProps) {
type="button" type="button"
onClick={onToggle} onClick={onToggle}
className={cn( className={cn(
"flex w-full items-center gap-2 rounded-md px-2 py-1.5", "group flex w-full items-center gap-2 rounded-md px-2 py-1.5",
"text-xs text-muted-foreground transition-colors hover:bg-muted/45", "text-xs text-muted-foreground transition-colors hover:bg-muted/45",
streaming && "reasoning-shimmer", streaming && "reasoning-shimmer",
)} )}
@ -498,7 +498,8 @@ function ReasoningBubble({ text, streaming }: ReasoningBubbleProps) {
{open && text.length > 0 && ( {open && text.length > 0 && (
<div <div
className={cn( className={cn(
"mt-1 whitespace-pre-wrap break-words border-l border-muted-foreground/20 pl-3", "mt-1 space-y-0.5 whitespace-pre-wrap break-words border-l border-muted-foreground/20 pl-3",
"animate-in fade-in-0 slide-in-from-top-1 duration-200",
"text-[12.5px] italic leading-relaxed text-muted-foreground/85", "text-[12.5px] italic leading-relaxed text-muted-foreground/85",
)} )}
> >

View File

@ -72,11 +72,12 @@ describe("MessageBubble", () => {
render(<MessageBubble message={message} />); render(<MessageBubble message={message} />);
const toggle = screen.getByRole("button", { name: /used 2 tools/i }); const toggle = screen.getByRole("button", { name: /used 2 tools/i });
expect(screen.getByText('weather("get")')).toBeInTheDocument(); expect(screen.queryByText('weather("get")')).not.toBeInTheDocument();
expect(screen.getByText('search "hk weather"')).toBeInTheDocument(); expect(screen.queryByText('search "hk weather"')).not.toBeInTheDocument();
fireEvent.click(toggle); fireEvent.click(toggle);
expect(screen.queryByText('weather("get")')).not.toBeInTheDocument(); expect(screen.getByText('weather("get")')).toBeInTheDocument();
expect(screen.getByText('search "hk weather"')).toBeInTheDocument();
}); });
it("renders video media as an inline player", () => { it("renders video media as an inline player", () => {