@agent-native/core 0.98.0 → 0.98.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.
Files changed (243) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +24 -0
  3. package/corpus/core/package.json +2 -2
  4. package/corpus/core/src/action-change-marker.ts +8 -1
  5. package/corpus/core/src/changelog/parse.ts +8 -3
  6. package/corpus/core/src/client/AgentPanel.tsx +2 -3
  7. package/corpus/core/src/client/AssistantChat.tsx +83 -39
  8. package/corpus/core/src/client/analytics.ts +29 -0
  9. package/corpus/core/src/client/blocks/library/wireframe-kit.tsx +16 -2
  10. package/corpus/core/src/client/chat-model-groups.ts +28 -0
  11. package/corpus/core/src/client/composer/RealtimeVoiceMode.tsx +442 -85
  12. package/corpus/core/src/client/composer/TiptapComposer.tsx +46 -29
  13. package/corpus/core/src/client/composer/VoiceButton.tsx +2 -1
  14. package/corpus/core/src/client/composer/index.ts +5 -0
  15. package/corpus/core/src/client/composer/useRealtimeVoiceMode.tsx +1206 -67
  16. package/corpus/core/src/client/index.ts +5 -0
  17. package/corpus/core/src/client/progress/RunsTray.tsx +13 -1
  18. package/corpus/core/src/client/settings/SecretsSection.tsx +387 -266
  19. package/corpus/core/src/client/settings/SettingsPanel.tsx +117 -38
  20. package/corpus/core/src/client/use-action.ts +12 -0
  21. package/corpus/core/src/client/use-agent-chat-home-handoff.ts +8 -1
  22. package/corpus/core/src/client/use-agent-engine-configured.ts +17 -3
  23. package/corpus/core/src/client/use-db-sync.ts +84 -34
  24. package/corpus/core/src/client/use-pausing-interval.ts +14 -4
  25. package/corpus/core/src/integrations/scope-store.ts +1 -1
  26. package/corpus/core/src/integrations/usage-budget-store.ts +1 -1
  27. package/corpus/core/src/localization/default-messages.ts +44 -1
  28. package/corpus/core/src/mcp-client/workspace-servers.ts +4 -4
  29. package/corpus/core/src/secrets/crypto.ts +28 -8
  30. package/corpus/core/src/server/action-change.ts +5 -1
  31. package/corpus/core/src/server/action-routes.ts +8 -0
  32. package/corpus/core/src/server/poll.ts +1 -0
  33. package/corpus/core/src/server/realtime-voice.ts +299 -13
  34. package/corpus/core/src/styles/agent-native.css +2 -2
  35. package/corpus/core/src/templates/default/.agents/skills/real-time-sync/SKILL.md +2 -2
  36. package/corpus/core/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +2 -2
  37. package/corpus/core/src/templates/workspace-core/.agents/skills/secrets/SKILL.md +7 -3
  38. package/corpus/core/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +39 -9
  39. package/corpus/core/src/vite/client.ts +9 -1
  40. package/corpus/templates/analytics/.agents/skills/real-time-sync/SKILL.md +2 -2
  41. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +10 -5
  42. package/corpus/templates/analytics/AGENTS.md +9 -13
  43. package/corpus/templates/analytics/actions/navigate.ts +59 -5
  44. package/corpus/templates/analytics/actions/view-screen.ts +3 -6
  45. package/corpus/templates/analytics/app/components/ProviderCorpusJobNotifier.tsx +20 -3
  46. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +2 -1
  47. package/corpus/templates/analytics/app/components/layout/Header.tsx +1 -1
  48. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +21 -6
  49. package/corpus/templates/analytics/app/global.css +19 -20
  50. package/corpus/templates/analytics/app/lib/provider-corpus-job-sync.ts +26 -0
  51. package/corpus/templates/analytics/app/pages/Settings.tsx +35 -341
  52. package/corpus/templates/analytics/app/pages/monitoring/ErrorsPanel.tsx +38 -7
  53. package/corpus/templates/analytics/app/pages/monitoring/MonitoringPage.tsx +1 -1
  54. package/corpus/templates/analytics/app/pages/monitoring/errors/i18n.ts +3 -0
  55. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +333 -272
  56. package/corpus/templates/analytics/app/pages/sessions/SessionDevToolsPanel.tsx +72 -17
  57. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +297 -108
  58. package/corpus/templates/analytics/app/root.tsx +28 -8
  59. package/corpus/templates/analytics/changelog/2026-06-24-first-party-signup-and-pageview-panels-now-honor-dashboard-filters.md +2 -1
  60. package/corpus/templates/analytics/changelog/2026-06-25-referrer-dashboard-panels-show-external-domains.md +2 -1
  61. package/corpus/templates/analytics/changelog/2026-07-06-mobile-chat-header-is-more-compact-and-the-menu-button-opens-navigation.md +1 -0
  62. package/corpus/templates/analytics/changelog/2026-07-08-dashboard-history-and-chat-context.md +1 -0
  63. package/corpus/templates/analytics/changelog/2026-07-08-settings-are-cleaner-searchable-and-alerts-have-their-own-tab.md +1 -0
  64. package/corpus/templates/analytics/changelog/2026-07-08-sidebar-sections-stay-collapsed-until-opened.md +1 -0
  65. package/corpus/templates/analytics/changelog/2026-07-11-analytics-no-longer-repeatedly-restarts-background-requests-.md +6 -0
  66. package/corpus/templates/analytics/changelog/2026-07-11-analytics-only-asks-for-replay-storage-when-you-enable-sessi.md +6 -0
  67. package/corpus/templates/analytics/changelog/2026-07-11-charts-now-show-their-loading-skeleton-while-refreshing.md +6 -0
  68. package/corpus/templates/analytics/changelog/2026-07-11-realtime-voice-now-shows-a-clear-connection-indicator-comple.md +6 -0
  69. package/corpus/templates/analytics/changelog/2026-07-11-replay-event-counts-stay-hidden-until-recordings-finish-load.md +6 -0
  70. package/corpus/templates/analytics/changelog/2026-07-11-replay-storage-key-mismatches-show-actionable-setup-guidance.md +6 -0
  71. package/corpus/templates/analytics/changelog/2026-07-11-replay-timelines-group-continuous-scrolling-and-dev-tools-co.md +6 -0
  72. package/corpus/templates/analytics/changelog/2026-07-11-session-filters-now-stay-compact-in-a-settings-menu-at-mediu.md +6 -0
  73. package/corpus/templates/analytics/changelog/2026-07-11-session-replay-console-errors-now-link-to-matching-issues-or.md +6 -0
  74. package/corpus/templates/analytics/changelog/2026-07-11-session-replays-load-large-recordings-faster-and-surface-fai.md +6 -0
  75. package/corpus/templates/analytics/changelog/2026-07-11-session-replays-preserve-recorded-styles-and-viewport-layout.md +6 -0
  76. package/corpus/templates/analytics/changelog/2026-07-11-session-replays-stay-centered-instead-of-clipping-wide-recor.md +6 -0
  77. package/corpus/templates/analytics/package.json +1 -1
  78. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +1 -303
  79. package/corpus/templates/analytics/server/handlers/session-replay.ts +57 -0
  80. package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +1 -1
  81. package/corpus/templates/analytics/server/lib/demo-dashboards.ts +16 -3
  82. package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +12 -0
  83. package/corpus/templates/analytics/server/lib/session-replay.ts +208 -2
  84. package/corpus/templates/analytics/server/plugins/onboarding.ts +7 -79
  85. package/corpus/templates/analytics/server/routes/api/session-replay/recordings/[recordingId]/chunks.get.ts +1 -0
  86. package/corpus/templates/assets/.agents/skills/real-time-sync/SKILL.md +2 -2
  87. package/corpus/templates/assets/app/global.css +4 -4
  88. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +0 -1
  89. package/corpus/templates/assets/app/routes/library.tsx +0 -1
  90. package/corpus/templates/brain/.agents/skills/real-time-sync/SKILL.md +2 -2
  91. package/corpus/templates/brain/app/global.css +5 -5
  92. package/corpus/templates/brain/app/root.tsx +0 -3
  93. package/corpus/templates/calendar/.agents/skills/real-time-sync/SKILL.md +2 -2
  94. package/corpus/templates/calendar/app/root.tsx +1 -0
  95. package/corpus/templates/chat/.agents/skills/real-time-sync/SKILL.md +2 -2
  96. package/corpus/templates/clips/app/hooks/use-auto-title.ts +74 -8
  97. package/corpus/templates/clips/app/hooks/use-library.ts +12 -25
  98. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +20 -22
  99. package/corpus/templates/clips/changelog/2026-07-11-generated-titles-no-longer-cause-continuous-background-refreshes.md +6 -0
  100. package/corpus/templates/clips/changelog/2026-07-11-s3-storage-setup-stays-collapsed-until-you-choose-it-as-the-.md +6 -0
  101. package/corpus/templates/content/.agents/skills/real-time-sync/SKILL.md +2 -2
  102. package/corpus/templates/content/app/hooks/use-comments.ts +0 -1
  103. package/corpus/templates/content/changelog/2026-07-11-comments-now-update-from-live-collaboration-events-without-c.md +6 -0
  104. package/corpus/templates/design/app/components/layout/Header.tsx +1 -1
  105. package/corpus/templates/forms/.agents/skills/real-time-sync/SKILL.md +2 -2
  106. package/corpus/templates/macros/app/components/layout/AppLayout.tsx +1 -2
  107. package/corpus/templates/macros/app/root.tsx +0 -3
  108. package/corpus/templates/mail/.agents/skills/real-time-sync/SKILL.md +2 -2
  109. package/corpus/templates/mail/app/hooks/use-compose-state.ts +1 -0
  110. package/corpus/templates/mail/app/root.tsx +1 -12
  111. package/corpus/templates/plan/.agents/skills/real-time-sync/SKILL.md +2 -2
  112. package/corpus/templates/plan/app/hooks/use-navigation-state.ts +5 -2
  113. package/corpus/templates/plan/app/hooks/use-plans.ts +6 -13
  114. package/corpus/templates/plan/app/pages/PlansPage.tsx +5 -27
  115. package/corpus/templates/plan/changelog/2026-07-11-plan-refreshes-only-when-content-changes.md +6 -0
  116. package/corpus/templates/slides/app/components/comments/SlideCommentsPanel.tsx +4 -1
  117. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +1 -1
  118. package/corpus/templates/slides/app/components/layout/Header.tsx +1 -1
  119. package/corpus/templates/slides/app/context/DeckContext.tsx +1 -0
  120. package/corpus/templates/slides/app/hooks/use-slide-comments.ts +7 -31
  121. package/corpus/templates/slides/changelog/2026-07-11-comments-now-update-from-live-collaboration-events-without-c.md +6 -0
  122. package/dist/action-change-marker.d.ts +1 -0
  123. package/dist/action-change-marker.d.ts.map +1 -1
  124. package/dist/action-change-marker.js +7 -1
  125. package/dist/action-change-marker.js.map +1 -1
  126. package/dist/changelog/parse.d.ts +4 -2
  127. package/dist/changelog/parse.d.ts.map +1 -1
  128. package/dist/changelog/parse.js +5 -3
  129. package/dist/changelog/parse.js.map +1 -1
  130. package/dist/client/AgentPanel.d.ts.map +1 -1
  131. package/dist/client/AgentPanel.js +2 -3
  132. package/dist/client/AgentPanel.js.map +1 -1
  133. package/dist/client/AssistantChat.d.ts +7 -0
  134. package/dist/client/AssistantChat.d.ts.map +1 -1
  135. package/dist/client/AssistantChat.js +62 -40
  136. package/dist/client/AssistantChat.js.map +1 -1
  137. package/dist/client/analytics.d.ts.map +1 -1
  138. package/dist/client/analytics.js +26 -0
  139. package/dist/client/analytics.js.map +1 -1
  140. package/dist/client/blocks/library/wireframe-kit.d.ts +2 -0
  141. package/dist/client/blocks/library/wireframe-kit.d.ts.map +1 -1
  142. package/dist/client/blocks/library/wireframe-kit.js +9 -2
  143. package/dist/client/blocks/library/wireframe-kit.js.map +1 -1
  144. package/dist/client/chat-model-groups.d.ts.map +1 -1
  145. package/dist/client/chat-model-groups.js +24 -0
  146. package/dist/client/chat-model-groups.js.map +1 -1
  147. package/dist/client/composer/RealtimeVoiceMode.d.ts +40 -4
  148. package/dist/client/composer/RealtimeVoiceMode.d.ts.map +1 -1
  149. package/dist/client/composer/RealtimeVoiceMode.js +172 -42
  150. package/dist/client/composer/RealtimeVoiceMode.js.map +1 -1
  151. package/dist/client/composer/TiptapComposer.d.ts +4 -0
  152. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  153. package/dist/client/composer/TiptapComposer.js +47 -29
  154. package/dist/client/composer/TiptapComposer.js.map +1 -1
  155. package/dist/client/composer/VoiceButton.d.ts.map +1 -1
  156. package/dist/client/composer/VoiceButton.js +2 -2
  157. package/dist/client/composer/VoiceButton.js.map +1 -1
  158. package/dist/client/composer/index.d.ts +2 -2
  159. package/dist/client/composer/index.d.ts.map +1 -1
  160. package/dist/client/composer/index.js +1 -1
  161. package/dist/client/composer/index.js.map +1 -1
  162. package/dist/client/composer/useRealtimeVoiceMode.d.ts +100 -3
  163. package/dist/client/composer/useRealtimeVoiceMode.d.ts.map +1 -1
  164. package/dist/client/composer/useRealtimeVoiceMode.js +952 -51
  165. package/dist/client/composer/useRealtimeVoiceMode.js.map +1 -1
  166. package/dist/client/index.d.ts +1 -1
  167. package/dist/client/index.d.ts.map +1 -1
  168. package/dist/client/index.js +1 -1
  169. package/dist/client/index.js.map +1 -1
  170. package/dist/client/progress/RunsTray.d.ts.map +1 -1
  171. package/dist/client/progress/RunsTray.js +10 -1
  172. package/dist/client/progress/RunsTray.js.map +1 -1
  173. package/dist/client/settings/SecretsSection.d.ts +2 -3
  174. package/dist/client/settings/SecretsSection.d.ts.map +1 -1
  175. package/dist/client/settings/SecretsSection.js +46 -22
  176. package/dist/client/settings/SecretsSection.js.map +1 -1
  177. package/dist/client/settings/SettingsPanel.d.ts +9 -0
  178. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  179. package/dist/client/settings/SettingsPanel.js +46 -12
  180. package/dist/client/settings/SettingsPanel.js.map +1 -1
  181. package/dist/client/use-action.d.ts +2 -0
  182. package/dist/client/use-action.d.ts.map +1 -1
  183. package/dist/client/use-action.js +10 -0
  184. package/dist/client/use-action.js.map +1 -1
  185. package/dist/client/use-agent-chat-home-handoff.d.ts +2 -0
  186. package/dist/client/use-agent-chat-home-handoff.d.ts.map +1 -1
  187. package/dist/client/use-agent-chat-home-handoff.js +7 -1
  188. package/dist/client/use-agent-chat-home-handoff.js.map +1 -1
  189. package/dist/client/use-agent-engine-configured.d.ts.map +1 -1
  190. package/dist/client/use-agent-engine-configured.js +17 -3
  191. package/dist/client/use-agent-engine-configured.js.map +1 -1
  192. package/dist/client/use-db-sync.d.ts +8 -2
  193. package/dist/client/use-db-sync.d.ts.map +1 -1
  194. package/dist/client/use-db-sync.js +57 -32
  195. package/dist/client/use-db-sync.js.map +1 -1
  196. package/dist/client/use-pausing-interval.d.ts.map +1 -1
  197. package/dist/client/use-pausing-interval.js +16 -4
  198. package/dist/client/use-pausing-interval.js.map +1 -1
  199. package/dist/integrations/scope-store.js +1 -1
  200. package/dist/integrations/scope-store.js.map +1 -1
  201. package/dist/integrations/usage-budget-store.js +1 -1
  202. package/dist/integrations/usage-budget-store.js.map +1 -1
  203. package/dist/localization/default-messages.d.ts +41 -1
  204. package/dist/localization/default-messages.d.ts.map +1 -1
  205. package/dist/localization/default-messages.js +41 -1
  206. package/dist/localization/default-messages.js.map +1 -1
  207. package/dist/mcp-client/workspace-servers.js +4 -4
  208. package/dist/mcp-client/workspace-servers.js.map +1 -1
  209. package/dist/observability/routes.d.ts +5 -5
  210. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  211. package/dist/secrets/crypto.d.ts +7 -6
  212. package/dist/secrets/crypto.d.ts.map +1 -1
  213. package/dist/secrets/crypto.js +30 -11
  214. package/dist/secrets/crypto.js.map +1 -1
  215. package/dist/secrets/routes.d.ts +9 -9
  216. package/dist/server/action-change.d.ts +1 -0
  217. package/dist/server/action-change.d.ts.map +1 -1
  218. package/dist/server/action-change.js +4 -1
  219. package/dist/server/action-change.js.map +1 -1
  220. package/dist/server/action-routes.d.ts.map +1 -1
  221. package/dist/server/action-routes.js +5 -0
  222. package/dist/server/action-routes.js.map +1 -1
  223. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  224. package/dist/server/poll.d.ts.map +1 -1
  225. package/dist/server/poll.js +1 -0
  226. package/dist/server/poll.js.map +1 -1
  227. package/dist/server/realtime-voice.d.ts +7 -0
  228. package/dist/server/realtime-voice.d.ts.map +1 -1
  229. package/dist/server/realtime-voice.js +222 -12
  230. package/dist/server/realtime-voice.js.map +1 -1
  231. package/dist/styles/agent-native.css +2 -2
  232. package/dist/templates/default/.agents/skills/real-time-sync/SKILL.md +2 -2
  233. package/dist/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +2 -2
  234. package/dist/templates/workspace-core/.agents/skills/secrets/SKILL.md +7 -3
  235. package/dist/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +39 -9
  236. package/dist/vite/client.d.ts.map +1 -1
  237. package/dist/vite/client.js +6 -1
  238. package/dist/vite/client.js.map +1 -1
  239. package/package.json +2 -2
  240. package/src/templates/default/.agents/skills/real-time-sync/SKILL.md +2 -2
  241. package/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +2 -2
  242. package/src/templates/workspace-core/.agents/skills/secrets/SKILL.md +7 -3
  243. package/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +39 -9
