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