diff --git a/packages/app/src/components/agent-stream-turn-footer.tsx b/packages/app/src/components/agent-stream-turn-footer.tsx index 300eb0804..00e87d29c 100644 --- a/packages/app/src/components/agent-stream-turn-footer.tsx +++ b/packages/app/src/components/agent-stream-turn-footer.tsx @@ -1,27 +1,24 @@ -import React, { memo, useCallback, useEffect, useMemo, type ReactNode } from "react"; +import React, { memo, useCallback, useMemo, type ReactNode } from "react"; import { View } from "react-native"; -import Animated, { - cancelAnimation, - Easing, - useAnimatedStyle, - useSharedValue, - withRepeat, - withTiming, -} from "react-native-reanimated"; -import { StyleSheet } from "react-native-unistyles"; +import { StyleSheet, withUnistyles } from "react-native-unistyles"; import { MAX_CONTENT_WIDTH } from "@/constants/layout"; +import type { Theme } from "@/styles/theme"; import type { TurnTiming } from "@/timeline/turn-time"; import type { StreamItem } from "@/types/stream"; -import { - getWorkingIndicatorDotStrength, - WORKING_INDICATOR_CYCLE_MS, - WORKING_INDICATOR_OFFSETS, -} from "@/utils/working-indicator"; import { collectAssistantTurnContentForStreamRenderStrategy, type StreamStrategy, } from "./agent-stream-render-strategy"; import { AssistantTurnFooter, LiveElapsed, STREAM_METADATA_FONT_SIZE } from "./message"; +import { SyncedLoader } from "./synced-loader"; + +const ThemedSyncedLoader = withUnistyles(SyncedLoader); +const workingIndicatorColorMapping = (theme: Theme) => ({ + color: + theme.colorScheme === "light" + ? theme.colors.palette.amber[700] + : theme.colors.palette.amber[500], +}); export type TurnContentStrategy = StreamStrategy; @@ -130,63 +127,10 @@ const WorkingIndicator = memo(function WorkingIndicator({ }: { inFlightTurnStartedAt?: Date | null; }) { - const progress = useSharedValue(0); - - useEffect(() => { - progress.value = 0; - progress.value = withRepeat( - withTiming(1, { - duration: WORKING_INDICATOR_CYCLE_MS, - easing: Easing.linear, - }), - -1, - false, - ); - - return () => { - cancelAnimation(progress); - progress.value = 0; - }; - }, [progress]); - - const translateDistance = -2; - const dotOneStyle = useAnimatedStyle(() => { - const strength = getWorkingIndicatorDotStrength(progress.value, WORKING_INDICATOR_OFFSETS[0]); - return { - opacity: 0.3 + strength * 0.7, - transform: [{ translateY: strength * translateDistance }], - }; - }); - - const dotTwoStyle = useAnimatedStyle(() => { - const strength = getWorkingIndicatorDotStrength(progress.value, WORKING_INDICATOR_OFFSETS[1]); - return { - opacity: 0.3 + strength * 0.7, - transform: [{ translateY: strength * translateDistance }], - }; - }); - - const dotThreeStyle = useAnimatedStyle(() => { - const strength = getWorkingIndicatorDotStrength(progress.value, WORKING_INDICATOR_OFFSETS[2]); - return { - opacity: 0.3 + strength * 0.7, - transform: [{ translateY: strength * translateDistance }], - }; - }); - - const dotOneCombinedStyle = useMemo(() => [stylesheet.workingDot, dotOneStyle], [dotOneStyle]); - const dotTwoCombinedStyle = useMemo(() => [stylesheet.workingDot, dotTwoStyle], [dotTwoStyle]); - const dotThreeCombinedStyle = useMemo( - () => [stylesheet.workingDot, dotThreeStyle], - [dotThreeStyle], - ); - return ( - - - - + + {inFlightTurnStartedAt ? ( ({ fontSize: STREAM_METADATA_FONT_SIZE, fontVariant: ["tabular-nums"], }, - workingDotsRow: { - flexDirection: "row", - alignItems: "center", - gap: theme.spacing[1], - transform: [{ translateY: 1 }], - }, - workingDot: { - width: 6, - height: 6, - borderRadius: 3, - backgroundColor: theme.colors.foregroundMuted, + workingLoader: { + marginLeft: -2, }, })); diff --git a/packages/app/src/utils/working-indicator.test.ts b/packages/app/src/utils/working-indicator.test.ts deleted file mode 100644 index c47648ebe..000000000 --- a/packages/app/src/utils/working-indicator.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { getWorkingIndicatorDotStrength, WORKING_INDICATOR_OFFSETS } from "./working-indicator"; - -describe("getWorkingIndicatorDotStrength", () => { - it("returns a stable triangular pulse over one cycle", () => { - expect(getWorkingIndicatorDotStrength(0, 0)).toBe(0); - expect(getWorkingIndicatorDotStrength(0.125, 0)).toBe(0.25); - expect(getWorkingIndicatorDotStrength(0.25, 0)).toBe(0.5); - expect(getWorkingIndicatorDotStrength(0.5, 0)).toBe(1); - expect(getWorkingIndicatorDotStrength(0.75, 0)).toBe(0.5); - expect(getWorkingIndicatorDotStrength(1, 0)).toBe(0); - }); - - it("keeps the dots phase-shifted instead of identical", () => { - const strengths = WORKING_INDICATOR_OFFSETS.map((offset) => - getWorkingIndicatorDotStrength(0, offset), - ); - - expect(strengths).toEqual([0, 0.26666666666666666, 0.5333333333333333]); - }); - - it("wraps progress cleanly across loop boundaries", () => { - expect(getWorkingIndicatorDotStrength(1.1, 0)).toBeCloseTo( - getWorkingIndicatorDotStrength(0.1, 0), - ); - expect(getWorkingIndicatorDotStrength(-0.1, 0)).toBeCloseTo( - getWorkingIndicatorDotStrength(0.9, 0), - ); - }); -}); diff --git a/packages/app/src/utils/working-indicator.ts b/packages/app/src/utils/working-indicator.ts deleted file mode 100644 index 4ed118364..000000000 --- a/packages/app/src/utils/working-indicator.ts +++ /dev/null @@ -1,21 +0,0 @@ -export const WORKING_INDICATOR_CYCLE_MS = 1200; -export const WORKING_INDICATOR_OFFSETS = [ - 0, - 160 / WORKING_INDICATOR_CYCLE_MS, - 320 / WORKING_INDICATOR_CYCLE_MS, -] as const; - -function normalizePhase(value: number): number { - "worklet"; - const wrapped = value % 1; - return wrapped < 0 ? wrapped + 1 : wrapped; -} - -export function getWorkingIndicatorDotStrength(progress: number, offset: number): number { - "worklet"; - const phase = normalizePhase(progress + offset); - if (phase <= 0.5) { - return phase * 2; - } - return (1 - phase) * 2; -}