fix: improve sorted chat activity rendering

Show tools and reasoning live in sorted mode
Keep ambiguous text deferred until final output
Avoid duplicate reasoning outside Activity
This commit is contained in:
Bohdan Triapitsyn
2026-04-17 14:26:55 +03:00
parent 767f423500
commit 4590e1684f
3 changed files with 30 additions and 31 deletions
@@ -36,19 +36,6 @@ const getMessageFinish = (message: ChatMessageEntry): string | undefined => {
return typeof finish === 'string' ? finish : undefined; return typeof finish === 'string' ? finish : undefined;
}; };
const isAssistantMessageCompleted = (message: ChatMessageEntry): boolean => {
const info = message.info as { time?: { completed?: unknown }; status?: unknown };
const completed = info.time?.completed;
const status = info.status;
if (typeof completed !== 'number' || completed <= 0) {
return false;
}
if (typeof status === 'string') {
return status === 'completed';
}
return true;
};
const buildTurnPartRecord = ( const buildTurnPartRecord = (
turnId: string, turnId: string,
messageId: string, messageId: string,
@@ -69,6 +56,7 @@ interface ProjectActivityInput {
turnId: string; turnId: string;
assistantMessages: ChatMessageEntry[]; assistantMessages: ChatMessageEntry[];
summarySourceMessageId?: string; summarySourceMessageId?: string;
summarySourcePartId?: string;
showTextJustificationActivity: boolean; showTextJustificationActivity: boolean;
} }
@@ -84,35 +72,42 @@ export const projectTurnActivity = (input: ProjectActivityInput): ProjectActivit
let hasTools = false; let hasTools = false;
let hasReasoning = false; let hasReasoning = false;
input.assistantMessages.forEach((message) => {
message.parts.forEach((part) => {
if (part.type === 'tool') {
hasTools = true;
return;
}
if (part.type === 'reasoning' && getPartText(part)) {
hasReasoning = true;
}
});
});
const taskMessageById = new Map<string, string>(); const taskMessageById = new Map<string, string>();
const taskOrder: string[] = []; const taskOrder: string[] = [];
const partsByAfterTool = new Map<string | null, TurnActivityRecord[]>(); const partsByAfterTool = new Map<string | null, TurnActivityRecord[]>();
let currentAfterToolPartId: string | null = null; let currentAfterToolPartId: string | null = null;
input.assistantMessages.forEach((message) => { input.assistantMessages.forEach((message) => {
const messageCompleted = isAssistantMessageCompleted(message);
const finish = getMessageFinish(message); const finish = getMessageFinish(message);
const messageHasTool = message.parts.some((part) => part.type === 'tool');
message.parts.forEach((part, partIndex) => { message.parts.forEach((part, partIndex) => {
const isTool = part.type === 'tool'; const isTool = part.type === 'tool';
if (isTool) {
hasTools = true;
}
const text = part.type === 'reasoning' || part.type === 'text' const text = part.type === 'reasoning' || part.type === 'text'
? getPartText(part) ? getPartText(part)
: undefined; : undefined;
const partId = part.id ?? `${message.info.id}-part-${partIndex}-${part.type}`;
if (part.type === 'reasoning' && text) {
hasReasoning = true;
}
const toolName = isTool const toolName = isTool
? (part as { tool?: unknown }).tool ? (part as { tool?: unknown }).tool
: undefined; : undefined;
const standaloneTool = isTool && isStandaloneTool(toolName); const standaloneTool = isTool && isStandaloneTool(toolName);
if (standaloneTool) { if (standaloneTool) {
const toolPartId = part.id ?? `${message.info.id}-part-${partIndex}-${part.type}`; const toolPartId = partId;
if (!taskMessageById.has(toolPartId)) { if (!taskMessageById.has(toolPartId)) {
taskMessageById.set(toolPartId, message.info.id); taskMessageById.set(toolPartId, message.info.id);
taskOrder.push(toolPartId); taskOrder.push(toolPartId);
@@ -120,6 +115,12 @@ export const projectTurnActivity = (input: ProjectActivityInput): ProjectActivit
currentAfterToolPartId = toolPartId; currentAfterToolPartId = toolPartId;
} }
const isConfirmedSummaryText = part.type === 'text'
&& typeof text === 'string'
&& finish === 'stop'
&& input.summarySourceMessageId === message.info.id
&& input.summarySourcePartId === partId;
let kind: TurnActivityRecord['kind'] | null = null; let kind: TurnActivityRecord['kind'] | null = null;
if (isTool) { if (isTool) {
kind = 'tool'; kind = 'tool';
@@ -130,10 +131,9 @@ export const projectTurnActivity = (input: ProjectActivityInput): ProjectActivit
} else if ( } else if (
input.showTextJustificationActivity input.showTextJustificationActivity
&& part.type === 'text' && part.type === 'text'
&& messageCompleted
&& typeof finish === 'string'
&& finish !== 'stop'
&& text && text
&& !isConfirmedSummaryText
&& (messageHasTool || (typeof finish === 'string' && finish !== 'stop'))
) { ) {
kind = 'justification'; kind = 'justification';
} }
@@ -168,6 +168,7 @@ export const projectTurnRecords = (
turnId: turn.turnId, turnId: turn.turnId,
assistantMessages: turn.assistantMessages, assistantMessages: turn.assistantMessages,
summarySourceMessageId: turn.summary.sourceMessageId, summarySourceMessageId: turn.summary.sourceMessageId,
summarySourcePartId: turn.summary.sourcePartId,
showTextJustificationActivity: effectiveOptions.showTextJustificationActivity, showTextJustificationActivity: effectiveOptions.showTextJustificationActivity,
}); });
turn.activityParts = activity.activityParts; turn.activityParts = activity.activityParts;
@@ -1106,6 +1106,8 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
&& hasAnchoredActivitySegments && hasAnchoredActivitySegments
&& Boolean(toggleActivityGroup); && Boolean(toggleActivityGroup);
const shouldDeferSortedInlineText = isSortedRenderMode && !hasStopFinish;
const renderedParts = React.useMemo(() => { const renderedParts = React.useMemo(() => {
const rendered: React.ReactNode[] = []; const rendered: React.ReactNode[] = [];
@@ -1152,7 +1154,7 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
if (part.type === 'text') { if (part.type === 'text') {
const activity = activityByPart.get(part); const activity = activityByPart.get(part);
if (isSortedRenderMode && !hasStopFinish) { if (shouldDeferSortedInlineText) {
i += 1; i += 1;
continue; continue;
} }
@@ -1177,10 +1179,6 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
if (part.type === 'reasoning') { if (part.type === 'reasoning') {
const activity = activityByPart.get(part); const activity = activityByPart.get(part);
if (isSortedRenderMode && !hasStopFinish) {
i += 1;
continue;
}
if (activity?.kind === 'reasoning') { if (activity?.kind === 'reasoning') {
i += 1; i += 1;
continue; continue;
@@ -1293,7 +1291,6 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
chatRenderMode, chatRenderMode,
collapsedPreviewCount, collapsedPreviewCount,
expandedTools, expandedTools,
hasStopFinish,
isMobile, isMobile,
isActivityOwnerMessage, isActivityOwnerMessage,
isSortedRenderMode, isSortedRenderMode,
@@ -1306,6 +1303,7 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
shouldShowTool, shouldShowTool,
streamPhase, streamPhase,
showReasoningTraces, showReasoningTraces,
shouldDeferSortedInlineText,
syntaxTheme, syntaxTheme,
toggleActivityGroup, toggleActivityGroup,
turnGroupingContext, turnGroupingContext,