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({...@@ -2641,6 +2641,7 @@ function ActivityGroupTimelineRow({
2641 details.push(2641 details.push(
2642 <ReasoningTraceBlock2642 <ReasoningTraceBlock
2643 key={entry.id}2643 key={entry.id}
2644 anchorKey={row.id}
2644 messages={messages}2645 messages={messages}
2645 live={row.active && index === row.entries.length - 1}2646 live={row.active && index === row.entries.length - 1}
2646 showHeader={work.length > 0}2647 showHeader={work.length > 0}
...@@ -2667,7 +2668,7 @@ function ActivityGroupTimelineRow({...@@ -2667,7 +2668,7 @@ function ActivityGroupTimelineRow({
2667 shimmer={thinking}2668 shimmer={thinking}
2668 />2669 />
2669 </button>2670 </button>
2670 {row.expanded ? <div className="mt-2 space-y-2">{details}</div> : null}2671 {row.expanded ? <div className="mt-2">{details}</div> : null}
2671 </div>2672 </div>
2672 );2673 );
2673}2674}
...@@ -2685,15 +2686,16 @@ function ThinkingTimelineRow() {...@@ -2685,15 +2686,16 @@ function ThinkingTimelineRow() {
2685}2686}
26862687
2687/**2688/**
2688 * Thinking inside an expanded activity group: the trace is already one click2689 * Thinking inside a tool group has its own disclosure, preserved across recycling.
2689 * deep, so the text renders under its "Thought" header without another toggle.
2690 * A group whose row already reads "Thought" (no visible tool) skips the header.2690 * A group whose row already reads "Thought" (no visible tool) skips the header.
2691 */2691 */
2692function ReasoningTraceBlock({2692function ReasoningTraceBlock({
2693 anchorKey,
2693 messages,2694 messages,
2694 live,2695 live,
2695 showHeader,2696 showHeader,
2696}: {2697}: {
2698 anchorKey: string;
2697 messages: ReadonlyArray<ChatMessage>;2699 messages: ReadonlyArray<ChatMessage>;
2698 live: boolean;2700 live: boolean;
2699 showHeader: boolean;2701 showHeader: boolean;
...@@ -2701,6 +2703,7 @@ function ReasoningTraceBlock({...@@ -2701,6 +2703,7 @@ function ReasoningTraceBlock({
2701 const ctx = use(TimelineRowCtx);2703 const ctx = use(TimelineRowCtx);
2702 const { isWorking, unsettledTurnId } = use(TimelineRowActivityCtx);2704 const { isWorking, unsettledTurnId } = use(TimelineRowActivityCtx);
2703 const first = messages[0]!;2705 const first = messages[0]!;
2706 const expanded = !showHeader || ctx.expandedReasoningMessageIds.has(first.id);
2704 const streaming =2707 const streaming =
2705 live &&2708 live &&
2706 messages.some((reasoningMessage) => reasoningMessage.streaming) &&2709 messages.some((reasoningMessage) => reasoningMessage.streaming) &&
...@@ -2714,10 +2717,17 @@ function ReasoningTraceBlock({...@@ -2714,10 +2717,17 @@ function ReasoningTraceBlock({
2714 return null;2717 return null;
2715 }2718 }
2716 const label = streaming ? "Thinking" : "Thought";2719 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);
2717 return (2722 return (
2718 <div className="flex flex-col">2723 <div className="flex flex-col">
2719 {showHeader ? (2724 {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 >
2721 <span className="flex size-6 shrink-0 items-center justify-center text-icon-muted">2731 <span className="flex size-6 shrink-0 items-center justify-center text-icon-muted">
2722 <BrainIcon aria-hidden className="block size-4 shrink-0 stroke-[1.8] opacity-70" />2732 <BrainIcon aria-hidden className="block size-4 shrink-0 stroke-[1.8] opacity-70" />
2723 </span>2733 </span>
...@@ -2725,28 +2735,38 @@ function ReasoningTraceBlock({...@@ -2725,28 +2735,38 @@ function ReasoningTraceBlock({
2725 ref={streaming ? observeVisibleAnimation : undefined}2735 ref={streaming ? observeVisibleAnimation : undefined}
2726 className="relative min-w-0 flex-1 truncate text-secondary-label"2736 className="relative min-w-0 flex-1 truncate text-secondary-label"
2727 >2737 >
2728 {label}2738 {headerText}
2729 {streaming ? <ActivityShimmerOverlay>{label}</ActivityShimmerOverlay> : null}2739 {streaming ? <ActivityShimmerOverlay>{headerText}</ActivityShimmerOverlay> : null}
2730 </span>2740 </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 ))}
2731 </div>2768 </div>
2732 ) : null}2769 ) : 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>
2750 </div>2770 </div>
2751 );2771 );
2752}2772}