fix: Make thinking/reasoning blocks display consistently and make the show justification setting work. (#332)
This commit is contained in:
@@ -4,6 +4,7 @@ import type { Part } from '@opencode-ai/sdk/v2';
|
||||
import UserTextPart from './parts/UserTextPart';
|
||||
import ToolPart from './parts/ToolPart';
|
||||
import ProgressiveGroup from './parts/ProgressiveGroup';
|
||||
import ReasoningPart from './parts/ReasoningPart';
|
||||
import { MessageFilesDisplay } from '../FileAttachment';
|
||||
import type { ToolPart as ToolPartType } from '@opencode-ai/sdk/v2';
|
||||
import type { StreamPhase, ToolPopupContent, AgentMentionInfo } from './types';
|
||||
@@ -556,8 +557,11 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
|
||||
const visibleActivityPartsForTurn = React.useMemo(() => {
|
||||
if (!turnGroupingContext) return [];
|
||||
|
||||
// Filter out reasoning if showReasoningTraces is off.
|
||||
// Justification parts are already filtered at the source (useTurnGrouping)
|
||||
// based on showTextJustificationActivity, so we keep them here.
|
||||
const base = !showReasoningTraces
|
||||
? activityPartsForTurn.filter((activity) => activity.kind === 'tool')
|
||||
? activityPartsForTurn.filter((activity) => activity.kind !== 'reasoning')
|
||||
: activityPartsForTurn;
|
||||
|
||||
// Tools rendered standalone are excluded from Activity group.
|
||||
@@ -581,10 +585,18 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
|
||||
(segment) => segment.afterToolPartId !== null
|
||||
);
|
||||
|
||||
if (visibleActivityPartsForTurn.length > 1 || (hasTaskSplitSegments && visibleActivityPartsForTurn.length > 0)) {
|
||||
const hasReasoningActivity = visibleActivityPartsForTurn.some(
|
||||
(activity) => activity.kind === 'reasoning'
|
||||
);
|
||||
|
||||
if (
|
||||
visibleActivityPartsForTurn.length > 1 ||
|
||||
(hasTaskSplitSegments && visibleActivityPartsForTurn.length > 0) ||
|
||||
hasReasoningActivity
|
||||
) {
|
||||
setHasEverHadMultipleVisibleActivities(true);
|
||||
}
|
||||
}, [turnGroupingContext, visibleActivityPartsForTurn.length]);
|
||||
}, [turnGroupingContext, visibleActivityPartsForTurn]);
|
||||
|
||||
const shouldShowActivityGroup = Boolean(turnGroupingContext && hasEverHadMultipleVisibleActivities);
|
||||
|
||||
@@ -611,8 +623,11 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
|
||||
activityGroupSegmentsForMessage
|
||||
.filter((segment) => (segment.afterToolPartId ?? null) === afterToolPartId)
|
||||
.forEach((segment) => {
|
||||
// Filter out reasoning if showReasoningTraces is off.
|
||||
// Justification parts are already filtered at the source (useTurnGrouping)
|
||||
// based on showTextJustificationActivity, so we keep them here.
|
||||
const visibleSegmentParts = !showReasoningTraces
|
||||
? segment.parts.filter((activity) => activity.kind === 'tool')
|
||||
? segment.parts.filter((activity) => activity.kind !== 'reasoning')
|
||||
: segment.parts;
|
||||
|
||||
if (visibleSegmentParts.length === 0) {
|
||||
@@ -638,6 +653,8 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
|
||||
};
|
||||
|
||||
// Activity groups and standalone tasks are interleaved in message order.
|
||||
// Note: Reasoning parts are rendered in the visibleParts.forEach loop below
|
||||
// when Activity group isn't showing, to maintain proper ordering with tools.
|
||||
renderActivitySegments(null);
|
||||
|
||||
standaloneToolParts.forEach((standaloneToolPart) => {
|
||||
@@ -715,6 +732,23 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
|
||||
|
||||
element = toolElement;
|
||||
endTime = isFinalized && typeof time?.end === 'number' ? time.end : null;
|
||||
} else if (activity.kind === 'reasoning' && showReasoningTraces) {
|
||||
// Fallback rendering for reasoning when Activity group isn't shown
|
||||
const time = (part as { time?: { end?: number | null | undefined } | null | undefined }).time;
|
||||
const partEndTime = typeof time?.end === 'number' ? time.end : null;
|
||||
|
||||
const reasoningElement = (
|
||||
<FadeInOnReveal key={`reasoning-${activity.id}`}>
|
||||
<ReasoningPart
|
||||
part={part}
|
||||
messageId={messageId}
|
||||
onContentChange={onContentChange}
|
||||
/>
|
||||
</FadeInOnReveal>
|
||||
);
|
||||
|
||||
element = reasoningElement;
|
||||
endTime = partEndTime;
|
||||
}
|
||||
|
||||
if (element) {
|
||||
@@ -752,6 +786,7 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
|
||||
expandedTools,
|
||||
isMobile,
|
||||
isToolFinalized,
|
||||
messageId,
|
||||
onContentChange,
|
||||
onShowPopup,
|
||||
onToggleTool,
|
||||
|
||||
@@ -33,8 +33,8 @@ const JustificationBlock: React.FC<JustificationBlockProps> = ({
|
||||
const rawText = partWithText.text || partWithText.content || '';
|
||||
const textContent = React.useMemo(() => cleanJustificationText(rawText), [rawText]);
|
||||
|
||||
const timeInfo = 'time' in part ? (part.time as { start: number; end?: number }) : null;
|
||||
if (!timeInfo?.end) {
|
||||
// Don't render if there's no text content
|
||||
if (!textContent || textContent.trim().length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -160,8 +160,9 @@ const ReasoningPart: React.FC<ReasoningPartProps> = ({
|
||||
const rawText = partWithText.text || partWithText.content || '';
|
||||
const textContent = React.useMemo(() => cleanReasoningText(rawText), [rawText]);
|
||||
|
||||
const timeInfo = 'time' in part ? (part.time as { start: number; end?: number }) : null;
|
||||
if (!timeInfo?.end) {
|
||||
// Show reasoning even if time.end isn't set yet (during streaming)
|
||||
// Only hide if there's no text content
|
||||
if (!textContent || textContent.trim().length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { toolDisplayStyles } from '@/lib/typography';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||
import { useSessionStore } from '@/stores/useSessionStore';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||
import type { ContentChangeReason } from '@/hooks/useChatScrollManager';
|
||||
|
||||
@@ -1067,6 +1068,22 @@ const ToolPart: React.FC<ToolPartProps> = ({ part, isExpanded, onToggle, syntaxT
|
||||
const diffStats = (part.tool === 'edit' || part.tool === 'multiedit' || part.tool === 'apply_patch') ? parseDiffStats(metadata) : null;
|
||||
const description = getToolDescription(part, state, isMobile, currentDirectory);
|
||||
const displayName = getToolMetadata(part.tool).displayName;
|
||||
|
||||
// Get justification text (tool title/description) when setting is enabled
|
||||
const showTextJustificationActivity = useUIStore((state) => state.showTextJustificationActivity);
|
||||
const justificationText = React.useMemo(() => {
|
||||
if (!showTextJustificationActivity) return null;
|
||||
// Get title or description from state - this is the "yapping" text like "Shows system information"
|
||||
const title = (stateWithData as { title?: string }).title;
|
||||
if (typeof title === 'string' && title.trim().length > 0) {
|
||||
return title;
|
||||
}
|
||||
const inputDesc = input?.description;
|
||||
if (typeof inputDesc === 'string' && inputDesc.trim().length > 0) {
|
||||
return inputDesc;
|
||||
}
|
||||
return null;
|
||||
}, [showTextJustificationActivity, stateWithData, input]);
|
||||
|
||||
const runtime = React.useContext(RuntimeAPIContext);
|
||||
|
||||
@@ -1147,7 +1164,12 @@ const ToolPart: React.FC<ToolPartProps> = ({ part, isExpanded, onToggle, syntaxT
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 flex-1 min-w-0 typography-meta" style={{ color: 'var(--tools-description)' }}>
|
||||
{description && (
|
||||
{justificationText && (
|
||||
<span className={cn("truncate italic", isMobile && "max-w-[120px]")} style={{ color: 'var(--tools-description)', opacity: 0.8 }}>
|
||||
{justificationText}
|
||||
</span>
|
||||
)}
|
||||
{!justificationText && description && (
|
||||
<span className={cn("truncate", isMobile && "max-w-[120px]")}>
|
||||
{description}
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user