From d8dc418fca2f4b52296188e24b399c1a7170893b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Fri, 21 Aug 2026 18:38:36 +0200 Subject: [PATCH] fix(mobile): disable transcript view clipping Fixes KILO-APP-25 --- .../session-message-list.mounted.test.tsx | 68 +++++++++++++++++++ .../agents/session-message-list.tsx | 2 + 2 files changed, 70 insertions(+) create mode 100644 apps/mobile/src/components/agents/session-message-list.mounted.test.tsx diff --git a/apps/mobile/src/components/agents/session-message-list.mounted.test.tsx b/apps/mobile/src/components/agents/session-message-list.mounted.test.tsx new file mode 100644 index 0000000000..857c4d6583 --- /dev/null +++ b/apps/mobile/src/components/agents/session-message-list.mounted.test.tsx @@ -0,0 +1,68 @@ +/* eslint-disable typescript-eslint/no-deprecated -- react-test-renderer mounts the React Native tree without a device */ +import { createElement } from 'react'; +import TestRenderer, { act } from 'react-test-renderer'; +import { describe, expect, it, vi } from 'vitest'; + +import { SessionMessageList } from './session-message-list'; + +const flashListProps = vi.hoisted(() => ({ current: null as Record | null })); + +vi.mock('@shopify/flash-list', () => ({ + FlashList: (props: Record) => { + flashListProps.current = props; + return null; + }, +})); +vi.mock('react-native', () => ({ + AccessibilityInfo: { announceForAccessibility: vi.fn() }, + Pressable: 'Pressable', + View: 'View', +})); +vi.mock('react-native-reanimated', () => ({ + default: { View: 'AnimatedView' }, + FadeIn: { duration: () => ({}) }, + FadeOut: { duration: () => ({}) }, +})); +vi.mock('@/components/ui/icons', () => ({ ChevronDown: 'ChevronDown' })); +vi.mock('@/lib/hooks/use-theme-colors', () => ({ + useThemeColors: () => ({ foreground: 'black' }), +})); +vi.mock('@/components/agents/use-session-list-auto-scroll', () => ({ + useSessionListAutoScroll: () => ({ + isAtBottom: true, + listRef: { current: null }, + scrollToLatestAnimated: vi.fn(), + handleContentSizeChange: vi.fn(), + handleListLayout: vi.fn(), + handleScroll: vi.fn(), + handleScrollBeginDrag: vi.fn(), + handleScrollEndDrag: vi.fn(), + handleMomentumScrollBegin: vi.fn(), + handleMomentumScrollEnd: vi.fn(), + }), +})); +vi.mock('@/components/agents/session-pagination-header', () => ({ + SessionPaginationHeader: () => null, +})); + +describe('SessionMessageList', () => { + it('disables clipped subviews to avoid Android Fabric reattachment races', () => { + act(() => { + TestRenderer.create( + createElement(SessionMessageList, { + sessionId: 'session-1', + items: ['message-1'], + keyExtractor: item => item, + hasOlderMessages: false, + isLoadingOlderMessages: false, + olderMessagesError: null, + olderMessagesOmittedItemCount: 0, + onLoadOlderMessages: () => undefined, + renderItem: () => null, + }) + ); + }); + + expect(flashListProps.current?.removeClippedSubviews).toBe(false); + }); +}); diff --git a/apps/mobile/src/components/agents/session-message-list.tsx b/apps/mobile/src/components/agents/session-message-list.tsx index cde3e397f8..c9c1233dbe 100644 --- a/apps/mobile/src/components/agents/session-message-list.tsx +++ b/apps/mobile/src/components/agents/session-message-list.tsx @@ -189,6 +189,8 @@ export function SessionMessageList({ data={items} keyExtractor={keyExtractor} renderItem={renderItem} + // Android Fabric can race clipped-view reattachment with rapid transcript updates. + removeClippedSubviews={false} onScroll={handleScroll} onScrollBeginDrag={handleScrollBeginDrag} onScrollEndDrag={handleScrollEndDrag}