From 9241fed442d936990c3732c1817bc707852a4d16 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Sat, 27 Dec 2025 02:37:29 +0200 Subject: [PATCH] feat: refactor message filtering to use filterSyntheticParts for improved clarity --- .../ui/src/components/chat/MessageList.tsx | 25 ++++++++++------- packages/ui/src/lib/messages/synthetic.ts | 27 ++++++++++++++++++- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/packages/ui/src/components/chat/MessageList.tsx b/packages/ui/src/components/chat/MessageList.tsx index 33ba9244..d6d3afc7 100644 --- a/packages/ui/src/components/chat/MessageList.tsx +++ b/packages/ui/src/components/chat/MessageList.tsx @@ -5,7 +5,7 @@ import ChatMessage from './ChatMessage'; import { PermissionCard } from './PermissionCard'; import type { Permission } from '@/types/permission'; import type { AnimationHandlers, ContentChangeReason } from '@/hooks/useChatScrollManager'; -import { isFullySyntheticMessage } from '@/lib/messages/synthetic'; +import { filterSyntheticParts } from '@/lib/messages/synthetic'; import { useTurnGrouping } from './hooks/useTurnGrouping'; interface MessageListProps { @@ -40,16 +40,21 @@ const MessageList: React.FC = ({ const displayMessages = React.useMemo(() => { const seenIds = new Set(); - return messages.filter((message) => { - const messageId = message.info?.id; - if (typeof messageId === 'string') { - if (seenIds.has(messageId)) { - return false; + return messages + .filter((message) => { + const messageId = message.info?.id; + if (typeof messageId === 'string') { + if (seenIds.has(messageId)) { + return false; + } + seenIds.add(messageId); } - seenIds.add(messageId); - } - return !isFullySyntheticMessage(message.parts); - }); + return true; + }) + .map((message) => ({ + ...message, + parts: filterSyntheticParts(message.parts), + })); }, [messages]); const { getContextForMessage } = useTurnGrouping(displayMessages); diff --git a/packages/ui/src/lib/messages/synthetic.ts b/packages/ui/src/lib/messages/synthetic.ts index 3ca1b679..43dc2a67 100644 --- a/packages/ui/src/lib/messages/synthetic.ts +++ b/packages/ui/src/lib/messages/synthetic.ts @@ -1,12 +1,16 @@ import type { Part } from "@opencode-ai/sdk"; -const isSyntheticPart = (part: Part | undefined): boolean => { +export const isSyntheticPart = (part: Part | undefined): boolean => { if (!part || typeof part !== "object") { return false; } return Boolean((part as { synthetic?: boolean }).synthetic); }; +/** + * Checks if a message consists entirely of synthetic parts. + * Used for status/completion logic (not display filtering). + */ export const isFullySyntheticMessage = (parts: Part[] | undefined): boolean => { if (!Array.isArray(parts) || parts.length === 0) { return false; @@ -14,3 +18,24 @@ export const isFullySyntheticMessage = (parts: Part[] | undefined): boolean => { return parts.every((part) => isSyntheticPart(part)); }; + +/** + * Filters out synthetic parts from a message, but only if there are + * non-synthetic parts present. If all parts are synthetic, returns + * them as-is so the message can still be displayed. + */ +export const filterSyntheticParts = (parts: Part[] | undefined): Part[] => { + if (!Array.isArray(parts) || parts.length === 0) { + return []; + } + + const hasNonSynthetic = parts.some((part) => !isSyntheticPart(part)); + + // If there are non-synthetic parts, filter out synthetic ones + if (hasNonSynthetic) { + return parts.filter((part) => !isSyntheticPart(part)); + } + + // If all parts are synthetic, return them all (so message is displayed) + return parts; +};