diff --git a/mobile/src/session/MobileAgentWorkingIndicator.tsx b/mobile/src/session/MobileAgentWorkingIndicator.tsx deleted file mode 100644 index cb766b0be51..00000000000 --- a/mobile/src/session/MobileAgentWorkingIndicator.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { useEffect, useRef } from 'react' -import { Animated, StyleSheet, Text, View } from 'react-native' -import { colors, spacing, typography } from '../theme/mobile-theme' - -/** Animated three-dot "agent is working" row, shown while the active agent is - * still producing a reply. Pure presentation — visibility is the caller's call. */ -export function MobileAgentWorkingIndicator(): React.JSX.Element { - const dots = [ - useRef(new Animated.Value(0.3)).current, - useRef(new Animated.Value(0.3)).current, - useRef(new Animated.Value(0.3)).current - ] - - useEffect(() => { - const animations = dots.map((dot, i) => - Animated.loop( - Animated.sequence([ - Animated.delay(i * 160), - Animated.timing(dot, { toValue: 1, duration: 320, useNativeDriver: true }), - Animated.timing(dot, { toValue: 0.3, duration: 320, useNativeDriver: true }) - ]) - ) - ) - animations.forEach((a) => a.start()) - return () => animations.forEach((a) => a.stop()) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - - return ( - - Agent is working - - {dots.map((dot, i) => ( - - ))} - - - ) -} - -const styles = StyleSheet.create({ - row: { - flexDirection: 'row', - alignItems: 'center', - gap: spacing.sm, - paddingHorizontal: spacing.md, - paddingVertical: spacing.sm - }, - label: { - color: colors.textMuted, - fontSize: typography.metaSize, - fontStyle: 'italic' - }, - dots: { - flexDirection: 'row', - gap: 4 - }, - dot: { - width: 5, - height: 5, - borderRadius: 2.5, - backgroundColor: colors.textSecondary - } -}) diff --git a/mobile/src/session/MobileNativeChatFloatingControls.tsx b/mobile/src/session/MobileNativeChatFloatingControls.tsx index b7f8430bcb3..2ca716de860 100644 --- a/mobile/src/session/MobileNativeChatFloatingControls.tsx +++ b/mobile/src/session/MobileNativeChatFloatingControls.tsx @@ -2,31 +2,23 @@ import { Pressable, Text, View } from 'react-native' import { ArrowDown, ChevronsDownUp, ChevronsUpDown } from 'lucide-react-native' import { colors } from '../theme/mobile-theme' import { styles } from './mobile-native-chat-view-styles' -import { MobileAgentWorkingIndicator } from './MobileAgentWorkingIndicator' /** Floats over the transcript's bottom-right so the Tools toggle and jump-to-latest take no row. */ export function MobileNativeChatFloatingControls({ toolCallCount, toolsExpanded, - bridgeWorking, onToggleTools, showJumpToTail, onJumpToTail }: { toolCallCount: number toolsExpanded: boolean - bridgeWorking: boolean onToggleTools: () => void showJumpToTail: boolean onJumpToTail: () => void }): React.JSX.Element { return ( - {bridgeWorking ? ( - - - - ) : null} {toolCallCount > 0 ? ( ({ MobileNativeChatAsk: 'ChatAsk' })) vi.mock('./MobileNativeChatPermission', () => ({ MobileNativeChatPermission: 'ChatPermission' })) vi.mock('./MobileNativeChatQuestion', () => ({ MobileNativeChatQuestion: 'ChatQuestion' })) vi.mock('../components/ActionSheetModal', () => ({ ActionSheetModal: 'ActionSheetModal' })) -vi.mock('./MobileAgentWorkingIndicator', () => ({ - MobileAgentWorkingIndicator: 'WorkingIndicator' -})) vi.mock('./MobileNativeChatComposer', async () => { const { composerStub } = await import('./MobileNativeChatView.render-stubs.test-fixture') return composerStub() diff --git a/mobile/src/session/MobileNativeChatView.failure-banner.test.ts b/mobile/src/session/MobileNativeChatView.failure-banner.test.ts index 9c056b83934..859ff1a2178 100644 --- a/mobile/src/session/MobileNativeChatView.failure-banner.test.ts +++ b/mobile/src/session/MobileNativeChatView.failure-banner.test.ts @@ -35,9 +35,6 @@ vi.mock('./MobileNativeChatAsk', () => ({ MobileNativeChatAsk: 'ChatAsk' })) vi.mock('./MobileNativeChatPermission', () => ({ MobileNativeChatPermission: 'ChatPermission' })) vi.mock('./MobileNativeChatQuestion', () => ({ MobileNativeChatQuestion: 'ChatQuestion' })) vi.mock('../components/ActionSheetModal', () => ({ ActionSheetModal: 'ActionSheetModal' })) -vi.mock('./MobileAgentWorkingIndicator', () => ({ - MobileAgentWorkingIndicator: 'WorkingIndicator' -})) vi.mock('./MobileNativeChatComposer', async () => { const { composerStub } = await import('./MobileNativeChatView.render-stubs.test-fixture') return composerStub() diff --git a/mobile/src/session/MobileNativeChatView.history-paging.test.ts b/mobile/src/session/MobileNativeChatView.history-paging.test.ts index 116464c23d6..02d061e5d2d 100644 --- a/mobile/src/session/MobileNativeChatView.history-paging.test.ts +++ b/mobile/src/session/MobileNativeChatView.history-paging.test.ts @@ -36,9 +36,6 @@ vi.mock('./MobileNativeChatAsk', () => ({ MobileNativeChatAsk: 'ChatAsk' })) vi.mock('./MobileNativeChatPermission', () => ({ MobileNativeChatPermission: 'ChatPermission' })) vi.mock('./MobileNativeChatQuestion', () => ({ MobileNativeChatQuestion: 'ChatQuestion' })) vi.mock('../components/ActionSheetModal', () => ({ ActionSheetModal: 'ActionSheetModal' })) -vi.mock('./MobileAgentWorkingIndicator', () => ({ - MobileAgentWorkingIndicator: 'WorkingIndicator' -})) vi.mock('./MobileNativeChatComposer', async () => { const { composerStub } = await import('./MobileNativeChatView.render-stubs.test-fixture') return composerStub() diff --git a/mobile/src/session/MobileNativeChatView.prompt-presentation.test.ts b/mobile/src/session/MobileNativeChatView.prompt-presentation.test.ts index cfd1157ab27..14efe3a7dc6 100644 --- a/mobile/src/session/MobileNativeChatView.prompt-presentation.test.ts +++ b/mobile/src/session/MobileNativeChatView.prompt-presentation.test.ts @@ -61,9 +61,6 @@ vi.mock('./MobileNativeChatMessage', () => ({ MobileNativeChatMessage: 'ChatMess vi.mock('./MobileNativeChatLiveLine', () => ({ MobileNativeChatLiveLine: 'LiveStatus' })) vi.mock('./MobileNativeChatAsk', () => ({ MobileNativeChatAsk: 'ChatAsk' })) vi.mock('./MobileNativeChatQuestion', () => ({ MobileNativeChatQuestion: 'ChatQuestion' })) -vi.mock('./MobileAgentWorkingIndicator', () => ({ - MobileAgentWorkingIndicator: 'WorkingIndicator' -})) vi.mock('./MobileNativeChatSessionOptionPickers', () => ({ MobileNativeChatSessionOptionPickers: 'SessionOptions' })) diff --git a/mobile/src/session/MobileNativeChatView.rows.test.ts b/mobile/src/session/MobileNativeChatView.rows.test.ts index 789df91ca68..a9402249c3e 100644 --- a/mobile/src/session/MobileNativeChatView.rows.test.ts +++ b/mobile/src/session/MobileNativeChatView.rows.test.ts @@ -31,9 +31,6 @@ vi.mock('./MobileNativeChatAsk', () => ({ MobileNativeChatAsk: 'ChatAsk' })) vi.mock('./MobileNativeChatPermission', () => ({ MobileNativeChatPermission: 'ChatPermission' })) vi.mock('./MobileNativeChatQuestion', () => ({ MobileNativeChatQuestion: 'ChatQuestion' })) vi.mock('../components/ActionSheetModal', () => ({ ActionSheetModal: 'ActionSheetModal' })) -vi.mock('./MobileAgentWorkingIndicator', () => ({ - MobileAgentWorkingIndicator: 'WorkingIndicator' -})) vi.mock('./MobileNativeChatComposer', async () => { const { composerStub } = await import('./MobileNativeChatView.render-stubs.test-fixture') return composerStub() diff --git a/mobile/src/session/MobileNativeChatView.stop.test.ts b/mobile/src/session/MobileNativeChatView.stop.test.ts index fc24ceb812b..bd5a4a6e6d3 100644 --- a/mobile/src/session/MobileNativeChatView.stop.test.ts +++ b/mobile/src/session/MobileNativeChatView.stop.test.ts @@ -30,9 +30,6 @@ vi.mock('./MobileNativeChatAsk', () => ({ MobileNativeChatAsk: 'ChatAsk' })) vi.mock('./MobileNativeChatPermission', () => ({ MobileNativeChatPermission: 'ChatPermission' })) vi.mock('./MobileNativeChatQuestion', () => ({ MobileNativeChatQuestion: 'ChatQuestion' })) vi.mock('../components/ActionSheetModal', () => ({ ActionSheetModal: 'ActionSheetModal' })) -vi.mock('./MobileAgentWorkingIndicator', () => ({ - MobileAgentWorkingIndicator: 'WorkingIndicator' -})) vi.mock('./MobileNativeChatComposer', async () => { const { composerStub } = await import('./MobileNativeChatView.render-stubs.test-fixture') return composerStub() diff --git a/mobile/src/session/MobileNativeChatView.tail-follow.test.ts b/mobile/src/session/MobileNativeChatView.tail-follow.test.ts index 701d506feff..8d78eed4f9a 100644 --- a/mobile/src/session/MobileNativeChatView.tail-follow.test.ts +++ b/mobile/src/session/MobileNativeChatView.tail-follow.test.ts @@ -36,9 +36,6 @@ vi.mock('./MobileNativeChatAsk', () => ({ MobileNativeChatAsk: 'ChatAsk' })) vi.mock('./MobileNativeChatPermission', () => ({ MobileNativeChatPermission: 'ChatPermission' })) vi.mock('./MobileNativeChatQuestion', () => ({ MobileNativeChatQuestion: 'ChatQuestion' })) vi.mock('../components/ActionSheetModal', () => ({ ActionSheetModal: 'ActionSheetModal' })) -vi.mock('./MobileAgentWorkingIndicator', () => ({ - MobileAgentWorkingIndicator: 'WorkingIndicator' -})) vi.mock('./MobileNativeChatComposer', async () => { const { composerStub } = await import('./MobileNativeChatView.render-stubs.test-fixture') return composerStub() diff --git a/mobile/src/session/MobileNativeChatView.tsx b/mobile/src/session/MobileNativeChatView.tsx index 66f2bdda156..2a3e1365e8b 100644 --- a/mobile/src/session/MobileNativeChatView.tsx +++ b/mobile/src/session/MobileNativeChatView.tsx @@ -284,8 +284,7 @@ export function MobileNativeChatView({ ) // Per-turn status rows: one live indicator while the turn runs, then a settled - // "Worked for N" row. The structured lane owns them; the bridge lane keeps its - // three-dot indicator. + // "Worked for N" row. The structured lane owns them; the bridge lane gets the live line only. const turns = useMobileNativeChatTurnDisclosure({ messages: data, enabled: structuredActivityUi, @@ -297,7 +296,7 @@ export function MobileNativeChatView({ activityText: turnIndicator?.activityText ?? null, // The display status, decided once in the session hook. stopping: turnIndicator?.stopping === true, - lineYields: structuredActivityUi && (ask != null || permission != null || question != null), + lineYields: ask != null || permission != null || question != null, scopeKey: sendSurfaceId }) @@ -323,7 +322,6 @@ export function MobileNativeChatView({ onOpenFile={onOpenFile} /> ) : null - const bridgeWorking = agentWorking === true && !structuredActivityUi const toolCallCount = useMemo(() => mobileNativeChatToolCallCount(folded), [folded]) const toggleTools = useCallback(() => setToolsExpanded((v) => !v), []) @@ -363,9 +361,7 @@ export function MobileNativeChatView({ keyExtractor={(item) => item.id} renderItem={history.renderListRow} contentContainerStyle={ - toolCallCount > 0 || bridgeWorking - ? styles.listContentUnderFloatingControls - : styles.listContent + toolCallCount > 0 ? styles.listContentUnderFloatingControls : styles.listContent } // Let link/file taps land while the composer keyboard is up // instead of being swallowed by the dismiss gesture. @@ -392,7 +388,6 @@ export function MobileNativeChatView({ ({ MobileNativeChatAsk: 'ChatAsk' })) vi.mock('./MobileNativeChatPermission', () => ({ MobileNativeChatPermission: 'ChatPermission' })) vi.mock('./MobileNativeChatQuestion', () => ({ MobileNativeChatQuestion: 'ChatQuestion' })) vi.mock('../components/ActionSheetModal', () => ({ ActionSheetModal: 'ActionSheetModal' })) -vi.mock('./MobileAgentWorkingIndicator', () => ({ - MobileAgentWorkingIndicator: 'WorkingIndicator' -})) // Stand-in composer: exposes the view's `handleSend` through a pressable, which is // the only composer behaviour these banner tests exercise. @@ -192,11 +189,6 @@ describe('MobileNativeChatView', () => { : null } - /** The bridge cue stays outside the scrolling transcript. */ - function workingIndicators(): unknown[] { - return renderer!.root.findAll((node) => String(node.type) === 'WorkingIndicator') - } - it('puts the clock bar under the prompt, activity at the tail, and drops the three dots', async () => { const folded = [userTurn('u1', 'go'), assistantTurn('a1', 'still working')] await render({ messages: folded, folded, structuredActivityUi: true, agentWorking: true }) @@ -207,7 +199,6 @@ describe('MobileNativeChatView', () => { expect(footerProps()).toEqual({ thinking: false, activityText: null, stopping: false }) expect(listIds().at(-1)).toBe('a1') expect(props.activeTurnIsWorking).toBe(true) - expect(workingIndicators()).toHaveLength(0) }) it.each([ @@ -318,22 +309,16 @@ describe('MobileNativeChatView', () => { expect(footerProps()).toMatchObject({ activityText: 'Running pnpm test' }) }) - it('keeps the bridge lane on the three-dot indicator with no turn status', async () => { + it('gives the bridge lane the same live Working line, with no turn status', async () => { const folded = [userTurn('u1', 'go')] await render({ messages: folded, folded, agentWorking: true }) const props = rowProps('u1') expect(props.structuredActivityUi).toBe(false) expect(props.turnStatus).toBeNull() expect(props.activeTurnIsWorking).toBe(false) - expect(footerProps()).toBeNull() - expect(workingIndicators()).toHaveLength(1) - expect( - renderer!.root.find((node) => String(node.type) === 'WorkingIndicator').parent?.props - .pointerEvents - ).toBe('none') + expect(footerProps()).toEqual({ thinking: false, activityText: null, stopping: false }) const list = renderer!.root.find((node) => String(node.type) === 'FlatList') - expect(list.props.ListFooterComponent).toBeNull() - expect(list.props.contentContainerStyle).toBe(styles.listContentUnderFloatingControls) + expect(list.props.contentContainerStyle).toBe(styles.listContent) }) it('settles the finished turn to a tappable duration', async () => { diff --git a/mobile/src/session/MobileNativeChatView.waiting-rows.test.ts b/mobile/src/session/MobileNativeChatView.waiting-rows.test.ts index 9a433253b99..443b0e0a259 100644 --- a/mobile/src/session/MobileNativeChatView.waiting-rows.test.ts +++ b/mobile/src/session/MobileNativeChatView.waiting-rows.test.ts @@ -45,9 +45,6 @@ vi.mock('../components/ActionSheetModal', () => ({ ActionSheetModal: 'ActionShee vi.mock('./MobileNativeChatAsk', () => ({ MobileNativeChatAsk: 'ChatAsk' })) vi.mock('./MobileNativeChatPermission', () => ({ MobileNativeChatPermission: 'ChatPermission' })) vi.mock('./MobileNativeChatQuestion', () => ({ MobileNativeChatQuestion: 'ChatQuestion' })) -vi.mock('./MobileAgentWorkingIndicator', () => ({ - MobileAgentWorkingIndicator: 'WorkingIndicator' -})) let renderer: ReactTestRenderer | null = null afterEach(() => { diff --git a/mobile/src/session/__mocks__/mobile-prompt-controller.ts b/mobile/src/session/__mocks__/mobile-prompt-controller.ts index 9af112119fd..19fa304dea6 100644 --- a/mobile/src/session/__mocks__/mobile-prompt-controller.ts +++ b/mobile/src/session/__mocks__/mobile-prompt-controller.ts @@ -71,9 +71,6 @@ vi.mock('../MobileNativeChatVisual', () => ({ useMobileNativeChatVisualRenderer: vi.mock('../../platform/use-reduced-motion', () => ({ useReducedMotionEnabled: () => null })) vi.mock('../MobileNativeChatLiveLine', () => ({ MobileNativeChatLiveLine: 'LiveStatus' })) vi.mock('../MobileNativeChatAsk', () => ({ MobileNativeChatAsk: 'ChatAsk' })) -vi.mock('../MobileAgentWorkingIndicator', () => ({ - MobileAgentWorkingIndicator: 'WorkingIndicator' -})) vi.mock('../MobileNativeChatSessionOptionPickers', () => ({ MobileNativeChatSessionOptionPickers: 'SessionOptions' })) diff --git a/mobile/src/session/mobile-native-chat-merged-snapshot-parity-hooks.test.tsx b/mobile/src/session/mobile-native-chat-merged-snapshot-parity-hooks.test.tsx index 53182603f66..84eb4112b81 100644 --- a/mobile/src/session/mobile-native-chat-merged-snapshot-parity-hooks.test.tsx +++ b/mobile/src/session/mobile-native-chat-merged-snapshot-parity-hooks.test.tsx @@ -74,9 +74,6 @@ vi.mock('../components/ActionSheetModal', () => ({ ActionSheetModal: 'ActionShee vi.mock('./MobileNativeChatAsk', () => ({ MobileNativeChatAsk: 'ChatAsk' })) vi.mock('./MobileNativeChatPermission', () => ({ MobileNativeChatPermission: 'ChatPermission' })) vi.mock('./MobileNativeChatQuestion', () => ({ MobileNativeChatQuestion: 'ChatQuestion' })) -vi.mock('./MobileAgentWorkingIndicator', () => ({ - MobileAgentWorkingIndicator: 'WorkingIndicator' -})) const STOP_ROW = `stopped-before-start:orca:${NEVER_OPENED}` diff --git a/mobile/src/session/mobile-native-chat-view-styles.ts b/mobile/src/session/mobile-native-chat-view-styles.ts index e5758b3a810..8d35da5d00e 100644 --- a/mobile/src/session/mobile-native-chat-view-styles.ts +++ b/mobile/src/session/mobile-native-chat-view-styles.ts @@ -43,9 +43,6 @@ export const styles = StyleSheet.create({ fontSize: typography.metaSize, fontWeight: '600' }, - workingPill: { - paddingHorizontal: 0 - }, pressed: { opacity: 0.6 }, diff --git a/mobile/src/session/use-mobile-native-chat-turn-disclosure.ts b/mobile/src/session/use-mobile-native-chat-turn-disclosure.ts index 495a223b9b4..7a68e168c1c 100644 --- a/mobile/src/session/use-mobile-native-chat-turn-disclosure.ts +++ b/mobile/src/session/use-mobile-native-chat-turn-disclosure.ts @@ -81,8 +81,8 @@ function useScopedOpenKeys(scopeKey: string): [ReadonlySet, (key: string } /** Owns the transcript's per-turn status rows and their disclosure state, and - * resolves what one list row needs. Bridge-lane chats pass `enabled: false` and - * keep their single three-dot working indicator instead. */ + * resolves what one list row needs. Bridge-lane chats pass `enabled: false`: no turn + * rows, but the same live "Working…" line while the agent works. */ export function useMobileNativeChatTurnDisclosure({ messages, enabled, @@ -188,7 +188,7 @@ export function useMobileNativeChatTurnDisclosure({ const line = useMemo( () => nativeChatLiveLine({ - draws: enabled && isWorking && !lineYields && active !== null, + draws: isWorking && !lineYields && (!enabled || active !== null), thinking: active?.thinking === true, stopping, activityText: activeActivityText,