@alia.onl/sdk 3.1.0 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,14 +1,33 @@
1
1
  import React, { useMemo } from 'react';
2
- import { View, Text, Platform, Linking } from 'react-native';
2
+ import { View, Text, Platform, Linking, useColorScheme } from 'react-native';
3
3
  import Markdown from 'react-native-markdown-display';
4
- import { useAliaColors } from '../theme';
4
+ import type { AliaColors } from '../theme';
5
+
6
+ // Hardcoded fallback colors for standalone SDK usage (when no color override is passed).
7
+ // The main app always passes resolved colors via the `colors` prop.
8
+ const FALLBACK_LIGHT: Pick<AliaColors, 'text' | 'muted' | 'border' | 'primary' | 'mutedForeground'> = {
9
+ text: '#11181C',
10
+ muted: '#F4F4F5',
11
+ border: '#E5E5EA',
12
+ primary: '#7C3AED',
13
+ mutedForeground: '#71717A',
14
+ };
15
+ const FALLBACK_DARK: typeof FALLBACK_LIGHT = {
16
+ text: '#ECEDEE',
17
+ muted: '#27272A',
18
+ border: '#2C2C2E',
19
+ primary: '#A78BFA',
20
+ mutedForeground: '#A1A1AA',
21
+ };
5
22
 
6
23
  const BODY_TEXT = { fontSize: 16, lineHeight: 28 } as const;
7
24
  const HEADING_TEXT = { fontSize: 16, lineHeight: 22 } as const;
8
25
  const SANS_FONT = Platform.select({ ios: 'Inter', android: 'Inter', default: 'Inter, sans-serif' })!;
9
26
  const MONO_FONT = Platform.select({ ios: 'Courier', android: 'monospace', default: 'monospace' })!;
10
27
 
