fix(ui): tolerate invalid message parts
This commit is contained in:
@@ -22,7 +22,7 @@ import MessageBody from './message/MessageBody';
|
||||
import type { AgentMentionInfo } from './message/types';
|
||||
import type { StreamPhase, ToolPopupContent } from './message/types';
|
||||
import { deriveMessageRole } from './message/messageRole';
|
||||
import { filterVisibleParts } from './message/partUtils';
|
||||
import { filterVisibleParts, normalizeParts } from './message/partUtils';
|
||||
import { normalizeUserDisplayParts } from './message/normalizeUserDisplayParts';
|
||||
import { flattenAssistantTextParts } from '@/lib/messages/messageText';
|
||||
import { isLikelyProviderAuthFailure, PROVIDER_AUTH_FAILURE_MESSAGE } from '@/lib/messages/providerAuthError';
|
||||
@@ -216,11 +216,12 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
|
||||
);
|
||||
|
||||
const normalizedParts = React.useMemo(() => {
|
||||
const safeParts = normalizeParts(message.parts);
|
||||
if (!isUser) {
|
||||
return message.parts;
|
||||
return safeParts;
|
||||
}
|
||||
|
||||
return normalizeUserDisplayParts(message.parts, { planModeEnabled });
|
||||
return normalizeUserDisplayParts(safeParts, { planModeEnabled });
|
||||
}, [isUser, message.parts, planModeEnabled]);
|
||||
|
||||
const previousUserMetadata = React.useMemo(() => {
|
||||
@@ -509,7 +510,7 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
|
||||
if (!isUser) {
|
||||
return undefined;
|
||||
}
|
||||
const mentionPart = message.parts.find((part) => part.type === 'agent');
|
||||
const mentionPart = normalizedParts.find((part) => part.type === 'agent');
|
||||
if (!mentionPart) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -522,7 +523,7 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
|
||||
? partWithName.source.value
|
||||
: `@${name}`;
|
||||
return { name, token: rawValue } satisfies AgentMentionInfo;
|
||||
}, [isUser, message.parts]);
|
||||
}, [isUser, normalizedParts]);
|
||||
|
||||
const shouldHideUserMessage = isUser && displayParts.length === 0;
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import { FadeInDisabledProvider } from './message/FadeInOnReveal';
|
||||
import { hasPendingUserSendAnimation, consumePendingUserSendAnimation } from '@/lib/userSendAnimation';
|
||||
import { streamPerfCount, streamPerfMeasure } from '@/stores/utils/streamDebug';
|
||||
import type { StreamPhase } from './message/types';
|
||||
import { normalizeParts } from './message/partUtils';
|
||||
|
||||
const MESSAGE_LIST_VIRTUALIZE_THRESHOLD = Number.POSITIVE_INFINITY;
|
||||
const MESSAGE_LIST_OVERSCAN = 6;
|
||||
@@ -51,7 +52,7 @@ const resolveMessageRole = (message: ChatMessageEntry): string | null => {
|
||||
|
||||
const hasCompactionPart = (message: ChatMessageEntry): boolean => {
|
||||
return message.parts.some((part) => {
|
||||
const type = (part as { type?: unknown }).type;
|
||||
const type = (part as { type?: unknown } | null | undefined)?.type;
|
||||
return type === 'compaction';
|
||||
});
|
||||
};
|
||||
@@ -75,7 +76,7 @@ const normalizeCompactionCommandMessage = (message: ChatMessageEntry): ChatMessa
|
||||
|
||||
let changedParts = false;
|
||||
const nextParts = message.parts.map((part) => {
|
||||
const type = (part as { type?: unknown }).type;
|
||||
const type = (part as { type?: unknown } | null | undefined)?.type;
|
||||
if (type !== 'compaction') {
|
||||
return part;
|
||||
}
|
||||
@@ -202,7 +203,7 @@ const getShellBridgeAssistantDetails = (message: ChatMessageEntry, expectedParen
|
||||
};
|
||||
};
|
||||
|
||||
if (part.type !== 'tool') {
|
||||
if (part?.type !== 'tool') {
|
||||
return { hide: false, details: null };
|
||||
}
|
||||
|
||||
@@ -270,9 +271,9 @@ const isSyntheticSubtaskBridgeAssistant = (message: ChatMessageEntry): { hide: b
|
||||
const onlyPart = message.parts[0] as unknown as {
|
||||
type?: unknown;
|
||||
tool?: unknown;
|
||||
};
|
||||
} | null | undefined;
|
||||
|
||||
if (onlyPart.type !== 'tool') {
|
||||
if (onlyPart?.type !== 'tool') {
|
||||
return { hide: false, taskSessionId: null };
|
||||
}
|
||||
|
||||
@@ -352,6 +353,17 @@ const withShellBridgeDetails = (message: ChatMessageEntry, details: ShellBridgeD
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeMessageParts = (message: ChatMessageEntry): ChatMessageEntry => {
|
||||
const parts = normalizeParts(message.parts);
|
||||
if (parts.length === message.parts.length) {
|
||||
return message;
|
||||
}
|
||||
return {
|
||||
...message,
|
||||
parts,
|
||||
};
|
||||
};
|
||||
|
||||
const normalizedMessageBySource = new WeakMap<ChatMessageEntry, ChatMessageEntry>();
|
||||
|
||||
const getNormalizedMessageForDisplay = (message: ChatMessageEntry): ChatMessageEntry => {
|
||||
@@ -360,7 +372,8 @@ const getNormalizedMessageForDisplay = (message: ChatMessageEntry): ChatMessageE
|
||||
return cached;
|
||||
}
|
||||
|
||||
const normalizedCompactionMessage = normalizeCompactionCommandMessage(message);
|
||||
const normalizedPartMessage = normalizeMessageParts(message);
|
||||
const normalizedCompactionMessage = normalizeCompactionCommandMessage(normalizedPartMessage);
|
||||
const filteredParts = filterSyntheticParts(normalizedCompactionMessage.parts);
|
||||
const normalized = filteredParts === normalizedCompactionMessage.parts
|
||||
? normalizedCompactionMessage
|
||||
|
||||
@@ -2,6 +2,14 @@ import type { Part } from '@opencode-ai/sdk/v2';
|
||||
|
||||
type PartWithText = Part & { text?: string; content?: string; value?: string };
|
||||
|
||||
export const isValidPart = (part: unknown): part is Part => {
|
||||
return Boolean(part && typeof part === 'object' && typeof (part as { type?: unknown }).type === 'string');
|
||||
};
|
||||
|
||||
export const normalizeParts = (parts: Part[]): Part[] => {
|
||||
return parts.filter(isValidPart);
|
||||
};
|
||||
|
||||
export const extractTextContent = (part: Part): string => {
|
||||
const partWithText = part as PartWithText;
|
||||
const rawText = partWithText.text;
|
||||
@@ -27,14 +35,15 @@ interface VisibleFilterOptions {
|
||||
|
||||
export const filterVisibleParts = (parts: Part[], options: VisibleFilterOptions = {}): Part[] => {
|
||||
const { includeReasoning = true } = options;
|
||||
const validParts = normalizeParts(parts);
|
||||
|
||||
// Check if there are any non-synthetic parts
|
||||
const hasNonSynthetic = parts.some((part) => {
|
||||
const hasNonSynthetic = validParts.some((part) => {
|
||||
const partWithSynthetic = part as PartWithSynthetic;
|
||||
return !partWithSynthetic.synthetic;
|
||||
});
|
||||
|
||||
return parts.filter((part) => {
|
||||
return validParts.filter((part) => {
|
||||
const partWithSynthetic = part as PartWithSynthetic;
|
||||
const isSynthetic = Boolean(partWithSynthetic.synthetic);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user