From 20fc151907e4aab63a0149dadd6aee92425b2659 Mon Sep 17 00:00:00 2001 From: Evgeny Shurakov Date: Wed, 4 Feb 2026 09:40:08 +0100 Subject: [PATCH] App Builder - Add session-based message pagination Migrated from Kilo-Org/kilocode-backend#4700 --- src/components/app-builder/AppBuilderChat.tsx | 45 +++++++++---- .../app-builder/utils/filterMessages.ts | 65 ++++++++++++++++++- 2 files changed, 96 insertions(+), 14 deletions(-) diff --git a/src/components/app-builder/AppBuilderChat.tsx b/src/components/app-builder/AppBuilderChat.tsx index 6f02e54f4d..27c3149c75 100644 --- a/src/components/app-builder/AppBuilderChat.tsx +++ b/src/components/app-builder/AppBuilderChat.tsx @@ -17,7 +17,12 @@ import { Button } from '@/components/ui/button'; import { MessageContent } from '@/components/cloud-agent/MessageContent'; import { TypingIndicator } from '@/components/cloud-agent/TypingIndicator'; import type { CloudMessage } from '@/components/cloud-agent/types'; -import { filterAppBuilderMessages } from './utils/filterMessages'; +import { + filterAppBuilderMessages, + paginateMessages, + getMessageRole, + DEFAULT_VISIBLE_SESSIONS, +} from './utils/filterMessages'; import { PromptInput } from '@/components/app-builder/PromptInput'; import { useProject } from './ProjectSession'; import type { Images } from '@/lib/images-schema'; @@ -34,15 +39,6 @@ type AppBuilderChatProps = { organizationId?: string; }; -/** - * Convert CloudMessage to display format with role - */ -function getMessageRole(msg: CloudMessage): 'user' | 'assistant' | 'system' { - // user_feedback messages should display as user messages - if (msg.say === 'user_feedback') return 'user'; - return msg.type; -} - const isDev = process.env.NODE_ENV === 'development'; /** @@ -197,8 +193,14 @@ export function AppBuilderChat({ onNewProject, organizationId }: AppBuilderChatP const [messageUuid, setMessageUuid] = useState(() => crypto.randomUUID()); const [selectedModel, setSelectedModel] = useState(projectModel ?? ''); const [hasImages, setHasImages] = useState(false); + const [visibleSessionCount, setVisibleSessionCount] = useState(DEFAULT_VISIBLE_SESSIONS); const trpc = useTRPC(); + // Reset pagination when project/manager changes + useEffect(() => { + setVisibleSessionCount(DEFAULT_VISIBLE_SESSIONS); + }, [manager]); + // Fetch eligibility to check if user can use App Builder const personalEligibilityQuery = useQuery({ ...trpc.appBuilder.checkEligibility.queryOptions(), @@ -309,12 +311,18 @@ export function AppBuilderChat({ onNewProject, organizationId }: AppBuilderChatP // Filter messages to show only important ones for cleaner UX const filteredMessages = useMemo(() => filterAppBuilderMessages(messages), [messages]); + // Apply session-based pagination to avoid overwhelming UI with long histories + const { visibleMessages, hasOlderMessages } = useMemo( + () => paginateMessages(filteredMessages, visibleSessionCount), + [filteredMessages, visibleSessionCount] + ); + // Split messages into static (complete) and dynamic (streaming) const { staticMessages, dynamicMessages } = useMemo(() => { const staticMsgs: CloudMessage[] = []; const dynamicMsgs: CloudMessage[] = []; - filteredMessages.forEach(msg => { + visibleMessages.forEach(msg => { if (msg.partial) { dynamicMsgs.push(msg); } else { @@ -323,7 +331,7 @@ export function AppBuilderChat({ onNewProject, organizationId }: AppBuilderChatP }); return { staticMessages: staticMsgs, dynamicMessages: dynamicMsgs }; - }, [filteredMessages]); + }, [visibleMessages]); // Auto-scroll effect useEffect(() => { @@ -413,7 +421,7 @@ export function AppBuilderChat({ onNewProject, organizationId }: AppBuilderChatP onScroll={handleScroll} className="absolute inset-0 overflow-x-hidden overflow-y-auto p-4" > - {filteredMessages.length === 0 ? ( + {visibleMessages.length === 0 ? (

Start building your app

@@ -422,6 +430,17 @@ export function AppBuilderChat({ onNewProject, organizationId }: AppBuilderChatP
) : ( <> + {hasOlderMessages && ( +
+ +
+ )} {isStreaming && dynamicMessages.length === 0 && } diff --git a/src/components/app-builder/utils/filterMessages.ts b/src/components/app-builder/utils/filterMessages.ts index 703e46946a..1cddba83f6 100644 --- a/src/components/app-builder/utils/filterMessages.ts +++ b/src/components/app-builder/utils/filterMessages.ts @@ -1,12 +1,36 @@ /** - * App Builder Message Filtering + * App Builder Message Filtering and Pagination * * Filters verbose agent messages to show only important, user-facing content. + * Also provides session-based pagination to avoid overwhelming the UI with long histories. * Designed for a clean "vibe" app builder experience. */ import type { CloudMessage } from '@/components/cloud-agent/types'; +/** + * Default number of "sessions" to show initially. + * A session is a user message plus all assistant/system messages that follow. + */ +export const DEFAULT_VISIBLE_SESSIONS = 2; + +/** + * Result type for paginated messages + */ +export type PaginatedMessagesResult = { + visibleMessages: CloudMessage[]; + hasOlderMessages: boolean; +}; + +/** + * Determine the display role of a message. + * user_feedback messages should display as user messages. + */ +export function getMessageRole(msg: CloudMessage): 'user' | 'assistant' | 'system' { + if (msg.say === 'user_feedback') return 'user'; + return msg.type; +} + /** * Say types to completely hide from the chat */ @@ -137,3 +161,42 @@ export function filterAppBuilderMessages(messages: CloudMessage[]): CloudMessage const deduplicated = deduplicateMessages(visibleMessages); return deduplicated; } + +/** + * Paginate messages by "sessions" - each session is a user message + * plus all assistant/system messages that follow until the next user message. + * + * This ensures we never cut a conversation turn in the middle. + * + * @param messages - Array of filtered CloudMessages + * @param visibleSessionCount - Number of user sessions to show (default: DEFAULT_VISIBLE_SESSIONS) + * @returns Visible messages and whether there are older messages to load + */ +export function paginateMessages( + messages: CloudMessage[], + visibleSessionCount = DEFAULT_VISIBLE_SESSIONS +): PaginatedMessagesResult { + // Find indices of all user messages (session boundaries) + const userMessageIndices: number[] = []; + for (let i = 0; i < messages.length; i++) { + if (getMessageRole(messages[i]) === 'user') { + userMessageIndices.push(i); + } + } + + // If we have fewer sessions than threshold, show all + if (userMessageIndices.length <= visibleSessionCount) { + return { + visibleMessages: messages, + hasOlderMessages: false, + }; + } + + // Find cutoff: start from the Nth-to-last user message + const cutoffIndex = userMessageIndices[userMessageIndices.length - visibleSessionCount]; + + return { + visibleMessages: messages.slice(cutoffIndex), + hasOlderMessages: true, + }; +}