@@ -1,6 +1,15 @@
1
1
  import { Button } from "@agent-native/toolkit/ui/button";
2
+ import {
3
+ Select,
4
+ SelectContent,
5
+ SelectGroup,
6
+ SelectItem,
7
+ SelectTrigger,
8
+ } from "@agent-native/toolkit/ui/select";
2
9
  import {
3
10
  IconAlertTriangle,
11
+ IconBrain,
12
+ IconLanguage,
4
13
  IconLoader2,
5
14
  IconMicrophone,
6
15
  IconPhoneOff,
@@ -9,14 +18,17 @@ import {
9
18
  } from "@tabler/icons-react";
10
19
  import {
11
20
  type MouseEvent,
21
+ useCallback,
12
22
  useEffect,
13
23
  useId,
24
+ useRef,
14
25
  useState,
15
26
  useSyncExternalStore,
16
27
  } from "react";
17
28
 
18
29
  import {
19
30
  Popover,
31
+ PopoverAnchor,
20
32
  PopoverContent,
21
33
  PopoverTrigger,
22
34
  } from "../components/ui/popover.js";
@@ -57,13 +69,39 @@ export interface RealtimeVoiceModeCopy {
57
69
  showChat: string;
58
70
  hideChat: string;
59
71
  endVoiceMode: string;
60
- microphoneSettings: string;
72
+ voiceSettings: string;
73
+ settings: {
74
+ microphone: string;
75
+ defaultMicrophone: string;
76
+ microphoneSwitchFailed: string;
77
+ language: string;
78
+ autoLanguage: string;
79
+ languages: Record<
80
+ Exclude<
81
+ import("./useRealtimeVoiceMode.js").RealtimeVoiceLanguage,
82
+ "auto"
83
+ >,
84
+ string
85
+ >;
86
+ intelligence: string;
87
+ intelligenceLevels: Record<
88
+ import("./useRealtimeVoiceMode.js").RealtimeVoiceIntelligence,
89
+ string
90
+ >;
91
+ voiceStyle: string;
92
+ voiceChangePending: string;
93
+ voiceDescriptions: Record<
94
+ import("./useRealtimeVoiceMode.js").RealtimeVoice,
95
+ string
96
+ >;
97
+ };
61
98
  status: Record<RealtimeVoiceModeState, string>;
62
99
  errors: {
63
100
  unsupported: string;
64
101
  responseFailed: string;
65
102
  sessionFailed: string;
66
103
  channelDisconnected: string;
104
+ connectionTimedOut?: string;
67
105
  connectionFailed: string;
68
106
  offerFailed: string;
69
107
  };
@@ -72,6 +110,7 @@ export interface RealtimeVoiceModeCopy {
72
110
  export interface RealtimeVoiceModeEntryProps {
73
111
  copy: RealtimeVoiceModeCopy;
74
112
  disabled?: boolean;
113
+ providerStatusPending?: boolean;
75
114
  open?: boolean;
76
115
  onOpenChange?: (open: boolean) => void;
77
116
  onStartVoiceMode: () => void;
@@ -92,6 +131,7 @@ export interface RealtimeVoiceModeEntryProps {
92
131
  export function RealtimeVoiceModeEntry({
93
132
  copy,
94
133
  disabled,
134
+ providerStatusPending = false,
95
135
  open: controlledOpen,
96
136
  onOpenChange,
97
137
  onStartVoiceMode,
@@ -107,6 +147,16 @@ export function RealtimeVoiceModeEntry({
107
147
  const open = controlledOpen ?? uncontrolledOpen;
108
148
  const titleId = useId();
109
149
  const descriptionId = useId();
150
+ const [collisionBoundary, setCollisionBoundary] =
151
+ useState<HTMLElement | null>(null);
152
+
153
+ const setTriggerNode = useCallback((node: HTMLButtonElement | null) => {
154
+ const nextBoundary =
155
+ node?.closest<HTMLElement>(".agent-panel-root") ?? null;
156
+ setCollisionBoundary((currentBoundary) =>
157
+ currentBoundary === nextBoundary ? currentBoundary : nextBoundary,
158
+ );
159
+ }, []);
110
160
 
111
161
  const setOpen = (nextOpen: boolean) => {
112
162
  if (controlledOpen === undefined) setUncontrolledOpen(nextOpen);
@@ -124,6 +174,7 @@ export function RealtimeVoiceModeEntry({
124
174
  <TooltipTrigger asChild>
125
175
  <PopoverTrigger asChild>
126
176
  <Button
177
+ ref={setTriggerNode}
127
178
  type="button"
128
179
  variant="ghost"
129
180
  size="icon"
@@ -146,11 +197,14 @@ export function RealtimeVoiceModeEntry({
146
197
  side="top"
147
198
  align="end"
148
199
  sideOffset={10}
200
+ collisionBoundary={collisionBoundary ?? undefined}
201
+ collisionPadding={16}
202
+ data-collision-boundary={collisionBoundary ? "agent-panel" : "viewport"}
149
203
  className={cn(
150
204
  "p-4",
151
205
  setupRequired
152
- ? "w-[min(30rem,calc(100vw-2rem))]"
153
- : "w-[min(22rem,calc(100vw-2rem))]",
206
+ ? "w-[min(calc(100vw-2rem),var(--radix-popover-content-available-width,30rem),30rem)]"
207
+ : "w-[min(calc(100vw-2rem),var(--radix-popover-content-available-width,22rem),22rem)]",
154
208
  )}
155
209
  aria-labelledby={titleId}
156
210
  aria-describedby={descriptionId}
@@ -168,7 +222,7 @@ export function RealtimeVoiceModeEntry({
168
222
  </p>
169
223
  </div>
170
224
 
171
- <div className="flex flex-col-reverse gap-2 sm:flex-row sm:flex-nowrap sm:justify-end">
225
+ <div className="flex flex-col-reverse gap-2 sm:flex-row sm:flex-wrap sm:justify-end">
172
226
  <Button
173
227
  type="button"
174
228
  variant="ghost"
@@ -206,9 +260,14 @@ export function RealtimeVoiceModeEntry({
206
260
  <Button
207
261
  type="button"
208
262
  size="sm"
263
+ disabled={providerStatusPending}
209
264
  onClick={() => choose(onStartVoiceMode)}
210
265
  >
211
- <IconMicrophone />
266
+ {providerStatusPending ? (
267
+ <IconLoader2 className="animate-spin" />
268
+ ) : (
269
+ <IconMicrophone />
270
+ )}
212
271
  {copy.startVoiceMode}
213
272
  </Button>
214
273
  )}
@@ -226,11 +285,35 @@ export interface RealtimeVoiceModeDockProps {
226
285
  audioLevels?: RealtimeVoiceAudioLevelStore;
227
286
  onToggleChat: () => void;
228
287
  onEndVoiceMode: () => void;
229
- onOpenMicrophoneSettings?: () => void;
288
+ settings?: RealtimeVoiceModeInlineSettings;
230
289
  errorMessage?: string | null;
231
290
  className?: string;
232
291
  }
233
292
 
293
+ export interface RealtimeVoiceModeSettingOption {
294
+ value: string;
295
+ label: string;
296
+ description?: string;
297
+ }
298
+
299
+ export interface RealtimeVoiceModeSelectSetting {
300
+ label: string;
301
+ value: string;
302
+ options: readonly RealtimeVoiceModeSettingOption[];
303
+ onValueChange: (value: string) => void;
304
+ disabled?: boolean;
305
+ }
306
+
307
+ export interface RealtimeVoiceModeInlineSettings {
308
+ dialogLabel: string;
309
+ appliesNextConversationNote?: string;
310
+ microphoneError?: string;
311
+ microphone: RealtimeVoiceModeSelectSetting;
312
+ language: RealtimeVoiceModeSelectSetting;
313
+ intelligence: RealtimeVoiceModeSelectSetting;
314
+ voiceStyle: RealtimeVoiceModeSelectSetting;
315
+ }
316
+
234
317
  const SILENT_AUDIO_LEVELS = createRealtimeVoiceAudioLevelStore();
235
318
  const WAVEFORM_WEIGHTS = [0.55, 0.82, 1, 0.82, 0.55];
236
319
  const AUDIO_ACTIVITY_THRESHOLD = 0.035;
@@ -255,7 +338,7 @@ function VoiceWaveform({
255
338
  }: {
256
339
  level: number;
257
340
  reducedMotion: boolean;
258
- activity: "user" | "assistant";
341
+ activity: "idle" | "user" | "assistant";
259
342
  }) {
260
343
  const visibleLevel = reducedMotion ? 0.45 : level;
261
344
  return (
@@ -269,45 +352,265 @@ function VoiceWaveform({
269
352
  <span
270
353
  key={index}
271
354
  className="h-5 w-0.5 origin-center rounded-full bg-current transition-transform duration-75 ease-out motion-reduce:transition-none"
272
- style={{ transform: `scaleY(${0.2 + visibleLevel * 0.8 * weight})` }}
355
+ style={{
356
+ transform: `scaleY(${0.2 + visibleLevel * 0.8 * weight})`,
357
+ }}
273
358
  />
274
359
  ))}
275
360
  </span>
276
361
  );
277
362
  }
278
363
 
364
+ function VoiceConnectingIndicator() {
365
+ return (
366
+ <IconLoader2
367
+ aria-hidden="true"
368
+ className="size-6 animate-spin motion-reduce:animate-none"
369
+ data-realtime-voice-connecting-indicator="true"
370
+ />
371
+ );
372
+ }
373
+
279
374
  const ORB_STATE_CLASSES: Record<RealtimeVoiceModeState, string> = {
280
375
  connecting:
281
- "bg-secondary text-secondary-foreground ring-secondary/40 hover:bg-secondary/80",
376
+ "bg-background/65 text-foreground ring-border/60 hover:bg-background/75",
282
377
  listening:
283
- "bg-primary text-primary-foreground ring-primary/20 hover:bg-primary/90",
378
+ "bg-background/65 text-foreground ring-border/60 hover:bg-background/75",
284
379
  speaking:
285
- "bg-foreground text-background ring-foreground/20 hover:bg-foreground/90",
380
+ "bg-background/65 text-foreground ring-border/60 hover:bg-background/75",
286
381
  working:
287
- "bg-secondary text-secondary-foreground ring-secondary/40 hover:bg-secondary/80",
382
+ "bg-background/65 text-foreground ring-border/60 hover:bg-background/75",
288
383
  error:
289
- "bg-destructive text-destructive-foreground ring-destructive/20 hover:bg-destructive/90",
290
- ending: "cursor-wait bg-muted text-muted-foreground ring-muted/40",
384
+ "bg-destructive/20 text-destructive ring-destructive/30 hover:bg-destructive/25",
385
+ ending: "cursor-wait bg-background/65 text-muted-foreground ring-border/60",
291
386
  };
292
387
 
293
- function VoiceStateIcon({ state }: { state: RealtimeVoiceModeState }) {
294
- switch (state) {
295
- case "connecting":
296
- return (
297
- <IconLoader2 className="animate-spin motion-reduce:animate-none" />
298
- );
299
- case "listening":
300
- return <IconMicrophone />;
301
- case "speaking":
302
- return <IconVolume />;
303
- case "working":
304
- case "ending":
305
- return (
306
- <IconLoader2 className="animate-spin motion-reduce:animate-none" />
388
+ function useChatPanelTranslation(chatVisible: boolean): number {
389
+ const [translation, setTranslation] = useState(0);
390
+
391
+ useEffect(() => {
392
+ if (!chatVisible || typeof window === "undefined") {
393
+ setTranslation(0);
394
+ return;
395
+ }
396
+
397
+ const direction = window.getComputedStyle(
398
+ document.documentElement,
399
+ ).direction;
400
+ const inlineEnd = direction === "rtl" ? "left" : "right";
401
+
402
+ const update = () => {
403
+ const panels = Array.from(
404
+ document.querySelectorAll<HTMLElement>(
405
+ `.agent-sidebar-panel[data-agent-sidebar-position="${inlineEnd}"][data-agent-sidebar-state="open"]`,
406
+ ),
307
407
  );
308
- case "error":
309
- return <IconAlertTriangle />;
310
- }
408
+ const panel = panels.find((candidate) => {
409
+ const rect = candidate.getBoundingClientRect();
410
+ return rect.width > 0 && rect.height > 0;
411
+ });
412
+
413
+ if (!panel) {
414
+ setTranslation(0);
415
+ return;
416
+ }
417
+
418
+ // A fullscreen chat has no unobscured side to move into. Keep the dock
419
+ // at its normal edge so it stays reachable instead of pinning it to the
420
+ // opposite side of the same overlay.
421
+ if (panel.dataset.agentSidebarLayout === "fullscreen") {
422
+ setTranslation(0);
423
+ return;
424
+ }
425
+
426
+ const rect = panel.getBoundingClientRect();
427
+ const overlap =
428
+ inlineEnd === "right"
429
+ ? Math.max(0, window.innerWidth - rect.left)
430
+ : Math.max(0, rect.right);
431
+ const maximumShift = Math.max(0, window.innerWidth - 96);
432
+ const distance = Math.min(overlap, maximumShift);
433
+ setTranslation(inlineEnd === "right" ? -distance : distance);
434
+ };
435
+
436
+ update();
437
+ const frame = window.requestAnimationFrame(update);
438
+ window.addEventListener("resize", update);
439
+
440
+ const resizeObserver =
441
+ typeof ResizeObserver === "undefined" ? null : new ResizeObserver(update);
442
+ const observedPanels = new WeakSet<HTMLElement>();
443
+ const observePanels = () => {
444
+ document
445
+ .querySelectorAll<HTMLElement>(".agent-sidebar-panel")
446
+ .forEach((panel) => {
447
+ if (!observedPanels.has(panel)) {
448
+ observedPanels.add(panel);
449
+ resizeObserver?.observe(panel);
450
+ }
451
+ });
452
+ };
453
+ observePanels();
454
+
455
+ const mutationObserver = new MutationObserver(() => {
456
+ observePanels();
457
+ update();
458
+ });
459
+ mutationObserver.observe(document.body, {
460
+ attributes: true,
461
+ attributeFilter: [
462
+ "data-agent-sidebar-layout",
463
+ "data-agent-sidebar-state",
464
+ "style",
465
+ ],
466
+ childList: true,
467
+ subtree: true,
468
+ });
469
+
470
+ return () => {
471
+ window.cancelAnimationFrame(frame);
472
+ window.removeEventListener("resize", update);
473
+ resizeObserver?.disconnect();
474
+ mutationObserver.disconnect();
475
+ };
476
+ }, [chatVisible]);
477
+
478
+ return translation;
479
+ }
480
+
481
+ function VoiceSettingRow({
482
+ icon: Icon,
483
+ setting,
484
+ onSelectOpenChange,
485
+ }: {
486
+ icon: typeof IconLanguage;
487
+ setting: RealtimeVoiceModeSelectSetting;
488
+ onSelectOpenChange: (open: boolean) => void;
489
+ }) {
490
+ const selected = setting.options.find(
491
+ (option) => option.value === setting.value,
492
+ );
493
+
494
+ return (
495
+ <Select
496
+ value={setting.value}
497
+ onValueChange={setting.onValueChange}
498
+ onOpenChange={onSelectOpenChange}
499
+ disabled={setting.disabled}
500
+ >
501
+ <SelectTrigger
502
+ aria-label={`${setting.label}: ${selected?.label ?? setting.value}`}
503
+ className="h-11 rounded-none border-0 bg-transparent px-3 shadow-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset"
504
+ >
505
+ <div className="flex min-w-0 flex-1 items-center gap-2.5">
506
+ <Icon className="size-4 shrink-0 text-muted-foreground" />
507
+ <span className="shrink-0 text-sm font-medium text-foreground">
508
+ {setting.label}
509
+ </span>
510
+ <span className="ms-auto min-w-0 truncate text-end text-sm text-muted-foreground">
511
+ {selected?.label ?? setting.value}
512
+ </span>
513
+ </div>
514
+ </SelectTrigger>
515
+ <SelectContent
516
+ align="end"
517
+ className="min-w-56"
518
+ data-realtime-voice-setting-options="true"
519
+ >
520
+ <SelectGroup>
521
+ {setting.options.map((option) => (
522
+ <SelectItem key={option.value} value={option.value}>
523
+ <span className="grid gap-0.5">
524
+ <span>{option.label}</span>
525
+ {option.description ? (
526
+ <span className="text-xs text-muted-foreground">
527
+ {option.description}
528
+ </span>
529
+ ) : null}
530
+ </span>
531
+ </SelectItem>
532
+ ))}
533
+ </SelectGroup>
534
+ </SelectContent>
535
+ </Select>
536
+ );
537
+ }
538
+
539
+ function VoiceInlineSettings({
540
+ settings,
541
+ disabled,
542
+ onSelectOpenChange,
543
+ }: {
544
+ settings: RealtimeVoiceModeInlineSettings;
545
+ disabled: boolean;
546
+ onSelectOpenChange: (open: boolean) => void;
547
+ }) {
548
+ const selectedVoice = settings.voiceStyle.options.find(
549
+ (option) => option.value === settings.voiceStyle.value,
550
+ );
551
+
552
+ return (
553
+ <div data-realtime-voice-settings="true">
554
+ <h2 className="sr-only">{settings.dialogLabel}</h2>
555
+ <div className="grid gap-0.5 px-4 pb-3 pt-4 text-center">
556
+ <div className="truncate text-base font-semibold text-foreground">
557
+ {selectedVoice?.label ?? settings.voiceStyle.value}
558
+ </div>
559
+ {selectedVoice?.description ? (
560
+ <div className="truncate text-xs text-muted-foreground">
561
+ {selectedVoice.description}
562
+ </div>
563
+ ) : null}
564
+ </div>
565
+ <div className="mx-2 mb-2 overflow-hidden rounded-xl border border-border/70 bg-background/70">
566
+ <VoiceSettingRow
567
+ icon={IconMicrophone}
568
+ setting={{
569
+ ...settings.microphone,
570
+ disabled: disabled || settings.microphone.disabled,
571
+ }}
572
+ onSelectOpenChange={onSelectOpenChange}
573
+ />
574
+ <div className="mx-3 h-px bg-border/70" />
575
+ <VoiceSettingRow
576
+ icon={IconLanguage}
577
+ setting={{
578
+ ...settings.language,
579
+ disabled: disabled || settings.language.disabled,
580
+ }}
581
+ onSelectOpenChange={onSelectOpenChange}
582
+ />
583
+ <div className="mx-3 h-px bg-border/70" />
584
+ <VoiceSettingRow
585
+ icon={IconBrain}
586
+ setting={{
587
+ ...settings.intelligence,
588
+ disabled: disabled || settings.intelligence.disabled,
589
+ }}
590
+ onSelectOpenChange={onSelectOpenChange}
591
+ />
592
+ <div className="mx-3 h-px bg-border/70" />
593
+ <VoiceSettingRow
594
+ icon={IconVolume}
595
+ setting={{
596
+ ...settings.voiceStyle,
597
+ disabled: disabled || settings.voiceStyle.disabled,
598
+ }}
599
+ onSelectOpenChange={onSelectOpenChange}
600
+ />
601
+ </div>
602
+ {settings.microphoneError ? (
603
+ <p className="px-4 pb-3 text-center text-xs text-destructive">
604
+ {settings.microphoneError}
605
+ </p>
606
+ ) : null}
607
+ {settings.appliesNextConversationNote ? (
608
+ <p className="px-4 pb-3 text-center text-xs text-muted-foreground">
609
+ {settings.appliesNextConversationNote}
610
+ </p>
611
+ ) : null}
612
+ </div>
613
+ );
311
614
  }
312
615
 
313
616
  /**
@@ -321,32 +624,69 @@ export function RealtimeVoiceModeDock({
321
624
  audioLevels = SILENT_AUDIO_LEVELS,
322
625
  onToggleChat,
323
626
  onEndVoiceMode,
324
- onOpenMicrophoneSettings,
627
+ settings,
325
628
  errorMessage,
326
629
  className,
327
630
  }: RealtimeVoiceModeDockProps) {
328
631
  const statusId = useId();
329
632
  const controlsId = useId();
330
633
  const [controlsOpen, setControlsOpen] = useState(false);
634
+ const [settingsOpen, setSettingsOpen] = useState(false);
635
+ const selectInteractionRef = useRef(false);
636
+ const selectInteractionFrameRef = useRef<number | null>(null);
331
637
  const levels = useSyncExternalStore(
332
638
  audioLevels.subscribe,
333
639
  audioLevels.getSnapshot,
334
640
  audioLevels.getSnapshot,
335
641
  );
336
642
  const reducedMotion = usePrefersReducedMotion();
337
- const activity =
338
- levels.output > AUDIO_ACTIVITY_THRESHOLD
643
+ // Microphone metering begins while the SDP request is still in flight. Keep
644
+ // the connecting affordance authoritative until WebRTC is established;
645
+ // otherwise speaking into the mic replaces the loader with a waveform and
646
+ // makes a stalled connection look like a live call.
647
+ const connected =
648
+ state === "listening" || state === "speaking" || state === "working";
649
+ const activity = connected
650
+ ? levels.output > AUDIO_ACTIVITY_THRESHOLD
339
651
  ? "assistant"
340
652
  : levels.input > AUDIO_ACTIVITY_THRESHOLD
341
653
  ? "user"
342
- : "idle";
654
+ : "idle"
655
+ : "idle";
343
656
  const activityLevel = activity === "assistant" ? levels.output : levels.input;
344
657
  const toggleLabel = chatVisible ? copy.hideChat : copy.showChat;
345
658
  const ending = state === "ending";
346
659
  const errorDetailVisible = state === "error" && Boolean(errorMessage);
347
- const orbScale = reducedMotion ? 1 : 1 + Math.min(activityLevel, 1) * 0.07;
660
+ const controlsVisible = controlsOpen || settingsOpen;
661
+ const chatPanelTranslation = useChatPanelTranslation(chatVisible);
662
+
663
+ const handleSelectOpenChange = useCallback((open: boolean) => {
664
+ if (selectInteractionFrameRef.current !== null) {
665
+ window.cancelAnimationFrame(selectInteractionFrameRef.current);
666
+ selectInteractionFrameRef.current = null;
667
+ }
668
+ selectInteractionRef.current = true;
669
+ if (!open) {
670
+ // Radix closes the portalled Select before its focus/outside events have
671
+ // fully settled. Keep the parent protected through the current frame.
672
+ selectInteractionFrameRef.current = window.requestAnimationFrame(() => {
673
+ selectInteractionRef.current = false;
674
+ selectInteractionFrameRef.current = null;
675
+ });
676
+ }
677
+ }, []);
678
+
679
+ useEffect(
680
+ () => () => {
681
+ if (selectInteractionFrameRef.current !== null) {
682
+ window.cancelAnimationFrame(selectInteractionFrameRef.current);
683
+ }
684
+ },
685
+ [],
686
+ );
348
687
 
349
688
  const closeControlsUnlessFocused = (event: MouseEvent<HTMLDivElement>) => {
689
+ if (settingsOpen) return;
350
690
  if (!event.currentTarget.contains(document.activeElement)) {
351
691
  setControlsOpen(false);
352
692
  }
@@ -355,12 +695,16 @@ export function RealtimeVoiceModeDock({
355
695
  return (
356
696
  <div
357
697
  className={cn(
358
- "pointer-events-none fixed bottom-4 end-4 flex max-w-[calc(100vw-2rem)] flex-col items-end gap-2",
698
+ "pointer-events-none fixed bottom-4 end-4 flex max-w-[calc(100vw-2rem)] flex-col items-end gap-2 transition-transform duration-200 ease-[var(--ease-collapse)] motion-reduce:transition-none",
359
699
  className,
360
700
  )}
361
- style={{ zIndex: 270 }}
701
+ style={{
702
+ zIndex: 270,
703
+ transform: `translateX(${chatPanelTranslation}px)`,
704
+ }}
362
705
  data-realtime-voice-state={state}
363
706
  data-realtime-voice-activity={activity}
707
+ data-realtime-voice-chat-offset={chatPanelTranslation}
364
708
  >
365
709
  {errorDetailVisible ? (
366
710
  <div
@@ -386,42 +730,72 @@ export function RealtimeVoiceModeDock({
386
730
  >
387
731
  <div
388
732
  id={controlsId}
389
- data-realtime-voice-controls={controlsOpen ? "open" : "closed"}
733
+ data-realtime-voice-controls={controlsVisible ? "open" : "closed"}
390
734
  className={cn(
391
- "flex items-center gap-1 rounded-full border border-border/70 bg-background/95 p-1 ps-3 shadow-lg backdrop-blur-md transition-[transform,opacity] duration-150 ease-out motion-reduce:transition-none",
392
- controlsOpen
735
+ "flex items-center gap-1 rounded-full border border-border/70 bg-background/95 p-1 shadow-lg backdrop-blur-md transition-[transform,opacity] duration-150 ease-out motion-reduce:transition-none",
736
+ controlsVisible
393
737
  ? "pointer-events-auto translate-x-0 opacity-100"
394
738
  : "pointer-events-none opacity-0 ltr:translate-x-2 rtl:-translate-x-2 group-hover:pointer-events-auto group-hover:translate-x-0 group-hover:opacity-100 group-focus-within:pointer-events-auto group-focus-within:translate-x-0 group-focus-within:opacity-100",
395
739
  )}
396
740
  >
397
- <div
741
+ <span
398
742
  id={statusId}
399
743
  role={state === "error" && !errorDetailVisible ? "alert" : "status"}
400
744
  aria-live={
401
745
  state === "error" && !errorDetailVisible ? "assertive" : "polite"
402
746
  }
403
- className="me-1 whitespace-nowrap text-xs font-medium text-foreground"
747
+ className="sr-only"
404
748
  >
405
749
  {copy.status[state]}
406
- </div>
750
+ </span>
407
751
 
408
- {onOpenMicrophoneSettings ? (
409
- <Tooltip>
410
- <TooltipTrigger asChild>
411
- <Button
412
- type="button"
413
- variant="ghost"
414
- size="icon"
752
+ {settings ? (
753
+ <Popover open={settingsOpen} onOpenChange={setSettingsOpen}>
754
+ <Tooltip>
755
+ <TooltipTrigger asChild>
756
+ <PopoverAnchor asChild>
757
+ <Button
758
+ type="button"
759
+ variant="ghost"
760
+ size="icon"
761
+ disabled={ending}
762
+ onClick={() => setSettingsOpen((open) => !open)}
763
+ aria-label={copy.voiceSettings}
764
+ aria-expanded={settingsOpen}
765
+ className="size-8 rounded-full text-muted-foreground transition-transform duration-150 ease-out active:scale-[0.97]"
766
+ >
767
+ <IconSettings />
768
+ </Button>
769
+ </PopoverAnchor>
770
+ </TooltipTrigger>
771
+ <TooltipContent>{copy.voiceSettings}</TooltipContent>
772
+ </Tooltip>
773
+ <PopoverContent
774
+ side="top"
775
+ align="end"
776
+ sideOffset={10}
777
+ className="w-[min(20rem,calc(100vw-2rem))] overflow-hidden p-0"
778
+ onOpenAutoFocus={(event) => event.preventDefault()}
779
+ onInteractOutside={(event) => {
780
+ const target = event.target;
781
+ if (
782
+ selectInteractionRef.current ||
783
+ (target instanceof Element &&
784
+ target.closest(
785
+ '[data-realtime-voice-setting-options="true"]',
786
+ ))
787
+ ) {
788
+ event.preventDefault();
789
+ }
790
+ }}
791
+ >
792
+ <VoiceInlineSettings
793
+ settings={settings}
415
794
  disabled={ending}
416
- onClick={onOpenMicrophoneSettings}
417
- aria-label={copy.microphoneSettings}
418
- className="size-8 rounded-full text-muted-foreground transition-transform duration-150 ease-out active:scale-[0.97]"
419
- >
420
- <IconSettings />
421
- </Button>
422
- </TooltipTrigger>
423
- <TooltipContent>{copy.microphoneSettings}</TooltipContent>
424
- </Tooltip>
795
+ onSelectOpenChange={handleSelectOpenChange}
796
+ />
797
+ </PopoverContent>
798
+ </Popover>
425
799
  ) : null}
426
800
 
427
801
  <Tooltip>
@@ -454,43 +828,26 @@ export function RealtimeVoiceModeDock({
454
828
  }}
455
829
  aria-label={toggleLabel}
456
830
  aria-pressed={chatVisible}
831
+ data-realtime-voice-chat-toggle="orb"
457
832
  aria-describedby={statusId}
458
833
  aria-controls={controlsId}
459
- aria-expanded={controlsOpen}
834
+ aria-expanded={controlsVisible}
460
835
  className={cn(
461
- "relative isolate size-16 overflow-visible rounded-full ring-4 shadow-xl transition-transform duration-150 ease-out focus-visible:ring-offset-2 active:scale-[0.97] motion-reduce:transition-none",
836
+ "relative isolate size-16 overflow-visible rounded-full ring-1 backdrop-blur-xl transition-transform duration-150 ease-out focus-visible:ring-offset-2 active:scale-[0.97] motion-reduce:transition-none",
462
837
  ORB_STATE_CLASSES[state],
463
838
  )}
464
839
  >
465
- <span
466
- aria-hidden="true"
467
- className={cn(
468
- "pointer-events-none absolute -inset-2 -z-10 rounded-full bg-current opacity-0 blur-md transition-[transform,opacity] duration-150 ease-out motion-reduce:transition-none",
469
- activity !== "idle" && "opacity-20",
470
- )}
471
- style={{ transform: `scale(${orbScale})` }}
472
- />
473
- <span
474
- aria-hidden="true"
475
- className="pointer-events-none absolute inset-1 overflow-hidden rounded-full bg-gradient-to-br from-background/70 via-background/15 to-transparent opacity-70"
476
- >
477
- <span className="absolute start-2.5 top-2 size-3 rounded-full bg-background/70 blur-[1px]" />
478
- <span
479
- className="absolute -bottom-2 -end-1 h-8 w-10 rounded-full bg-background/25 blur-md transition-transform duration-150 ease-out motion-reduce:transition-none"
480
- style={{
481
- transform: `scale(${reducedMotion ? 1 : 1 + activityLevel * 0.25})`,
482
- }}
483
- />
484
- </span>
485
840
  <span className="relative z-10 flex items-center justify-center">
486
- {activity === "idle" ? (
487
- <VoiceStateIcon state={state} />
488
- ) : (
841
+ {state === "connecting" ? (
842
+ <VoiceConnectingIndicator />
843
+ ) : state !== "error" ? (
489
844
  <VoiceWaveform
490
845
  level={activityLevel}
491
846
  reducedMotion={reducedMotion}
492
847
  activity={activity}
493
848
  />
849
+ ) : (
850
+ <IconAlertTriangle />
494
851
  )}
495
852
  </span>
496
853
  </Button>