authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-09-17 17:34:19-03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-17 13:34:19-07:00
log592021f008010ee947c326df0452d093ead8aeb1
treec1a48c39e74a7a513d7ab6a6c51ee6386182d8f5
parent4749035bda13b4b6260499caedbc0d69a2f60e6f
signature Signed by PGP key B5690EEEBB952194

fix(web): collapse thoughts within tool groups (#12302)

Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com> Co-authored-by: Julius Marminge <julius0216@outlook.com>

1 files changed, 43 insertions(+), 23 deletions(-)

apps/web/src/components/chat/MessagesTimeline.tsx+43-23
......@@ -2641,6 +2641,7 @@ function ActivityGroupTimelineRow({
26412641 details.push(
26422642 <ReasoningTraceBlock
26432643 key={entry.id}
2644 anchorKey={row.id}
26442645 messages={messages}
26452646 live={row.active && index === row.entries.length - 1}
26462647 showHeader={work.length > 0}
......@@ -2667,7 +2668,7 @@ function ActivityGroupTimelineRow({
26672668 shimmer={thinking}
26682669 />
26692670 </button>
2670 {row.expanded ? <div className="mt-2 space-y-2">{details}</div> : null}
2671 {row.expanded ? <div className="mt-2">{details}</div> : null}
26712672 </div>
26722673 );
26732674}
......@@ -2685,15 +2686,16 @@ function ThinkingTimelineRow() {
26852686}
26862687
26872688/**
2688 * Thinking inside an expanded activity group: the trace is already one click
2689 * deep, so the text renders under its "Thought" header without another toggle.
2689 * Thinking inside a tool group has its own disclosure, preserved across recycling.
26902690 * A group whose row already reads "Thought" (no visible tool) skips the header.
26912691 */
26922692function ReasoningTraceBlock({
2693 anchorKey,
26932694 messages,
26942695 live,
26952696 showHeader,
26962697}: {
2698 anchorKey: string;
26972699 messages: ReadonlyArray<ChatMessage>;
26982700 live: boolean;
26992701 showHeader: boolean;
......@@ -2701,6 +2703,7 @@ function ReasoningTraceBlock({
27012703 const ctx = use(TimelineRowCtx);
27022704 const { isWorking, unsettledTurnId } = use(TimelineRowActivityCtx);
27032705 const first = messages[0]!;
2706 const expanded = !showHeader || ctx.expandedReasoningMessageIds.has(first.id);
27042707 const streaming =
27052708 live &&
27062709 messages.some((reasoningMessage) => reasoningMessage.streaming) &&
......@@ -2714,10 +2717,17 @@ function ReasoningTraceBlock({
27142717 return null;
27152718 }
27162719 const label = streaming ? "Thinking" : "Thought";
2720 const collapsedPreview = messages.find((message) => message.text.trim().length > 0)?.text.trim();
2721 const headerText = expanded ? label : (collapsedPreview ?? label);
27172722 return (
27182723 <div className="flex flex-col">
27192724 {showHeader ? (
2720 <div className="flex min-h-6 select-none items-center gap-1.5 px-0.5 py-0.5 text-sm leading-relaxed">
2725 <button
2726 type="button"
2727 aria-expanded={expanded}
2728 onClick={() => ctx.onToggleReasoning(first.id, !expanded, anchorKey)}
2729 className="flex min-h-6 cursor-pointer select-none items-center gap-1.5 rounded-md px-0.5 text-start text-sm leading-relaxed transition-colors hover:bg-accent/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/70"
2730 >
27212731 <span className="flex size-6 shrink-0 items-center justify-center text-icon-muted">
27222732 <BrainIcon aria-hidden className="block size-4 shrink-0 stroke-[1.8] opacity-70" />
27232733 </span>
......@@ -2725,28 +2735,38 @@ function ReasoningTraceBlock({
27252735 ref={streaming ? observeVisibleAnimation : undefined}
27262736 className="relative min-w-0 flex-1 truncate text-secondary-label"
27272737 >
2728 {label}
2729 {streaming ? <ActivityShimmerOverlay>{label}</ActivityShimmerOverlay> : null}
2738 {headerText}
2739 {streaming ? <ActivityShimmerOverlay>{headerText}</ActivityShimmerOverlay> : null}
27302740 </span>
2741 <span className="flex size-4 shrink-0 items-center justify-center" aria-hidden>
2742 <ChevronRightIcon
2743 className={cn(
2744 "size-3 shrink-0 text-icon-muted opacity-70 transition-transform duration-200",
2745 expanded && "rotate-90",
2746 )}
2747 />
2748 </span>
2749 </button>
2750 ) : null}
2751 {expanded ? (
2752 <div className="ms-7 flex max-h-96 flex-col gap-3 overflow-auto px-0.5 py-1 select-text">
2753 {messages.map((reasoningMessage) => (
2754 <ChatMarkdown
2755 key={reasoningMessage.id}
2756 className="text-foreground"
2757 text={reasoningMessage.text}
2758 cwd={ctx.markdownCwd}
2759 threadRef={ctx.threadRef ?? undefined}
2760 isStreaming={streaming && reasoningMessage.streaming}
2761 lineBreaks
2762 skills={ctx.skills}
2763 headingLevelOffset={MESSAGE_HEADING_LEVEL}
2764 onUseArtifactTemplate={ctx.onUseArtifactTemplate}
2765 onImageExpand={ctx.onImageExpand}
2766 />
2767 ))}
27312768 </div>
27322769 ) : null}
2733 <div className="ms-7 flex max-h-96 flex-col gap-3 overflow-auto px-0.5 py-1 select-text">
2734 {messages.map((reasoningMessage) => (
2735 <ChatMarkdown
2736 key={reasoningMessage.id}
2737 className="text-foreground"
2738 text={reasoningMessage.text}
2739 cwd={ctx.markdownCwd}
2740 threadRef={ctx.threadRef ?? undefined}
2741 isStreaming={streaming && reasoningMessage.streaming}
2742 lineBreaks
2743 skills={ctx.skills}
2744 headingLevelOffset={MESSAGE_HEADING_LEVEL}
2745 onUseArtifactTemplate={ctx.onUseArtifactTemplate}
2746 onImageExpand={ctx.onImageExpand}
2747 />
2748 ))}
2749 </div>
27502770 </div>
27512771 );
27522772}