From ed31305ced90fc13109652c2488863af2b4442ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=BA=A6=E4=B8=9C?= Date: Tue, 2 Jun 2026 03:21:56 +0800 Subject: [PATCH 1/4] feat(mobile): add agent voice call flow Co-authored-by: multica-agent --- apps/mobile/app.config.ts | 2 +- .../app/(app)/[workspace]/(tabs)/chat.tsx | 20 +- apps/mobile/components/chat/chat-composer.tsx | 288 +++-------------- .../components/chat/voice-call-panel.tsx | 305 ++++++++++++++++++ .../components/composer/message-composer.tsx | 47 +-- apps/mobile/data/api.ts | 5 +- apps/mobile/package.json | 2 +- 7 files changed, 394 insertions(+), 275 deletions(-) create mode 100644 apps/mobile/components/chat/voice-call-panel.tsx diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index ec4a7861cf2..19cdd5c8b1a 100644 --- a/apps/mobile/app.config.ts +++ b/apps/mobile/app.config.ts @@ -60,7 +60,7 @@ export default ({ config }: ConfigContext): ExpoConfig => { "expo-audio", { microphonePermission: - "Allow Multica to record short voice messages for Agent chat transcription.", + "Allow Multica to use the microphone for voice calls with agents.", }, ], "expo-secure-store", diff --git a/apps/mobile/app/(app)/[workspace]/(tabs)/chat.tsx b/apps/mobile/app/(app)/[workspace]/(tabs)/chat.tsx index fcb343862f6..730a411e6fe 100644 --- a/apps/mobile/app/(app)/[workspace]/(tabs)/chat.tsx +++ b/apps/mobile/app/(app)/[workspace]/(tabs)/chat.tsx @@ -76,6 +76,7 @@ import { ChatTitleButton } from "@/components/chat/chat-title-button"; import { ChatSessionActions } from "@/components/chat/chat-session-actions"; import { ChatMessageList } from "@/components/chat/chat-message-list"; import { ChatComposer } from "@/components/chat/chat-composer"; +import { VoiceCallPanel } from "@/components/chat/voice-call-panel"; import { AgentPickerSheet } from "@/components/chat/agent-picker-sheet"; import { NoAgentBanner } from "@/components/chat/no-agent-banner"; import { OfflineBanner } from "@/components/chat/offline-banner"; @@ -90,6 +91,7 @@ export default function ChatTab() { const [activeSessionId, setActiveSessionId] = useState(null); const [selectedAgentId, setSelectedAgentId] = useState(null); const [agentPickerOpen, setAgentPickerOpen] = useState(false); + const [voiceCallOpen, setVoiceCallOpen] = useState(false); // Bridge to the chat-sessions formSheet route. Mirror local // activeSessionId into the store so the picker can render the current @@ -317,6 +319,7 @@ export default function ChatTab() { // ── Header / sheet actions ───────────────────────────────────────────── const handleNewChat = useCallback(() => { + setVoiceCallOpen(false); if (availableAgents.length > 1) { setAgentPickerOpen(true); return; @@ -326,6 +329,7 @@ export default function ChatTab() { }, [availableAgents.length]); const handlePickAgent = useCallback((agent: Agent) => { + setVoiceCallOpen(false); setSelectedAgentId(agent.id); setActiveSessionId(null); }, []); @@ -334,6 +338,7 @@ export default function ChatTab() { // when they delete the active one in the sheet). useEffect(() => { if (!selectRequest) return; + setVoiceCallOpen(false); setSelectedAgentId(null); setActiveSessionId(selectRequest.id); consumeSelect(); @@ -351,6 +356,7 @@ export default function ChatTab() { style: "destructive", onPress: () => { const id = activeSession.id; + setVoiceCallOpen(false); setActiveSessionId(null); deleteSession.mutate(id); }, @@ -414,12 +420,24 @@ export default function ChatTab() { agentName={currentAgent?.name} availability={presenceAvailability} /> + {voiceCallOpen && currentAgent ? ( + setVoiceCallOpen(false)} + /> + ) : null} setDraft(draftKey, next)} onSend={handleSend} onStop={handleStop} - latestAssistantMessage={latestAssistantMessage} + onVoiceCall={() => setVoiceCallOpen(true)} sending={sending} disabled={disabled} disabledReason={disabledReason} diff --git a/apps/mobile/components/chat/chat-composer.tsx b/apps/mobile/components/chat/chat-composer.tsx index 0e91d1c87b7..da2c53ed4b2 100644 --- a/apps/mobile/components/chat/chat-composer.tsx +++ b/apps/mobile/components/chat/chat-composer.tsx @@ -9,6 +9,9 @@ * session, `sending` flips true and we replace the Send button slot * with a Stop affordance (filled foreground bg + stop glyph). Tap → * `onStop()` cancels the in-flight task. + * - **Voice call entry**: when `onVoiceCall` is provided a round phone + * button renders as a leading action. Tap → chat.tsx opens the + * ``. * - **Mention picker mode=chat**: chat is user ↔ single agent so * @member / @agent / @squad / @all are noise + would notify the * wrong people. Picker route honors `?mode=chat` and surfaces only @@ -26,28 +29,15 @@ * Previously a hand-written 400-LOC twin of inline-comment-composer.tsx; * now ~50 LOC plus the StopButton subcomponent. */ -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { Alert, AppState, Pressable, View } from "react-native"; +import { useCallback } from "react"; +import { Pressable, View } from "react-native"; import Animated, { FadeIn, FadeOut } from "react-native-reanimated"; import { Ionicons } from "@expo/vector-icons"; import * as Haptics from "expo-haptics"; -import { - RecordingPresets, - requestRecordingPermissionsAsync, - setAudioModeAsync, - useAudioPlayer, - useAudioPlayerStatus, - useAudioRecorder, - useAudioRecorderState, -} from "expo-audio"; -import type { ChatMessage } from "@multica/core/types"; import { MessageComposer } from "@/components/composer/message-composer"; -import { api } from "@/data/api"; import { useWorkspaceStore } from "@/data/workspace-store"; import { useColorScheme } from "@/lib/use-color-scheme"; import { THEME } from "@/lib/theme"; -import { Text } from "@/components/ui/text"; -import { IconButton } from "@/components/ui/icon-button"; interface Props { /** Current draft text (controlled). Empty string = no draft. */ @@ -60,7 +50,8 @@ interface Props { onSend: (content: string, attachmentIds: string[]) => Promise | void; /** Cancel the in-flight agent task. Only callable while `sending===true`. */ onStop: () => void; - latestAssistantMessage?: ChatMessage | null; + /** Open the call-like voice session UI. */ + onVoiceCall?: () => void; /** True while an agent task is running for the active session. The * composer swaps Send for Stop. */ sending: boolean; @@ -78,42 +69,14 @@ export function ChatComposer({ onChangeText, onSend, onStop, - latestAssistantMessage, + onVoiceCall, sending, disabled = false, disabledReason, }: Props) { const wsSlug = useWorkspaceStore((s) => s.currentWorkspaceSlug); - const [voiceState, setVoiceState] = useState< - "idle" | "recording" | "transcribing" | "sending" | "synthesizing" | "playing" | "failed" - >("idle"); - const [voiceError, setVoiceError] = useState(null); - const [lastPlayedAssistantId, setLastPlayedAssistantId] = useState(null); - const autoPlayPendingRef = useRef(false); - const autoPlayAfterMessageIdRef = useRef(null); - const recorder = useAudioRecorder(RecordingPresets.LOW_QUALITY); - const recorderState = useAudioRecorderState(recorder, 250); - const player = useAudioPlayer(null, { updateInterval: 250 }); - const playerStatus = useAudioPlayerStatus(player); - - const statusLabel = useMemo(() => { - switch (voiceState) { - case "recording": - return `Recording ${Math.round(recorderState.durationMillis / 1000)}s`; - case "transcribing": - return "Transcribing…"; - case "sending": - return "Sending transcript…"; - case "synthesizing": - return "Preparing audio…"; - case "playing": - return "Playing reply…"; - case "failed": - return voiceError ?? "Voice action failed"; - default: - return null; - } - }, [recorderState.durationMillis, voiceError, voiceState]); + const { colorScheme } = useColorScheme(); + const theme = THEME[colorScheme]; const onSubmit = useCallback( async ({ @@ -137,153 +100,8 @@ export function ChatComposer({ onStop(); }, [onStop]); - const startRecording = useCallback(async () => { - if (disabled || sending || voiceState !== "idle") return; - setVoiceError(null); - const permission = await requestRecordingPermissionsAsync(); - if (!permission.granted) { - setVoiceState("failed"); - setVoiceError("Microphone permission is required."); - Alert.alert( - "Microphone access needed", - "Enable microphone access in Settings to send voice messages.", - ); - return; - } - await setAudioModeAsync({ - allowsRecording: true, - playsInSilentMode: true, - }); - await recorder.prepareToRecordAsync(); - recorder.record({ forDuration: 60 }); - setVoiceState("recording"); - }, [disabled, recorder, sending, voiceState]); - - const stopRecordingAndSend = useCallback(async () => { - if (voiceState !== "recording") return; - try { - await recorder.stop(); - await setAudioModeAsync({ - allowsRecording: false, - playsInSilentMode: true, - }); - const uri = recorder.uri; - if (!uri) { - throw new Error("Recording did not produce an audio file."); - } - setVoiceState("transcribing"); - const { transcript } = await api.transcribeSpeech({ - uri, - name: "voice-message.m4a", - type: "audio/m4a", - }); - const trimmed = transcript.trim(); - if (!trimmed) { - throw new Error("No speech was detected."); - } - setVoiceState("sending"); - autoPlayPendingRef.current = true; - autoPlayAfterMessageIdRef.current = latestAssistantMessage?.id ?? null; - await onSend(trimmed, []); - setVoiceState("idle"); - } catch (err) { - autoPlayPendingRef.current = false; - autoPlayAfterMessageIdRef.current = null; - setVoiceState("failed"); - setVoiceError(err instanceof Error ? err.message : "Voice message failed."); - } - }, [latestAssistantMessage?.id, onSend, recorder, voiceState]); - - const toggleRecording = useCallback(() => { - if (voiceState === "recording") { - void stopRecordingAndSend(); - return; - } - void startRecording(); - }, [startRecording, stopRecordingAndSend, voiceState]); - - const playAssistant = useCallback( - async (message?: ChatMessage | null) => { - const target = message ?? latestAssistantMessage; - if (!target?.content.trim()) return; - try { - setVoiceError(null); - setVoiceState("synthesizing"); - const audio = await api.synthesizeSpeech(target.content); - player.replace({ - uri: `data:${audio.content_type};base64,${audio.audio_base64}`, - }); - await player.seekTo(0); - player.play(); - setLastPlayedAssistantId(target.id); - } catch (err) { - setVoiceState("failed"); - setVoiceError(err instanceof Error ? err.message : "Playback failed."); - } - }, - [latestAssistantMessage, player], - ); - - const retryVoice = useCallback(() => { - setVoiceError(null); - setVoiceState("idle"); - }, []); - - useEffect(() => { - if (playerStatus.playing) { - setVoiceState((current) => - current === "synthesizing" || current === "idle" ? "playing" : current, - ); - return; - } - if (playerStatus.didJustFinish) { - setVoiceState("idle"); - } - }, [playerStatus.didJustFinish, playerStatus.playing]); - - useEffect(() => { - if (!autoPlayPendingRef.current) return; - if (!latestAssistantMessage) return; - if (latestAssistantMessage.id === autoPlayAfterMessageIdRef.current) return; - if (latestAssistantMessage.id === lastPlayedAssistantId) return; - autoPlayPendingRef.current = false; - autoPlayAfterMessageIdRef.current = null; - void playAssistant(latestAssistantMessage); - }, [latestAssistantMessage, lastPlayedAssistantId, playAssistant]); - - useEffect(() => { - if (voiceState !== "recording") return; - const subscription = AppState.addEventListener("change", (nextState) => { - if (nextState === "active") return; - void recorder.stop().catch(() => {}); - void setAudioModeAsync({ - allowsRecording: false, - playsInSilentMode: true, - }).catch(() => {}); - setVoiceState("failed"); - setVoiceError("Recording stopped because Multica moved to the background."); - }); - return () => subscription.remove(); - }, [recorder, voiceState]); - return ( - void playAssistant()} - onRetryPress={retryVoice} - /> ( + + ) + : undefined + } renderStop={() => } manageKeyboard={false} /> @@ -311,70 +140,27 @@ export function ChatComposer({ ); } -function VoiceControls({ - statusLabel, - failed, - recording, - busy, - playing, +function VoiceCallButton({ + onPress, disabled, - canPlay, - onRecordPress, - onPlayPress, - onRetryPress, + color, }: { - statusLabel: string | null; - failed: boolean; - recording: boolean; - busy: boolean; - playing: boolean; + onPress: () => void; disabled: boolean; - canPlay: boolean; - onRecordPress: () => void; - onPlayPress: () => void; - onRetryPress: () => void; + color: string; }) { - const { colorScheme } = useColorScheme(); - const theme = THEME[colorScheme]; return ( - - - - - {statusLabel ?? "Voice mode"} - - {failed ? ( - - ) : null} - - - + + + ); } diff --git a/apps/mobile/components/chat/voice-call-panel.tsx b/apps/mobile/components/chat/voice-call-panel.tsx new file mode 100644 index 00000000000..2b4b3eeec47 --- /dev/null +++ b/apps/mobile/components/chat/voice-call-panel.tsx @@ -0,0 +1,305 @@ +import { useCallback, useMemo, useState } from "react"; +import { Alert, Pressable, View } from "react-native"; +import { Ionicons } from "@expo/vector-icons"; +import * as Haptics from "expo-haptics"; +import { + RecordingPresets, + requestRecordingPermissionsAsync, + setAudioModeAsync, + useAudioRecorder, + useAudioRecorderState, +} from "expo-audio"; +import type { Agent, ChatPendingTask } from "@multica/core/types"; +import { Button } from "@/components/ui/button"; +import { Text } from "@/components/ui/text"; +import { TextField } from "@/components/ui/text-field"; +import { api } from "@/data/api"; +import { cn } from "@/lib/utils"; + +type CallStatus = + | "idle" + | "requesting_permission" + | "recording" + | "ready" + | "sending" + | "permission_denied" + | "failed"; + +interface Props { + agent: Agent; + pendingTask?: ChatPendingTask; + disabled?: boolean; + disabledReason?: string; + onClose: () => void; + onStop: () => void; + ensureSession: (titleSeed: string) => Promise; + onSend: (content: string, attachmentIds?: string[]) => Promise | void; +} + +const IS_IOS = process.env.EXPO_OS === "ios"; + +export function VoiceCallPanel({ + agent, + pendingTask, + disabled = false, + disabledReason, + onClose, + onStop, + ensureSession, + onSend, +}: Props) { + const recorder = useAudioRecorder(RecordingPresets.LOW_QUALITY); + const recorderState = useAudioRecorderState(recorder, 250); + const [status, setStatus] = useState("idle"); + const [recordingUri, setRecordingUri] = useState(null); + const [transcript, setTranscript] = useState(""); + const [error, setError] = useState(null); + + const agentWorking = !!pendingTask?.task_id; + const hasRecording = !!recordingUri; + const transcriptText = transcript.trim(); + const canSend = + !disabled && + !agentWorking && + hasRecording && + transcriptText.length > 0 && + status !== "sending"; + + const statusLabel = useMemo(() => { + if (disabled && disabledReason) return disabledReason; + if (agentWorking) return "Agent is working"; + switch (status) { + case "requesting_permission": + return "Connecting microphone"; + case "recording": + return "Recording"; + case "ready": + return "Ready to send"; + case "sending": + return "Sending voice message"; + case "permission_denied": + return "Microphone blocked"; + case "failed": + return "Voice call failed"; + default: + return "Connected"; + } + }, [agentWorking, disabled, disabledReason, status]); + + const resetRecording = useCallback(() => { + setRecordingUri(null); + setTranscript(""); + setStatus("idle"); + setError(null); + }, []); + + const startRecording = useCallback(async () => { + if (disabled || agentWorking || status === "recording") return; + setError(null); + setStatus("requesting_permission"); + try { + const permission = await requestRecordingPermissionsAsync(); + if (!permission.granted) { + setStatus("permission_denied"); + setError("Enable microphone access in Settings to start a voice call."); + return; + } + await setAudioModeAsync({ + allowsRecording: true, + playsInSilentMode: true, + }); + await recorder.prepareToRecordAsync(); + recorder.record(); + setRecordingUri(null); + setStatus("recording"); + if (IS_IOS) { + void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + } + } catch (err) { + setStatus("failed"); + setError(err instanceof Error ? err.message : "Could not start recording."); + } + }, [agentWorking, disabled, recorder, status]); + + const stopRecording = useCallback(async () => { + if (status !== "recording") return; + try { + await recorder.stop(); + await setAudioModeAsync({ allowsRecording: false }); + const uri = recorder.uri; + if (!uri) { + setStatus("failed"); + setError("Recording did not produce an audio file. Try again."); + return; + } + setRecordingUri(uri); + setStatus("ready"); + if (IS_IOS) { + void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + } + } catch (err) { + setStatus("failed"); + setError(err instanceof Error ? err.message : "Could not stop recording."); + } + }, [recorder, status]); + + const sendVoice = useCallback(async () => { + if (!canSend) return; + const uri = recordingUri; + if (!uri) return; + setStatus("sending"); + setError(null); + try { + const content = `Voice input transcript:\n\n${transcriptText}`; + const sessionId = await ensureSession(transcriptText); + if (!sessionId) { + throw new Error("No chat session is available for this agent."); + } + const attachment = await api.uploadFile( + { + uri, + name: `voice-${Date.now()}.m4a`, + type: "audio/mp4", + }, + { chatSessionId: sessionId }, + ); + await onSend(content, attachment.id ? [attachment.id] : []); + resetRecording(); + } catch (err) { + setStatus("failed"); + const message = + err instanceof Error ? err.message : "Voice message failed to send."; + setError(message); + Alert.alert("Voice message failed", message); + } + }, [ + canSend, + ensureSession, + onSend, + recordingUri, + resetRecording, + transcriptText, + ]); + + return ( + + + + + + Voice call + + + {agent.name} + + + + + {statusLabel} + + + + + + + + + + + + + + {status === "recording" + ? `Recording ${formatDuration(recorderState.durationMillis)}` + : hasRecording + ? "Recording captured" + : "Tap to record a voice message"} + + + + + + Transcript + + + + The agent receives this text prompt. Audio is attached for context. + + + + {error ? ( + + {error} + + ) : null} + + + + + + + + ); +} + +function formatDuration(ms: number | undefined) { + const totalSeconds = Math.max(0, Math.floor((ms ?? 0) / 1000)); + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + return `${minutes}:${seconds.toString().padStart(2, "0")}`; +} diff --git a/apps/mobile/components/composer/message-composer.tsx b/apps/mobile/components/composer/message-composer.tsx index 896a491222a..c19d3dfa140 100644 --- a/apps/mobile/components/composer/message-composer.tsx +++ b/apps/mobile/components/composer/message-composer.tsx @@ -86,9 +86,9 @@ interface Props { mentionPickerPath: Href; /** Attachment upload context — forwarded to `api.uploadFile`. Comment - * passes `issueId`; chat omits both (uploads are session-scoped via - * the message id assigned by the server post-send). */ - uploadContext?: { issueId?: string; commentId?: string }; + * passes `issueId`; chat may pass `chatSessionId` so attachments can + * be back-filled to the message id assigned by the server post-send. */ + uploadContext?: { issueId?: string; commentId?: string; chatSessionId?: string }; placeholder?: string; pillLabel?: string; @@ -115,6 +115,9 @@ interface Props { * this to show a Stop affordance while the agent is running. */ isSending?: boolean; renderStop?: () => ReactNode; + /** Optional leading action next to the collapsed pill. Chat uses this + * for voice-call entry without expanding the text composer. */ + renderLeadingAction?: () => ReactNode; /** Hard-disable. Used when chat has no usable agent. The pill shows * `disabledReason` instead of `pillLabel`, and the pill is @@ -168,6 +171,7 @@ export function MessageComposer({ expandTrigger, isSending = false, renderStop, + renderLeadingAction, disabled = false, disabledReason, manageKeyboard = true, @@ -454,23 +458,26 @@ export function MessageComposer({ className="border-t border-border bg-background px-3 pt-2" style={{ paddingBottom: (manageKeyboard ? insets.bottom : 0) + 8 }} > - - - - {disabled && disabledReason ? disabledReason : pillLabel} - - + + {renderLeadingAction ? renderLeadingAction() : null} + + + + {disabled && disabledReason ? disabledReason : pillLabel} + + + ); diff --git a/apps/mobile/data/api.ts b/apps/mobile/data/api.ts index 56ee0e13131..090077789b1 100644 --- a/apps/mobile/data/api.ts +++ b/apps/mobile/data/api.ts @@ -1269,7 +1269,7 @@ class ApiClient { */ async uploadFile( asset: FileAsset, - opts?: { issueId?: string; commentId?: string }, + opts?: { issueId?: string; commentId?: string; chatSessionId?: string }, ): Promise { const rid = createRequestId(); const start = Date.now(); @@ -1295,6 +1295,9 @@ class ApiClient { ); if (opts?.issueId) formData.append("issue_id", opts.issueId); if (opts?.commentId) formData.append("comment_id", opts.commentId); + if (opts?.chatSessionId) { + formData.append("chat_session_id", opts.chatSessionId); + } console.log(`[api] → POST ${path}`, { rid, filename: asset.name }); diff --git a/apps/mobile/package.json b/apps/mobile/package.json index fab490412ec..49377c60909 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -44,7 +44,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "expo": "~55.0.23", - "expo-audio": "~1.1.1", + "expo-audio": "~55.0.14", "expo-build-properties": "~55.0.13", "expo-clipboard": "~55.0.13", "expo-constants": "~55.0.16", From 67b2f4433ce18fbf9d50e42d948b1616ce88c5c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=BA=A6=E4=B8=9C?= Date: Tue, 2 Jun 2026 03:25:18 +0800 Subject: [PATCH 2/4] chore: trigger ci for LUM-240 Co-authored-by: multica-agent From b0b3735fc9e1fbf5fc4428f0cdda3d6d668e7849 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=BA=A6=E4=B8=9C?= Date: Fri, 5 Jun 2026 01:22:29 +0800 Subject: [PATCH 3/4] chore: remove unused latestAssistantMessage from chat.tsx Co-authored-by: multica-agent --- apps/mobile/app/(app)/[workspace]/(tabs)/chat.tsx | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/apps/mobile/app/(app)/[workspace]/(tabs)/chat.tsx b/apps/mobile/app/(app)/[workspace]/(tabs)/chat.tsx index 730a411e6fe..04aa6055545 100644 --- a/apps/mobile/app/(app)/[workspace]/(tabs)/chat.tsx +++ b/apps/mobile/app/(app)/[workspace]/(tabs)/chat.tsx @@ -179,16 +179,6 @@ export default function ChatTab() { presenceDetail === "loading" ? undefined : presenceDetail.availability; const isArchived = activeSession?.status === "archived"; const sending = !!pendingTask?.task_id; - const latestAssistantMessage = useMemo(() => { - for (let i = messages.length - 1; i >= 0; i -= 1) { - const message = messages[i]; - if (message?.role === "assistant" && message.content.trim()) { - return message; - } - } - return null; - }, [messages]); - // ── Drafts ───────────────────────────────────────────────────────────── const draftKey = activeSessionId ?? DRAFT_NEW_SESSION; const draft = useChatDraftsStore((s) => s.drafts[draftKey] ?? ""); From cc92ea59f0e08ceb021aaa13b6a4dc49e10c868c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=BA=A6=E4=B8=9C?= Date: Fri, 5 Jun 2026 01:31:04 +0800 Subject: [PATCH 4/4] chore: regenerate lockfile for expo-audio ~55.0.14 Co-authored-by: multica-agent --- pnpm-lock.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0ff425b7809..5acde5f3468 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -388,8 +388,8 @@ importers: specifier: ~55.0.23 version: 55.0.24(@babel/core@7.29.0)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.14)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3) expo-audio: - specifier: ~1.1.1 - version: 1.1.1(expo-asset@55.0.17(expo@55.0.24)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3))(expo@55.0.24)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + specifier: ~55.0.14 + version: 55.0.14(expo-asset@55.0.17(expo@55.0.24)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3))(expo@55.0.24)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) expo-build-properties: specifier: ~55.0.13 version: 55.0.14(expo@55.0.24) @@ -6352,8 +6352,8 @@ packages: react: '*' react-native: '*' - expo-audio@1.1.1: - resolution: {integrity: sha512-CPCpJ+0AEHdzWROc0f00Zh6e+irLSl2ALos/LPvxEeIcJw1APfBa4DuHPkL4CQCWsVe7EnUjFpdwpqsEUWcP0g==} + expo-audio@55.0.14: + resolution: {integrity: sha512-Biy6ffKXrnKHgcWSVWLKVdWLNhV/pj1JWJeotY6nDR6fVe8mjXQDCvi6EbaSFPdffVHym6UB2siKzWUNSnG+kQ==} peerDependencies: expo: '*' expo-asset: '*' @@ -17136,7 +17136,7 @@ snapshots: - supports-color - typescript - expo-audio@1.1.1(expo-asset@55.0.17(expo@55.0.24)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3))(expo@55.0.24)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0): + expo-audio@55.0.14(expo-asset@55.0.17(expo@55.0.24)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3))(expo@55.0.24)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0): dependencies: expo: 55.0.24(@babel/core@7.29.0)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.14)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3) expo-asset: 55.0.17(expo@55.0.24)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)