| ... | @@ -2641,6 +2641,7 @@ function ActivityGroupTimelineRow({ | ... | @@ -2641,6 +2641,7 @@ function ActivityGroupTimelineRow({ |
| 2641 | details.push( | 2641 | details.push( |
| 2642 | <ReasoningTraceBlock | 2642 | <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 | } |
| 2686 | | 2687 | |
| 2687 | /** | 2688 | /** |
| 2688 | * Thinking inside an expanded activity group: the trace is already one click | 2689 | * 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 | */ |
| 2692 | function ReasoningTraceBlock({ | 2692 | function 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 | } |