| ... | ... | @@ -171,7 +171,6 @@ import { useAssistantCitationTarget, type CitationHistoryPage } from "./useAssis |
| 171 | 171 | import { |
| 172 | 172 | computeStableMessagesTimelineRows, |
| 173 | 173 | deriveMessagesTimelineRowsWithState, |
| 174 | | LIVE_ACTIVITY_ROW_ID, |
| 175 | 174 | deriveUnsettledTurnId, |
| 176 | 175 | type MessagesTimelineRowsProjection, |
| 177 | 176 | liveWorkEntryLabel, |
| ... | ... | @@ -2606,6 +2605,7 @@ function ActivityGroupTimelineRow({ |
| 2606 | 2605 | const liveWork = trailingWork.findLast(workEntryIsActiveTurnActivity) ?? trailingWork.at(-1); |
| 2607 | 2606 | const thinking = row.active && liveWork === undefined; |
| 2608 | 2607 | const iconWork = row.active ? liveWork : work.at(-1); |
| 2608 | const failed = iconWork !== undefined && workEntryDisplayIndicatesToolFailure(iconWork); |
| 2609 | 2609 | const label = row.active |
| 2610 | 2610 | ? liveWork |
| 2611 | 2611 | ? liveWorkEntryLabel(liveWork, ctx.workspaceRoot, true) |
| ... | ... | @@ -2639,20 +2639,11 @@ function ActivityGroupTimelineRow({ |
| 2639 | 2639 | if (next.kind === "message") messages.push(next.message); |
| 2640 | 2640 | } |
| 2641 | 2641 | details.push( |
| 2642 | | <ReasoningTimelineRow |
| 2642 | <ReasoningTraceBlock |
| 2643 | 2643 | key={entry.id} |
| 2644 | | disclosureAnchorKey={row.id} |
| 2645 | | row={{ |
| 2646 | | kind: "message", |
| 2647 | | id: row.active && index === row.entries.length - 1 ? LIVE_ACTIVITY_ROW_ID : entry.id, |
| 2648 | | createdAt: entry.createdAt, |
| 2649 | | message: entry.message, |
| 2650 | | reasoningMessages: messages, |
| 2651 | | durationStart: entry.createdAt, |
| 2652 | | showAssistantMeta: false, |
| 2653 | | showAssistantCopyButton: false, |
| 2654 | | assistantCopyStreaming: false, |
| 2655 | | }} |
| 2644 | messages={messages} |
| 2645 | live={row.active && index === row.entries.length - 1} |
| 2646 | showHeader={work.length > 0} |
| 2656 | 2647 | />, |
| 2657 | 2648 | ); |
| 2658 | 2649 | } |
| ... | ... | @@ -2663,6 +2654,7 @@ function ActivityGroupTimelineRow({ |
| 2663 | 2654 | <button |
| 2664 | 2655 | type="button" |
| 2665 | 2656 | className="group/live-work flex min-h-6 w-full max-w-full cursor-pointer items-center rounded-md text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/70" |
| 2657 | aria-label={failed ? `${label}, tool call failed` : undefined} |
| 2666 | 2658 | aria-expanded={row.expanded} |
| 2667 | 2659 | onClick={() => ctx.onToggleWorkGroup(row.groupId, row.id)} |
| 2668 | 2660 | > |
| ... | ... | @@ -2670,6 +2662,7 @@ function ActivityGroupTimelineRow({ |
| 2670 | 2662 | label={label} |
| 2671 | 2663 | iconName={iconWork ? workEntryIconName(iconWork) : "brain"} |
| 2672 | 2664 | toolIcon={iconWork?.toolIcon ?? iconWork?.toolSource?.icon} |
| 2665 | failed={failed} |
| 2673 | 2666 | active={row.active} |
| 2674 | 2667 | shimmer={thinking} |
| 2675 | 2668 | /> |
| ... | ... | @@ -2691,6 +2684,73 @@ function ThinkingTimelineRow() { |
| 2691 | 2684 | ); |
| 2692 | 2685 | } |
| 2693 | 2686 | |
| 2687 | /** |
| 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. |
| 2690 | * A group whose row already reads "Thought" (no visible tool) skips the header. |
| 2691 | */ |
| 2692 | function ReasoningTraceBlock({ |
| 2693 | messages, |
| 2694 | live, |
| 2695 | showHeader, |
| 2696 | }: { |
| 2697 | messages: ReadonlyArray<ChatMessage>; |
| 2698 | live: boolean; |
| 2699 | showHeader: boolean; |
| 2700 | }) { |
| 2701 | const ctx = use(TimelineRowCtx); |
| 2702 | const { isWorking, unsettledTurnId } = use(TimelineRowActivityCtx); |
| 2703 | const first = messages[0]!; |
| 2704 | const streaming = |
| 2705 | live && |
| 2706 | messages.some((reasoningMessage) => reasoningMessage.streaming) && |
| 2707 | isWorking && |
| 2708 | first.turnId !== null && |
| 2709 | first.turnId === unsettledTurnId; |
| 2710 | if ( |
| 2711 | messages.every((reasoningMessage) => reasoningMessage.text.trim().length === 0) && |
| 2712 | !streaming |
| 2713 | ) { |
| 2714 | return null; |
| 2715 | } |
| 2716 | const label = streaming ? "Thinking" : "Thought"; |
| 2717 | return ( |
| 2718 | <div className="flex flex-col"> |
| 2719 | {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"> |
| 2721 | <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" /> |
| 2723 | </span> |
| 2724 | <span |
| 2725 | ref={streaming ? observeVisibleAnimation : undefined} |
| 2726 | className="relative min-w-0 flex-1 truncate text-secondary-label" |
| 2727 | > |
| 2728 | {label} |
| 2729 | {streaming ? <ActivityShimmerOverlay>{label}</ActivityShimmerOverlay> : null} |
| 2730 | </span> |
| 2731 | </div> |
| 2732 | ) : 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> |
| 2751 | ); |
| 2752 | } |
| 2753 | |
| 2694 | 2754 | /** |
| 2695 | 2755 | * A provider's thinking trace. Collapsed by default: reasoning is context for |
| 2696 | 2756 | * the answer, not the answer. The open/closed flag lives on the list so it |
| ... | ... | @@ -2698,35 +2758,18 @@ function ThinkingTimelineRow() { |
| 2698 | 2758 | */ |
| 2699 | 2759 | const ReasoningTimelineRow = memo(function ReasoningTimelineRow({ |
| 2700 | 2760 | row, |
| 2701 | | disclosureAnchorKey = row.id, |
| 2702 | 2761 | }: { |
| 2703 | 2762 | row: Extract<TimelineRow, { kind: "message" }>; |
| 2704 | | disclosureAnchorKey?: string; |
| 2705 | 2763 | }) { |
| 2706 | 2764 | const ctx = use(TimelineRowCtx); |
| 2707 | | const { isWorking, unsettledTurnId } = use(TimelineRowActivityCtx); |
| 2708 | 2765 | const { message } = row; |
| 2709 | | const messages = row.reasoningMessages ?? [message]; |
| 2710 | | // A block left open by a crashed provider or a restarted server never gets |
| 2711 | | // its completion. Only the live turn may claim to still be thinking, so a |
| 2712 | | // settled turn cannot shimmer "Thinking" at the user forever. |
| 2713 | | const streaming = |
| 2714 | | row.id === LIVE_ACTIVITY_ROW_ID && |
| 2715 | | messages.some((reasoningMessage) => reasoningMessage.streaming) && |
| 2716 | | isWorking && |
| 2717 | | message.turnId !== null && |
| 2718 | | message.turnId === unsettledTurnId; |
| 2719 | 2766 | const expanded = ctx.expandedReasoningMessageIds.has(message.id); |
| 2720 | 2767 | const { onToggleReasoning } = ctx; |
| 2721 | 2768 | const toggle = useCallback(() => { |
| 2722 | | onToggleReasoning(message.id, !expanded, disclosureAnchorKey); |
| 2723 | | }, [expanded, message.id, disclosureAnchorKey, onToggleReasoning]); |
| 2724 | | const label = `${streaming ? "Thinking" : "Thought"}${messages.length > 1 ? ` (×${messages.length})` : ""}`; |
| 2769 | onToggleReasoning(message.id, !expanded, row.id); |
| 2770 | }, [expanded, message.id, row.id, onToggleReasoning]); |
| 2725 | 2771 | |
| 2726 | | if ( |
| 2727 | | messages.every((reasoningMessage) => reasoningMessage.text.trim().length === 0) && |
| 2728 | | !streaming |
| 2729 | | ) { |
| 2772 | if (message.text.trim().length === 0) { |
| 2730 | 2773 | return null; |
| 2731 | 2774 | } |
| 2732 | 2775 | |
| ... | ... | @@ -2742,12 +2785,8 @@ const ReasoningTimelineRow = memo(function ReasoningTimelineRow({ |
| 2742 | 2785 | <BrainIcon aria-hidden className="block size-4 shrink-0 stroke-[1.8] opacity-70" /> |
| 2743 | 2786 | </span> |
| 2744 | 2787 | <span className="flex min-w-0 flex-1 items-center gap-1.5"> |
| 2745 | | <span |
| 2746 | | ref={streaming ? observeVisibleAnimation : undefined} |
| 2747 | | className="relative min-w-0 flex-1 truncate text-secondary-label text-sm leading-relaxed" |
| 2748 | | > |
| 2749 | | {label} |
| 2750 | | {streaming ? <ActivityShimmerOverlay>{label}</ActivityShimmerOverlay> : null} |
| 2788 | <span className="relative min-w-0 flex-1 truncate text-secondary-label text-sm leading-relaxed"> |
| 2789 | Thought |
| 2751 | 2790 | </span> |
| 2752 | 2791 | <span className="flex size-4 shrink-0 items-center justify-center" aria-hidden> |
| 2753 | 2792 | <ChevronRightIcon |
| ... | ... | @@ -2760,21 +2799,18 @@ const ReasoningTimelineRow = memo(function ReasoningTimelineRow({ |
| 2760 | 2799 | </span> |
| 2761 | 2800 | </button> |
| 2762 | 2801 | {expanded ? ( |
| 2763 | | <div className="mt-1 ms-7 flex max-h-96 flex-col gap-3 overflow-auto rounded-md bg-muted/40 px-3 py-2 text-secondary-label select-text"> |
| 2764 | | {messages.map((reasoningMessage) => ( |
| 2765 | | <ChatMarkdown |
| 2766 | | key={reasoningMessage.id} |
| 2767 | | text={reasoningMessage.text} |
| 2768 | | cwd={ctx.markdownCwd} |
| 2769 | | threadRef={ctx.threadRef ?? undefined} |
| 2770 | | isStreaming={streaming && reasoningMessage.streaming} |
| 2771 | | lineBreaks |
| 2772 | | skills={ctx.skills} |
| 2773 | | headingLevelOffset={MESSAGE_HEADING_LEVEL} |
| 2774 | | onUseArtifactTemplate={ctx.onUseArtifactTemplate} |
| 2775 | | onImageExpand={ctx.onImageExpand} |
| 2776 | | /> |
| 2777 | | ))} |
| 2802 | <div className="mt-1 ms-7 flex max-h-96 flex-col gap-3 overflow-auto px-0.5 py-1 select-text"> |
| 2803 | <ChatMarkdown |
| 2804 | className="text-foreground" |
| 2805 | text={message.text} |
| 2806 | cwd={ctx.markdownCwd} |
| 2807 | threadRef={ctx.threadRef ?? undefined} |
| 2808 | lineBreaks |
| 2809 | skills={ctx.skills} |
| 2810 | headingLevelOffset={MESSAGE_HEADING_LEVEL} |
| 2811 | onUseArtifactTemplate={ctx.onUseArtifactTemplate} |
| 2812 | onImageExpand={ctx.onImageExpand} |
| 2813 | /> |
| 2778 | 2814 | </div> |
| 2779 | 2815 | ) : null} |
| 2780 | 2816 | </div> |