| ... | @@ -171,7 +171,6 @@ import { useAssistantCitationTarget, type CitationHistoryPage } from "./useAssis | ... | @@ -171,7 +171,6 @@ import { useAssistantCitationTarget, type CitationHistoryPage } from "./useAssis |
| 171 | import { | 171 | import { |
| 172 | computeStableMessagesTimelineRows, | 172 | computeStableMessagesTimelineRows, |
| 173 | deriveMessagesTimelineRowsWithState, | 173 | deriveMessagesTimelineRowsWithState, |
| 174 | LIVE_ACTIVITY_ROW_ID, | | |
| 175 | deriveUnsettledTurnId, | 174 | deriveUnsettledTurnId, |
| 176 | type MessagesTimelineRowsProjection, | 175 | type MessagesTimelineRowsProjection, |
| 177 | liveWorkEntryLabel, | 176 | liveWorkEntryLabel, |
| ... | @@ -2606,6 +2605,7 @@ function ActivityGroupTimelineRow({ | ... | @@ -2606,6 +2605,7 @@ function ActivityGroupTimelineRow({ |
| 2606 | const liveWork = trailingWork.findLast(workEntryIsActiveTurnActivity) ?? trailingWork.at(-1); | 2605 | const liveWork = trailingWork.findLast(workEntryIsActiveTurnActivity) ?? trailingWork.at(-1); |
| 2607 | const thinking = row.active && liveWork === undefined; | 2606 | const thinking = row.active && liveWork === undefined; |
| 2608 | const iconWork = row.active ? liveWork : work.at(-1); | 2607 | const iconWork = row.active ? liveWork : work.at(-1); |
| | 2608 | const failed = iconWork !== undefined && workEntryDisplayIndicatesToolFailure(iconWork); |
| 2609 | const label = row.active | 2609 | const label = row.active |
| 2610 | ? liveWork | 2610 | ? liveWork |
| 2611 | ? liveWorkEntryLabel(liveWork, ctx.workspaceRoot, true) | 2611 | ? liveWorkEntryLabel(liveWork, ctx.workspaceRoot, true) |
| ... | @@ -2639,20 +2639,11 @@ function ActivityGroupTimelineRow({ | ... | @@ -2639,20 +2639,11 @@ function ActivityGroupTimelineRow({ |
| 2639 | if (next.kind === "message") messages.push(next.message); | 2639 | if (next.kind === "message") messages.push(next.message); |
| 2640 | } | 2640 | } |
| 2641 | details.push( | 2641 | details.push( |
| 2642 | <ReasoningTimelineRow | 2642 | <ReasoningTraceBlock |
| 2643 | key={entry.id} | 2643 | key={entry.id} |
| 2644 | disclosureAnchorKey={row.id} | 2644 | messages={messages} |
| 2645 | row={{ | 2645 | live={row.active && index === row.entries.length - 1} |
| 2646 | kind: "message", | 2646 | showHeader={work.length > 0} |
| 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 | }} | | |
| 2656 | />, | 2647 | />, |
| 2657 | ); | 2648 | ); |
| 2658 | } | 2649 | } |
| ... | @@ -2663,6 +2654,7 @@ function ActivityGroupTimelineRow({ | ... | @@ -2663,6 +2654,7 @@ function ActivityGroupTimelineRow({ |
| 2663 | <button | 2654 | <button |
| 2664 | type="button" | 2655 | type="button" |
| 2665 | 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" | 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 | aria-expanded={row.expanded} | 2658 | aria-expanded={row.expanded} |
| 2667 | onClick={() => ctx.onToggleWorkGroup(row.groupId, row.id)} | 2659 | onClick={() => ctx.onToggleWorkGroup(row.groupId, row.id)} |
| 2668 | > | 2660 | > |
| ... | @@ -2670,6 +2662,7 @@ function ActivityGroupTimelineRow({ | ... | @@ -2670,6 +2662,7 @@ function ActivityGroupTimelineRow({ |
| 2670 | label={label} | 2662 | label={label} |
| 2671 | iconName={iconWork ? workEntryIconName(iconWork) : "brain"} | 2663 | iconName={iconWork ? workEntryIconName(iconWork) : "brain"} |
| 2672 | toolIcon={iconWork?.toolIcon ?? iconWork?.toolSource?.icon} | 2664 | toolIcon={iconWork?.toolIcon ?? iconWork?.toolSource?.icon} |
| | 2665 | failed={failed} |
| 2673 | active={row.active} | 2666 | active={row.active} |
| 2674 | shimmer={thinking} | 2667 | shimmer={thinking} |
| 2675 | /> | 2668 | /> |
| ... | @@ -2691,6 +2684,73 @@ function ThinkingTimelineRow() { | ... | @@ -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 | * A provider's thinking trace. Collapsed by default: reasoning is context for | 2755 | * A provider's thinking trace. Collapsed by default: reasoning is context for |
| 2696 | * the answer, not the answer. The open/closed flag lives on the list so it | 2756 | * the answer, not the answer. The open/closed flag lives on the list so it |
| ... | @@ -2698,35 +2758,18 @@ function ThinkingTimelineRow() { | ... | @@ -2698,35 +2758,18 @@ function ThinkingTimelineRow() { |
| 2698 | */ | 2758 | */ |
| 2699 | const ReasoningTimelineRow = memo(function ReasoningTimelineRow({ | 2759 | const ReasoningTimelineRow = memo(function ReasoningTimelineRow({ |
| 2700 | row, | 2760 | row, |
| 2701 | disclosureAnchorKey = row.id, | | |
| 2702 | }: { | 2761 | }: { |
| 2703 | row: Extract<TimelineRow, { kind: "message" }>; | 2762 | row: Extract<TimelineRow, { kind: "message" }>; |
| 2704 | disclosureAnchorKey?: string; | | |
| 2705 | }) { | 2763 | }) { |
| 2706 | const ctx = use(TimelineRowCtx); | 2764 | const ctx = use(TimelineRowCtx); |
| 2707 | const { isWorking, unsettledTurnId } = use(TimelineRowActivityCtx); | | |
| 2708 | const { message } = row; | 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 | const expanded = ctx.expandedReasoningMessageIds.has(message.id); | 2766 | const expanded = ctx.expandedReasoningMessageIds.has(message.id); |
| 2720 | const { onToggleReasoning } = ctx; | 2767 | const { onToggleReasoning } = ctx; |
| 2721 | const toggle = useCallback(() => { | 2768 | const toggle = useCallback(() => { |
| 2722 | onToggleReasoning(message.id, !expanded, disclosureAnchorKey); | 2769 | onToggleReasoning(message.id, !expanded, row.id); |
| 2723 | }, [expanded, message.id, disclosureAnchorKey, onToggleReasoning]); | 2770 | }, [expanded, message.id, row.id, onToggleReasoning]); |
| 2724 | const label = `${streaming ? "Thinking" : "Thought"}${messages.length > 1 ? ` (×${messages.length})` : ""}`; | | |
| 2725 | | 2771 | |
| 2726 | if ( | 2772 | if (message.text.trim().length === 0) { |
| 2727 | messages.every((reasoningMessage) => reasoningMessage.text.trim().length === 0) && | | |
| 2728 | !streaming | | |
| 2729 | ) { | | |
| 2730 | return null; | 2773 | return null; |
| 2731 | } | 2774 | } |
| 2732 | | 2775 | |
| ... | @@ -2742,12 +2785,8 @@ const ReasoningTimelineRow = memo(function ReasoningTimelineRow({ | ... | @@ -2742,12 +2785,8 @@ const ReasoningTimelineRow = memo(function ReasoningTimelineRow({ |
| 2742 | <BrainIcon aria-hidden className="block size-4 shrink-0 stroke-[1.8] opacity-70" /> | 2785 | <BrainIcon aria-hidden className="block size-4 shrink-0 stroke-[1.8] opacity-70" /> |
| 2743 | </span> | 2786 | </span> |
| 2744 | <span className="flex min-w-0 flex-1 items-center gap-1.5"> | 2787 | <span className="flex min-w-0 flex-1 items-center gap-1.5"> |
| 2745 | <span | 2788 | <span className="relative min-w-0 flex-1 truncate text-secondary-label text-sm leading-relaxed"> |
| 2746 | ref={streaming ? observeVisibleAnimation : undefined} | 2789 | Thought |
| 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} | | |
| 2751 | </span> | 2790 | </span> |
| 2752 | <span className="flex size-4 shrink-0 items-center justify-center" aria-hidden> | 2791 | <span className="flex size-4 shrink-0 items-center justify-center" aria-hidden> |
| 2753 | <ChevronRightIcon | 2792 | <ChevronRightIcon |
| ... | @@ -2760,21 +2799,18 @@ const ReasoningTimelineRow = memo(function ReasoningTimelineRow({ | ... | @@ -2760,21 +2799,18 @@ const ReasoningTimelineRow = memo(function ReasoningTimelineRow({ |
| 2760 | </span> | 2799 | </span> |
| 2761 | </button> | 2800 | </button> |
| 2762 | {expanded ? ( | 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"> | 2802 | <div className="mt-1 ms-7 flex max-h-96 flex-col gap-3 overflow-auto px-0.5 py-1 select-text"> |
| 2764 | {messages.map((reasoningMessage) => ( | 2803 | <ChatMarkdown |
| 2765 | <ChatMarkdown | 2804 | className="text-foreground" |
| 2766 | key={reasoningMessage.id} | 2805 | text={message.text} |
| 2767 | text={reasoningMessage.text} | 2806 | cwd={ctx.markdownCwd} |
| 2768 | cwd={ctx.markdownCwd} | 2807 | threadRef={ctx.threadRef ?? undefined} |
| 2769 | threadRef={ctx.threadRef ?? undefined} | 2808 | lineBreaks |
| 2770 | isStreaming={streaming && reasoningMessage.streaming} | 2809 | skills={ctx.skills} |
| 2771 | lineBreaks | 2810 | headingLevelOffset={MESSAGE_HEADING_LEVEL} |
| 2772 | skills={ctx.skills} | 2811 | onUseArtifactTemplate={ctx.onUseArtifactTemplate} |
| 2773 | headingLevelOffset={MESSAGE_HEADING_LEVEL} | 2812 | onImageExpand={ctx.onImageExpand} |
| 2774 | onUseArtifactTemplate={ctx.onUseArtifactTemplate} | 2813 | /> |
| 2775 | onImageExpand={ctx.onImageExpand} | | |
| 2776 | /> | | |
| 2777 | ))} | | |
| 2778 | </div> | 2814 | </div> |
| 2779 | ) : null} | 2815 | ) : null} |
| 2780 | </div> | 2816 | </div> |