11
- function createRules(isDark: boolean, textColor: string) {
28
+ function createRules(colors: AliaColors) {
29
+ const { text: textColor, muted, border, primary, mutedForeground } = colors;
30
+
12
31
  return {
13
32
  heading1: (node: any, children: any) => (
14
33
  <Text
@@ -66,9 +85,9 @@ function createRules(isDark: boolean, textColor: string) {
66
85
  marginVertical: 8,
67
86
  overflow: 'hidden',
68
87
  borderRadius: 6,
69
- backgroundColor: isDark ? '#18181b' : '#f4f4f5',
88
+ backgroundColor: muted,
70
89
  borderWidth: 1,
71
- borderColor: isDark ? '#3f3f46' : '#e4e4e7',
90
+ borderColor: border,
72
91
  padding: 12,
73
92
  }}
74
93
  >
@@ -78,7 +97,7 @@ function createRules(isDark: boolean, textColor: string) {
78
97
  <Text
79
98
  key={node.key}
80
99
  style={{
81
- backgroundColor: isDark ? '#27272a' : '#f4f4f5',
100
+ backgroundColor: muted,
82
101
  paddingHorizontal: 4,
83
102
  paddingVertical: 1,
84
103
  fontSize: 13,
@@ -98,7 +117,7 @@ function createRules(isDark: boolean, textColor: string) {
98
117
 
99
118
  return (
100
119
  <View key={node.key} style={{ flexDirection: 'row', paddingVertical: 2, paddingLeft: 16 }}>
101
- <Text style={{ marginRight: 8, minWidth: 14, ...BODY_TEXT, color: '#71717A' }}>{bullet}</Text>
120
+ <Text style={{ marginRight: 8, minWidth: 14, ...BODY_TEXT, color: mutedForeground }}>{bullet}</Text>
102
121
  <Text style={{ flex: 1, ...BODY_TEXT, color: textColor }}>{children}</Text>
103
122
  </View>
104
123
  );
@@ -115,7 +134,7 @@ function createRules(isDark: boolean, textColor: string) {
115
134
  link: (node: any, children: any) => (
116
135
  <Text
117
136
  key={node.key}
118
- style={{ color: isDark ? '#60a5fa' : '#2563eb', textDecorationLine: 'underline', ...BODY_TEXT }}
137
+ style={{ color: primary, textDecorationLine: 'underline', ...BODY_TEXT }}
119
138
  onPress={() => {
120
139
  if (node.attributes?.href) Linking.openURL(node.attributes.href);
121
140
  }}
@@ -132,8 +151,8 @@ function createRules(isDark: boolean, textColor: string) {
132
151
  style={{
133
152
  marginVertical: 8,
134
153
  borderLeftWidth: 4,
135
- borderLeftColor: isDark ? '#52525b' : '#d4d4d8',
136
- backgroundColor: isDark ? 'rgba(39,39,42,0.5)' : 'rgba(244,244,245,0.5)',
154
+ borderLeftColor: border,
155
+ backgroundColor: muted,
137
156
  paddingHorizontal: 12,
138
157
  paddingVertical: 4,
139
158
  borderTopRightRadius: 6,
@@ -149,20 +168,19 @@ function createRules(isDark: boolean, textColor: string) {
149
168
  style={{
150
169
  marginVertical: 16,
151
170
  height: 1,
152
- backgroundColor: isDark ? '#3f3f46' : '#d4d4d8',
171
+ backgroundColor: border,
153
172
  }}
154
173
  />
155
174
  ),
156
- text: (node: any) => {
157
- return node.content;
158
- },
159
175
  body: (node: any, children: any) => {
160
- return <View key={node.key}>{children}</View>;
176
+ return <View key={node.key} style={{ marginBottom: -8 }}>{children}</View>;
161
177
  },
162
178
  };
163
179
  }
164
180
 
165
- function createStyles(isDark: boolean, textColor: string, borderColor: string, mutedBg: string) {
181
+ function createStyles(colors: AliaColors) {
182
+ const { text: textColor, muted, border, primary, mutedForeground } = colors;
183
+
166
184
  return {
167
185
  body: { ...BODY_TEXT, color: textColor, fontFamily: SANS_FONT },
168
186
  text: { ...BODY_TEXT, color: textColor, fontFamily: SANS_FONT },
@@ -170,14 +188,14 @@ function createStyles(isDark: boolean, textColor: string, borderColor: string, m
170
188
  strong: { fontWeight: '600' as const },
171
189
  em: { fontStyle: 'italic' as const },
172
190
  s: { textDecorationLine: 'line-through' as const },
173
- bullet_list_icon: { marginLeft: 0, marginRight: 8, ...BODY_TEXT },
174
- ordered_list_icon: { marginLeft: 0, marginRight: 8, ...BODY_TEXT },
191
+ bullet_list_icon: { marginLeft: 0, marginRight: 8, ...BODY_TEXT, color: mutedForeground },
192
+ ordered_list_icon: { marginLeft: 0, marginRight: 8, ...BODY_TEXT, color: mutedForeground },
175
193
  bullet_list_content: { flex: 1 },
176
194
  ordered_list_content: { flex: 1 },
177
195
  code_inline: {
178
196
  fontSize: 13,
179
197
  fontFamily: MONO_FONT,
180
- backgroundColor: isDark ? '#27272a' : '#f4f4f5',
198
+ backgroundColor: muted,
181
199
  borderWidth: 0,
182
200
  borderRadius: 3,
183
201
  paddingHorizontal: 4,
@@ -186,28 +204,28 @@ function createStyles(isDark: boolean, textColor: string, borderColor: string, m
186
204
  code_block: {
187
205
  fontSize: 13,
188
206
  fontFamily: MONO_FONT,
189
- backgroundColor: isDark ? '#18181b' : '#f4f4f5',
207
+ backgroundColor: muted,
190
208
  borderWidth: 1,
191
- borderColor: isDark ? '#3f3f46' : '#e4e4e7',
209
+ borderColor: border,
192
210
  borderRadius: 6,
193
211
  padding: 12,
194
212
  },
195
213
  fence: {
196
214
  fontSize: 13,
197
215
  fontFamily: MONO_FONT,
198
- backgroundColor: isDark ? '#18181b' : '#f4f4f5',
216
+ backgroundColor: muted,
199
217
  borderWidth: 1,
200
- borderColor: isDark ? '#3f3f46' : '#e4e4e7',
218
+ borderColor: border,
201
219
  borderRadius: 6,
202
220
  padding: 12,
203
221
  },
204
- table: { borderWidth: 1, borderColor, borderRadius: 6, marginVertical: 8 },
205
- thead: { backgroundColor: mutedBg },
206
- th: { flex: 1, padding: 8, fontWeight: '600' as const, ...BODY_TEXT },
207
- td: { flex: 1, padding: 8, ...BODY_TEXT },
208
- tr: { borderBottomWidth: 1, borderColor, flexDirection: 'row' as const },
209
- link: { textDecorationLine: 'underline' as const, color: isDark ? '#60a5fa' : '#2563eb', ...BODY_TEXT },
210
- hr: { backgroundColor: isDark ? '#3f3f46' : '#d4d4d8', height: 1, marginVertical: 16 },
222
+ table: { borderWidth: 1, borderColor: border, borderRadius: 12, marginVertical: 8, overflow: 'hidden' as const },
223
+ thead: { backgroundColor: muted },
224
+ th: { flex: 1, padding: 8, fontWeight: '600' as const, ...BODY_TEXT, color: textColor },
225
+ td: { flex: 1, padding: 8, ...BODY_TEXT, color: textColor },
226
+ tr: { borderBottomWidth: 1, borderColor: border, flexDirection: 'row' as const },
227
+ link: { textDecorationLine: 'underline' as const, color: primary, ...BODY_TEXT },
228
+ hr: { backgroundColor: border, height: 1, marginVertical: 16 },
211
229
  heading1: { fontSize: 18, fontWeight: '600' as const },
212
230
  heading2: { ...HEADING_TEXT, fontWeight: '600' as const },
213
231
  heading3: { ...HEADING_TEXT, fontWeight: '600' as const },
@@ -217,16 +235,18 @@ function createStyles(isDark: boolean, textColor: string, borderColor: string, m
217
235
  };
218
236
  }
219
237
 
220
- export function AliaMarkdown({ content }: { content: string }) {
221
- const colors = useAliaColors();
222
- const isDark = colors.isDark;
223
- const textColor = isDark ? '#ffffff' : '#0a0a0a';
238
+ export interface AliaMarkdownProps {
239
+ content: string;
240
+ colors?: Partial<AliaColors>;
241
+ }
242
+
243
+ export function AliaMarkdown({ content, colors: colorOverrides }: AliaMarkdownProps) {
244
+ const scheme = useColorScheme();
245
+ const fallback = scheme === 'dark' ? FALLBACK_DARK : FALLBACK_LIGHT;
246
+ const colors = { ...fallback, ...colorOverrides } as AliaColors;
224
247
 
225
- const customRules = useMemo(() => createRules(isDark, textColor), [isDark]);
226
- const markdownStyles = useMemo(
227
- () => createStyles(isDark, textColor, colors.border, colors.muted),
228
- [isDark, colors.border, colors.muted],
229
- );
248
+ const customRules = useMemo(() => createRules(colors), [colors.text, colors.muted, colors.border, colors.primary, colors.mutedForeground]);
249
+ const markdownStyles = useMemo(() => createStyles(colors), [colors.text, colors.muted, colors.border, colors.primary, colors.mutedForeground]);
230
250
 
231
251
  return <Markdown rules={customRules} style={markdownStyles}>{content}</Markdown>;
232
252
  }
@@ -8,7 +8,7 @@ import Animated, {
8
8
  withSequence,
9
9
  cancelAnimation,
10
10
  } from 'react-native-reanimated';
11
- import { useAliaColors } from '../theme';
11
+
12
12
 
13
13
  // Context
14
14
  type ReasoningContextType = {
@@ -80,7 +80,6 @@ export function Reasoning({ isStreaming = false, defaultOpen = true, duration: e
80
80
  // Trigger — shows "Thinking for Xs..." label with pulsing icon
81
81
  export function ReasoningTrigger({ onPress }: ReasoningTriggerProps) {
82
82
  const { isStreaming, isOpen, setIsOpen, duration } = useReasoning();
83
- const colors = useAliaColors();
84
83
  const pulseOpacity = useSharedValue(1);
85
84
 
86
85
  useEffect(() => {
@@ -114,7 +113,6 @@ export function ReasoningTrigger({ onPress }: ReasoningTriggerProps) {
114
113
  // Content — renders thinking text (collapsible)
115
114
  export function ReasoningContent({ children }: ReasoningContentProps) {
116
115
  const { isOpen, isStreaming } = useReasoning();
117
- const colors = useAliaColors();
118
116
 
119
117
  if (!isOpen) return null;
120
118
 
@@ -1,6 +1,7 @@
1
1
  import { useState } from 'react';
2
2
  import { View, Pressable } from 'react-native';
3
3
  import { Text } from './ui/text';
4
+ import { cn } from '../lib/utils';
4
5
  import {
5
6
  Search,
6
7
  BookOpen,
@@ -10,7 +11,6 @@ import {
10
11
  ExternalLink,
11
12
  Loader2,
12
13
  } from 'lucide-react-native';
13
- import { useAliaColors } from '../theme';
14
14
  import type { ResearchProgress } from '../types';
15
15
 
16
16
  interface ResearchProgressCardProps {
@@ -30,7 +30,6 @@ const PHASE_LABELS: Record<string, string> = {
30
30
  const PHASE_ORDER = ['decomposing', 'searching', 'synthesizing', 'follow_up', 'finalizing', 'complete'];
31
31
 
32
32
  export function ResearchProgressCard({ progress }: ResearchProgressCardProps) {
33
- const colors = useAliaColors();
34
33
  const [showSources, setShowSources] = useState(false);
35
34
  const [showQuestions, setShowQuestions] = useState(true);
36
35
 
@@ -43,9 +42,9 @@ export function ResearchProgressCard({ progress }: ResearchProgressCardProps) {
43
42
  <View className="flex-row items-center gap-2">
44
43
  <View className="w-8 h-8 rounded-full bg-primary/10 items-center justify-center">
45
44
  {isComplete ? (
46
- <CheckCircle size={16} color="#22c55e" />
45
+ <CheckCircle size={16} className="text-green-500" />
47
46
  ) : (
48
- <Search size={16} color={colors.tint} />
47
+ <Search size={16} className="text-primary" />
49
48
  )}
50
49
  </View>
51
50
  <View className="flex-1">
@@ -70,12 +69,12 @@ export function ResearchProgressCard({ progress }: ResearchProgressCardProps) {
70
69
  {PHASE_ORDER.map((phase, i) => (
71
70
  <View
72
71
  key={phase}
73
- className="flex-1 h-1 rounded-full"
74
- style={{
75
- backgroundColor: i <= phaseIndex
76
- ? (isComplete ? '#22c55e' : colors.tint)
77
- : colors.muted,
78
- }}
72
+ className={cn(
73
+ 'flex-1 h-1 rounded-full',
74
+ i <= phaseIndex
75
+ ? (isComplete ? 'bg-green-500' : 'bg-primary')
76
+ : 'bg-muted',
77
+ )}
79
78
  />
80
79
  ))}
81
80
  </View>
@@ -87,14 +86,14 @@ export function ResearchProgressCard({ progress }: ResearchProgressCardProps) {
87
86
  onPress={() => setShowQuestions(!showQuestions)}
88
87
  className="flex-row items-center gap-1 mb-1"
89
88
  >
90
- <BookOpen size={12} color={colors.secondaryText} />
89
+ <BookOpen size={12} className="text-muted-foreground" />
91
90
  <Text className="text-xs font-medium text-muted-foreground">
92
91
  Research angles ({progress.subQuestions.length})
93
92
  </Text>
94
93
  {showQuestions ? (
95
- <ChevronUp size={12} color={colors.secondaryText} />
94
+ <ChevronUp size={12} className="text-muted-foreground" />
96
95
  ) : (
97
- <ChevronDown size={12} color={colors.secondaryText} />
96
+ <ChevronDown size={12} className="text-muted-foreground" />
98
97
  )}
99
98
  </Pressable>
100
99
  {showQuestions && (
@@ -112,7 +111,7 @@ export function ResearchProgressCard({ progress }: ResearchProgressCardProps) {
112
111
  {/* Current search query indicator */}
113
112
  {progress.currentQuery && !isComplete && (
114
113
  <View className="flex-row items-center gap-2 bg-muted/50 rounded-lg px-3 py-1.5">
115
- <Loader2 size={12} color={colors.secondaryText} />
114
+ <Loader2 size={12} className="text-muted-foreground" />
116
115
  <Text className="text-xs text-muted-foreground flex-1" numberOfLines={1}>
117
116
  {progress.currentQuery}
118
117
  </Text>
@@ -126,14 +125,14 @@ export function ResearchProgressCard({ progress }: ResearchProgressCardProps) {
126
125
  onPress={() => setShowSources(!showSources)}
127
126
  className="flex-row items-center gap-1"
128
127
  >
129
- <ExternalLink size={12} color={colors.secondaryText} />
128
+ <ExternalLink size={12} className="text-muted-foreground" />
130
129
  <Text className="text-xs font-medium text-muted-foreground">
131
130
  Sources ({progress.sources.length})
132
131
  </Text>
133
132
  {showSources ? (
134
- <ChevronUp size={12} color={colors.secondaryText} />
133
+ <ChevronUp size={12} className="text-muted-foreground" />
135
134
  ) : (
136
- <ChevronDown size={12} color={colors.secondaryText} />
135
+ <ChevronDown size={12} className="text-muted-foreground" />
137
136
  )}
138
137
  </Pressable>
139
138
  {showSources && (
@@ -1,5 +1,5 @@
1
1
  import React, { useState, useEffect } from "react";
2
- import { View, Text, StyleSheet } from "react-native";
2
+ import { View, Text } from "react-native";
3
3
  import Animated, {
4
4
  useSharedValue,
5
5
  useAnimatedStyle,
@@ -8,7 +8,6 @@ import Animated, {
8
8
  withSequence,
9
9
  Easing,
10
10
  } from "react-native-reanimated";
11
- import { useAliaColors } from "../theme";
12
11
 
13
12
  const thinkingPhrases = [
14
13
  "Thinking...",
@@ -33,7 +32,6 @@ const workingPhrases = [
33
32
  ];
34
33
 
35
34
  export function ThinkingIndicator({ isWorking = false, statusText }: { isWorking?: boolean; statusText?: string }) {
36
- const colors = useAliaColors();
37
35
  const phrases = isWorking ? workingPhrases : thinkingPhrases;
38
36
  const [phraseIndex, setPhraseIndex] = useState(() =>
39
37
  Math.floor(Math.random() * phrases.length)
@@ -104,31 +102,18 @@ export function ThinkingIndicator({ isWorking = false, statusText }: { isWorking
104
102
  const shownText = statusText || displayText;
105
103
 
106
104
  return (
107
- <View style={styles.container}>
105
+ <View className="flex-row items-center gap-2 py-2">
108
106
  <Animated.View style={spinStyle}>
109
- <Text style={{ fontSize: 16, color: colors.mutedForeground }}>✱</Text>
107
+ <Text className="text-base text-muted-foreground">✱</Text>
110
108
  </Animated.View>
111
- <View style={styles.textRow}>
112
- <Text style={{ fontSize: 16, color: colors.mutedForeground }}>{shownText}</Text>
109
+ <View className="flex-row items-center">
110
+ <Text className="text-base text-muted-foreground">{shownText}</Text>
113
111
  {(statusText || isTyping) && (
114
112
  <Animated.View style={cursorStyle}>
115
- <Text style={{ fontSize: 16, color: colors.mutedForeground }}>|</Text>
113
+ <Text className="text-base text-muted-foreground">|</Text>
116
114
  </Animated.View>
117
115
  )}
118
116
  </View>
119
117
  </View>
120
118
  );
121
119
  }
122
-
123
- const styles = StyleSheet.create({
124
- container: {
125
- flexDirection: 'row',
126
- alignItems: 'center',
127
- gap: 8,
128
- paddingVertical: 8,
129
- },
130
- textRow: {
131
- flexDirection: 'row',
132
- alignItems: 'center',
133
- },
134
- });
@@ -1,5 +1,5 @@
1
- import React, { useEffect } from 'react';
2
- import { View, Platform, useWindowDimensions } from 'react-native';
1
+ import React, { useEffect, useMemo } from 'react';
2
+ import { View, Platform, useWindowDimensions, useColorScheme } from 'react-native';
3
3
  import Svg, { Path } from 'react-native-svg';
4
4
  import Animated, {
5
5
  useSharedValue,
@@ -15,6 +15,13 @@ import type { AgentState } from '../../types';
15
15
 
16
16
  const AnimatedPath = Animated.createAnimatedComponent(Path);
17
17
 
18
+ /** Cross-platform blur: CSS string on web, RN array syntax on native */
19
+ function blurStyle(radius: number) {
20
+ return Platform.OS === 'web'
21
+ ? ({ filter: `blur(${radius}px)`, WebkitFilter: `blur(${radius}px)` } as any)
22
+ : ({ filter: [{ blur: radius }] } as any);
23
+ }
24
+
18
25
  const GLOW_HEIGHT = 350;
19
26
  const NUM_POINTS = 8;
20
27
 
@@ -167,20 +174,79 @@ const BLOB_COLORS: Record<AgentState, string[]> = {
167
174
  speaking: ['#1e2870', '#6366f1', '#9333ea', '#0ea5e9'],
168
175
  };
169
176
 
177
+ // Derive wave & blob palettes from a hex color
178
+ function hexToRgb(hex: string): [number, number, number] {
179
+ const h = hex.replace('#', '');
180
+ return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
181
+ }
182
+
183
+ function darken(rgb: [number, number, number], factor: number): [number, number, number] {
184
+ return [Math.round(rgb[0] * factor), Math.round(rgb[1] * factor), Math.round(rgb[2] * factor)];
185
+ }
186
+
187
+ function lighten(rgb: [number, number, number], factor: number): [number, number, number] {
188
+ return [
189
+ Math.min(255, Math.round(rgb[0] + (255 - rgb[0]) * factor)),
190
+ Math.min(255, Math.round(rgb[1] + (255 - rgb[1]) * factor)),
191
+ Math.min(255, Math.round(rgb[2] + (255 - rgb[2]) * factor)),
192
+ ];
193
+ }
194
+
195
+ function deriveWavePalette(hex: string, isDark: boolean): { waves: string[]; blobs: string[] } {
196
+ const rgb = hexToRgb(hex);
197
+ // Dark mode: lighter/brighter waves visible against dark bg
198
+ // Light mode: darker waves visible against light bg
199
+ const layer0 = isDark ? darken(rgb, 0.55) : darken(rgb, 0.3);
200
+ const layer1 = isDark ? lighten(rgb, 0.15) : darken(rgb, 0.5);
201
+ const layer2 = isDark ? lighten(rgb, 0.4) : darken(rgb, 0.15);
202
+ const layer3 = isDark ? lighten(rgb, 0.6) : rgb;
203
+ return {
204
+ waves: [
205
+ `rgba(${layer0[0]}, ${layer0[1]}, ${layer0[2]}, 0.8)`,
206
+ `rgba(${layer1[0]}, ${layer1[1]}, ${layer1[2]}, 0.45)`,
207
+ `rgba(${layer2[0]}, ${layer2[1]}, ${layer2[2]}, 0.35)`,
208
+ `rgba(${layer3[0]}, ${layer3[1]}, ${layer3[2]}, 0.25)`,
209
+ ],
210
+ blobs: [
211
+ `rgb(${layer0[0]}, ${layer0[1]}, ${layer0[2]})`,
212
+ `rgb(${layer1[0]}, ${layer1[1]}, ${layer1[2]})`,
213
+ `rgb(${layer2[0]}, ${layer2[1]}, ${layer2[2]})`,
214
+ `rgb(${layer3[0]}, ${layer3[1]}, ${layer3[2]})`,
215
+ ],
216
+ };
217
+ }
218
+
170
219
  interface AudioWaveVisualizerProps {
171
220
  waveAmplitude: SharedValue<number>;
172
221
  agentState: AgentState;
173
222
  isConnected: boolean;
223
+ /** Theme primary color hex — derives idle/listening wave palette */
224
+ primaryColor?: string;
225
+ /** Whether the app is in dark mode (affects wave brightness) */
226
+ isDarkMode?: boolean;
174
227
  }
175
228
 
176
229
  export function AudioWaveVisualizer({
177
230
  waveAmplitude,
178
231
  agentState = 'idle',
179
232
  isConnected,
233
+ primaryColor,
234
+ isDarkMode,
180
235
  }: AudioWaveVisualizerProps) {
181
236
  const { width: screenWidth } = useWindowDimensions();
237
+ const scheme = useColorScheme();
238
+ const effectiveIsDark = isDarkMode ?? (scheme === 'dark');
182
239
  const state = WAVE_COLORS[agentState] ? agentState : 'idle';
183
240
 
241
+ // Use theme-derived palette for idle/listening, keep hardcoded for thinking/speaking
242
+ const themed = useMemo(
243
+ () => (primaryColor ? deriveWavePalette(primaryColor, effectiveIsDark) : null),
244
+ [primaryColor, effectiveIsDark],
245
+ );
246
+ const isThemed = (state === 'idle' || state === 'listening') && themed != null;
247
+ const waveColors = isThemed ? themed.waves : WAVE_COLORS[state];
248
+ const blobColors = isThemed ? themed.blobs : BLOB_COLORS[state];
249
+
184
250
  return (
185
251
  <View
186
252
  style={{
@@ -194,7 +260,7 @@ export function AudioWaveVisualizer({
194
260
  <GlowBlob
195
261
  key={`blob-${i}`}
196
262
  blob={blob}
197
- color={BLOB_COLORS[state][i]}
263
+ color={blobColors[i]}
198
264
  waveAmplitude={waveAmplitude}
199
265
  isConnected={isConnected}
200
266
  screenWidth={screenWidth}
@@ -206,7 +272,7 @@ export function AudioWaveVisualizer({
206
272
  <OceanWave
207
273
  key={`wave-${i}`}
208
274
  layer={layer}
209
- color={WAVE_COLORS[state][i]}
275
+ color={waveColors[i]}
210
276
  waveAmplitude={waveAmplitude}
211
277
  isConnected={isConnected}
212
278
  screenWidth={screenWidth}
@@ -279,14 +345,6 @@ function OceanWave({
279
345
  return { d };
280
346
  });
281
347
 
282
- const webBlurStyle =
283
- Platform.OS === 'web'
284
- ? ({
285
- filter: `blur(${layer.blur}px)`,
286
- WebkitFilter: `blur(${layer.blur}px)`,
287
- } as any)
288
- : {};
289
-
290
348
  return (
291
349
  <View
292
350
  style={{
@@ -296,7 +354,7 @@ function OceanWave({
296
354
  right: 0,
297
355
  bottom: 0,
298
356
  opacity: isConnected ? 1 : 0,
299
- ...webBlurStyle,
357
+ ...blurStyle(layer.blur),
300
358
  pointerEvents: 'none',
301
359
  }}
302
360
  >
@@ -365,13 +423,5 @@ function GlowBlob({
365
423
  };
366
424
  });
367
425
 
368
- const webBlurStyle =
369
- Platform.OS === 'web'
370
- ? ({
371
- filter: `blur(${blob.blur}px)`,
372
- WebkitFilter: `blur(${blob.blur}px)`,
373
- } as any)
374
- : {};
375
-
376
- return <Animated.View style={[animatedStyle, webBlurStyle]} />;
426
+ return <Animated.View style={[animatedStyle, blurStyle(blob.blur)]} />;
377
427
  }