Update files
This commit is contained in:
@@ -4,10 +4,11 @@ import {
|
||||
Text,
|
||||
ActivityIndicator,
|
||||
Platform,
|
||||
Alert,
|
||||
} from "react-native";
|
||||
import { useState, useEffect, useRef, useCallback, useMemo } from "react";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { ArrowUp, Square, Pencil } from "lucide-react-native";
|
||||
import { ArrowUp, Square, Pencil, AudioLines } from "lucide-react-native";
|
||||
import Animated, { useAnimatedStyle } from "react-native-reanimated";
|
||||
import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
@@ -30,6 +31,7 @@ import { useAgentCommandsQuery } from "@/hooks/use-agent-commands-query";
|
||||
import { encodeImages } from "@/utils/encode-images";
|
||||
import { useKeyboardNavStore } from "@/stores/keyboard-nav-store";
|
||||
import { focusWithRetries } from "@/utils/web-focus";
|
||||
import { useVoiceOptional } from "@/contexts/voice-context";
|
||||
|
||||
type QueuedMessage = {
|
||||
id: string;
|
||||
@@ -76,6 +78,7 @@ export function AgentInputArea({
|
||||
const client = useSessionStore(
|
||||
(state) => state.sessions[serverId]?.client ?? null
|
||||
);
|
||||
const voice = useVoiceOptional();
|
||||
const isConnected = client?.isConnected ?? false;
|
||||
|
||||
const agent = useSessionStore((state) =>
|
||||
@@ -411,6 +414,29 @@ export function AgentInputArea({
|
||||
messageInputRef.current?.focus();
|
||||
}
|
||||
|
||||
const isVoiceModeForAgent =
|
||||
voice?.isVoiceModeForAgent(serverId, agentId) ?? false;
|
||||
|
||||
const handleToggleRealtimeVoice = useCallback(() => {
|
||||
if (!voice || !isConnected) {
|
||||
return;
|
||||
}
|
||||
if (voice.isVoiceSwitching) {
|
||||
return;
|
||||
}
|
||||
if (voice.isVoiceModeForAgent(serverId, agentId)) {
|
||||
void voice.stopVoice().catch((error) => {
|
||||
console.error("[AgentInputArea] Failed to stop voice mode", error);
|
||||
Alert.alert("Voice failed", "Unable to stop realtime voice mode.");
|
||||
});
|
||||
return;
|
||||
}
|
||||
void voice.startVoice(serverId, agentId).catch((error) => {
|
||||
console.error("[AgentInputArea] Failed to start voice mode", error);
|
||||
Alert.alert("Voice failed", "Unable to start realtime voice mode.");
|
||||
});
|
||||
}, [agentId, isConnected, serverId, voice]);
|
||||
|
||||
function handleEditQueuedMessage(id: string) {
|
||||
const item = queuedMessages.find((q) => q.id === id);
|
||||
if (!item) return;
|
||||
@@ -494,7 +520,7 @@ export function AgentInputArea({
|
||||
]
|
||||
);
|
||||
|
||||
const rightContent = isAgentRunning ? (
|
||||
const cancelButton = isAgentRunning ? (
|
||||
<Pressable
|
||||
onPress={handleCancelAgent}
|
||||
disabled={!isConnected || isCancellingAgent}
|
||||
@@ -515,6 +541,35 @@ export function AgentInputArea({
|
||||
</Pressable>
|
||||
) : null;
|
||||
|
||||
const rightContent = (
|
||||
<View style={styles.rightControls}>
|
||||
<Pressable
|
||||
onPress={handleToggleRealtimeVoice}
|
||||
disabled={!isConnected || voice?.isVoiceSwitching}
|
||||
accessibilityLabel={
|
||||
isVoiceModeForAgent ? "Disable realtime voice mode" : "Enable realtime voice mode"
|
||||
}
|
||||
accessibilityRole="button"
|
||||
style={[
|
||||
styles.realtimeVoiceButton as any,
|
||||
isVoiceModeForAgent ? (styles.realtimeVoiceButtonActive as any) : undefined,
|
||||
(!isConnected || voice?.isVoiceSwitching
|
||||
? styles.buttonDisabled
|
||||
: undefined) as any,
|
||||
]}
|
||||
>
|
||||
{voice?.isVoiceSwitching ? (
|
||||
<ActivityIndicator size="small" color="white" />
|
||||
) : isVoiceModeForAgent ? (
|
||||
<Square size={16} color="white" fill="white" />
|
||||
) : (
|
||||
<AudioLines size={18} color={theme.colors.foreground} />
|
||||
)}
|
||||
</Pressable>
|
||||
{cancelButton}
|
||||
</View>
|
||||
);
|
||||
|
||||
const leftContent = <AgentStatusBar agentId={agentId} serverId={serverId} />;
|
||||
|
||||
return (
|
||||
@@ -588,6 +643,8 @@ export function AgentInputArea({
|
||||
isScreenFocused={isScreenFocused}
|
||||
leftContent={leftContent}
|
||||
rightContent={rightContent}
|
||||
voiceServerId={serverId}
|
||||
voiceAgentId={agentId}
|
||||
isAgentRunning={isAgentRunning}
|
||||
onQueue={handleQueue}
|
||||
onKeyPress={handleCommandKeyPress}
|
||||
@@ -631,6 +688,25 @@ const styles = StyleSheet.create(((theme: Theme) => ({
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
rightControls: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[2],
|
||||
},
|
||||
realtimeVoiceButton: {
|
||||
width: 34,
|
||||
height: 34,
|
||||
borderRadius: theme.borderRadius.full,
|
||||
backgroundColor: theme.colors.surface0,
|
||||
borderWidth: theme.borderWidth[1],
|
||||
borderColor: theme.colors.border,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
realtimeVoiceButtonActive: {
|
||||
backgroundColor: theme.colors.palette.green[600],
|
||||
borderColor: theme.colors.palette.green[800],
|
||||
},
|
||||
buttonDisabled: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
|
||||
@@ -102,7 +102,7 @@ export function AgentStreamView({
|
||||
const flatListRef = useRef<FlatList<StreamItem>>(null);
|
||||
const { theme } = useUnistyles();
|
||||
const insets = useSafeAreaInsets();
|
||||
const { isVoiceMode } = useVoice();
|
||||
const { isVoiceModeForAgent } = useVoice();
|
||||
const [isNearBottom, setIsNearBottom] = useState(true);
|
||||
const hasScrolledInitially = useRef(false);
|
||||
const hasAutoScrolledOnce = useRef(false);
|
||||
@@ -114,6 +114,7 @@ export function AgentStreamView({
|
||||
|
||||
// Get serverId (fallback to agent's serverId if not provided)
|
||||
const resolvedServerId = serverId ?? agent.serverId ?? "";
|
||||
const isVoiceMode = isVoiceModeForAgent(resolvedServerId, agentId);
|
||||
|
||||
const client = useSessionStore(
|
||||
(state) => state.sessions[resolvedServerId]?.client ?? null
|
||||
|
||||
@@ -61,6 +61,8 @@ export interface MessageInputProps {
|
||||
leftContent?: React.ReactNode;
|
||||
/** Content to render on the right side after voice button (e.g., realtime button, cancel button) */
|
||||
rightContent?: React.ReactNode;
|
||||
voiceServerId?: string;
|
||||
voiceAgentId?: string;
|
||||
/** When true and there's sendable content, calls onQueue instead of onSubmit */
|
||||
isAgentRunning?: boolean;
|
||||
/** Callback for queue button when agent is running */
|
||||
@@ -117,6 +119,8 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
isScreenFocused = true,
|
||||
leftContent,
|
||||
rightContent,
|
||||
voiceServerId,
|
||||
voiceAgentId,
|
||||
isAgentRunning = false,
|
||||
onQueue,
|
||||
onKeyPress: onKeyPressCallback,
|
||||
@@ -310,15 +314,20 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
}));
|
||||
|
||||
const handleVoicePress = useCallback(async () => {
|
||||
const isRealtimeVoiceForCurrentAgent =
|
||||
voice &&
|
||||
!!voiceServerId &&
|
||||
!!voiceAgentId &&
|
||||
voice.isVoiceModeForAgent(voiceServerId, voiceAgentId);
|
||||
if (isDictating) {
|
||||
await cancelDictation();
|
||||
} else {
|
||||
if (voice?.isVoiceMode) {
|
||||
if (isRealtimeVoiceForCurrentAgent) {
|
||||
await voice.stopVoice();
|
||||
}
|
||||
await startDictation();
|
||||
}
|
||||
}, [isDictating, cancelDictation, startDictation, voice]);
|
||||
}, [isDictating, cancelDictation, startDictation, voice, voiceAgentId, voiceServerId]);
|
||||
|
||||
const handleCancelRecording = useCallback(async () => {
|
||||
await cancelDictation();
|
||||
@@ -626,6 +635,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
<Mic size={20} color={theme.colors.foreground} />
|
||||
)}
|
||||
</Pressable>
|
||||
{rightContent}
|
||||
{shouldShowSendButton && (
|
||||
<Pressable
|
||||
onPress={handleSendMessage}
|
||||
@@ -647,7 +657,6 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
<ArrowUp size={20} color="white" />
|
||||
</Pressable>
|
||||
)}
|
||||
{!shouldShowSendButton && rightContent}
|
||||
</View>
|
||||
</View>
|
||||
</Animated.View>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useMemo, useState, useEffect } from "react";
|
||||
import { View, Pressable, Text, Platform, Alert, ActivityIndicator } from "react-native";
|
||||
import { View, Pressable, Text, Platform } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import Animated, {
|
||||
useAnimatedStyle,
|
||||
@@ -9,21 +9,17 @@ import Animated, {
|
||||
} from "react-native-reanimated";
|
||||
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
||||
import { Plus, Settings, Users, AudioLines } from "lucide-react-native";
|
||||
import { Plus, Settings, Users } from "lucide-react-native";
|
||||
import { router } from "expo-router";
|
||||
import { usePanelStore } from "@/stores/panel-store";
|
||||
import { GroupedAgentList } from "./grouped-agent-list";
|
||||
import { useAggregatedAgents } from "@/hooks/use-aggregated-agents";
|
||||
import { useSidebarAnimation } from "@/contexts/sidebar-animation-context";
|
||||
import { useTauriDragHandlers, useTrafficLightPadding } from "@/utils/tauri-window";
|
||||
import { useVoice } from "@/contexts/voice-context";
|
||||
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
|
||||
import { VoicePanel } from "./voice-panel";
|
||||
import { useSidebarAgentSections } from "@/hooks/use-sidebar-agent-sections";
|
||||
import { useSidebarCollapsedSectionsStore } from "@/stores/sidebar-collapsed-sections-store";
|
||||
import { useKeyboardNavStore } from "@/stores/keyboard-nav-store";
|
||||
import { deriveSidebarShortcutAgentKeys } from "@/utils/sidebar-shortcuts";
|
||||
import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet";
|
||||
|
||||
const DESKTOP_SIDEBAR_WIDTH = 320;
|
||||
const SIDEBAR_AGENT_LIMIT = 50; // temporary for screenshots
|
||||
@@ -56,9 +52,6 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
|
||||
} = useSidebarAnimation();
|
||||
const trafficLightPadding = useTrafficLightPadding();
|
||||
const dragHandlers = useTauriDragHandlers();
|
||||
const { connectionStates } = useDaemonConnections();
|
||||
const { isVoiceMode, isVoiceSwitching, startVoice, stopVoice } = useVoice();
|
||||
const [showVoiceHostPicker, setShowVoiceHostPicker] = useState(false);
|
||||
|
||||
// Track user-initiated refresh to avoid showing spinner on background revalidation
|
||||
const [isManualRefresh, setIsManualRefresh] = useState(false);
|
||||
@@ -142,65 +135,6 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
|
||||
router.push("/agents");
|
||||
}, [backdropOpacity, closeToAgent, isMobile, translateX, windowWidth]);
|
||||
|
||||
const voiceEligibleHosts = useMemo(() => {
|
||||
return Array.from(connectionStates.values()).filter((entry) => entry.status === "online");
|
||||
}, [connectionStates]);
|
||||
const hasAnyConfiguredHosts = connectionStates.size > 0;
|
||||
|
||||
const handleToggleVoice = useCallback(() => {
|
||||
if (isVoiceSwitching) {
|
||||
return;
|
||||
}
|
||||
if (isVoiceMode) {
|
||||
void stopVoice().catch((error) => {
|
||||
console.error("[SlidingSidebar] Failed to stop voice", error);
|
||||
Alert.alert("Voice failed", "Unable to stop voice mode.");
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (voiceEligibleHosts.length === 0) {
|
||||
if (!hasAnyConfiguredHosts) {
|
||||
Alert.alert(
|
||||
"No hosts available",
|
||||
"Add a host in Settings before starting Voice mode.",
|
||||
[{ text: "Open Settings", onPress: () => router.push("/settings") }, { text: "OK" }]
|
||||
);
|
||||
return;
|
||||
}
|
||||
Alert.alert(
|
||||
"Hosts reconnecting",
|
||||
"Every host is offline right now. Paseo reconnects automatically—try Voice mode again once one comes online."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (voiceEligibleHosts.length === 1) {
|
||||
void startVoice(voiceEligibleHosts[0].daemon.serverId).catch((error) => {
|
||||
console.error("[SlidingSidebar] Failed to start voice", error);
|
||||
Alert.alert("Voice failed", "Unable to start Voice mode for this host.");
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setShowVoiceHostPicker(true);
|
||||
}, [hasAnyConfiguredHosts, isVoiceMode, isVoiceSwitching, startVoice, stopVoice, voiceEligibleHosts]);
|
||||
|
||||
const handleSelectVoiceHost = useCallback(
|
||||
(serverId: string) => {
|
||||
setShowVoiceHostPicker(false);
|
||||
void startVoice(serverId).catch((error) => {
|
||||
console.error("[SlidingSidebar] Failed to start voice", error);
|
||||
Alert.alert("Voice failed", "Unable to start Voice mode for this host.");
|
||||
});
|
||||
},
|
||||
[startVoice]
|
||||
);
|
||||
|
||||
const handleDismissVoiceHostPicker = useCallback(() => {
|
||||
setShowVoiceHostPicker(false);
|
||||
}, []);
|
||||
|
||||
// Close gesture (swipe left to close when sidebar is open)
|
||||
// Only activates on leftward swipe, fails on rightward or vertical movement
|
||||
// This mirrors the explorer-sidebar pattern for the right sidebar
|
||||
@@ -301,8 +235,6 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
|
||||
parentGestureRef={closeGestureRef}
|
||||
/>
|
||||
|
||||
{isVoiceMode ? <VoicePanel /> : null}
|
||||
|
||||
{/* Footer */}
|
||||
<View style={styles.sidebarFooter}>
|
||||
<Pressable
|
||||
@@ -319,43 +251,6 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
|
||||
)}
|
||||
</Pressable>
|
||||
<View style={styles.footerIconRow}>
|
||||
<Pressable
|
||||
style={styles.footerIconButton}
|
||||
testID="sidebar-voice"
|
||||
nativeID="sidebar-voice"
|
||||
collapsable={false}
|
||||
accessible
|
||||
accessibilityLabel="Voice"
|
||||
accessibilityRole="button"
|
||||
disabled={isVoiceSwitching}
|
||||
onPress={handleToggleVoice}
|
||||
>
|
||||
{({ hovered }) => (
|
||||
isVoiceSwitching ? (
|
||||
<ActivityIndicator
|
||||
size="small"
|
||||
color={
|
||||
isVoiceMode
|
||||
? theme.colors.foreground
|
||||
: hovered
|
||||
? theme.colors.foreground
|
||||
: theme.colors.foregroundMuted
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<AudioLines
|
||||
size={20}
|
||||
color={
|
||||
isVoiceMode
|
||||
? theme.colors.foreground
|
||||
: hovered
|
||||
? theme.colors.foreground
|
||||
: theme.colors.foregroundMuted
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={styles.footerIconButton}
|
||||
testID="sidebar-settings"
|
||||
@@ -375,32 +270,6 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
|
||||
</View>
|
||||
</Animated.View>
|
||||
</GestureDetector>
|
||||
|
||||
<AdaptiveModalSheet
|
||||
title="Choose a host"
|
||||
visible={showVoiceHostPicker}
|
||||
onClose={handleDismissVoiceHostPicker}
|
||||
snapPoints={["45%", "70%"]}
|
||||
testID="voice-host-picker"
|
||||
>
|
||||
<View style={styles.hostPickerList}>
|
||||
{voiceEligibleHosts.map((entry) => (
|
||||
<Pressable
|
||||
key={entry.daemon.serverId}
|
||||
style={styles.hostPickerOption}
|
||||
onPress={() => handleSelectVoiceHost(entry.daemon.serverId)}
|
||||
>
|
||||
<Text style={styles.hostPickerOptionText}>{entry.daemon.label}</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
<Pressable
|
||||
style={styles.hostPickerCancel}
|
||||
onPress={handleDismissVoiceHostPicker}
|
||||
>
|
||||
<Text style={styles.hostPickerCancelText}>Cancel</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</AdaptiveModalSheet>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -439,8 +308,6 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
|
||||
selectedAgentId={selectedAgentId}
|
||||
/>
|
||||
|
||||
{isVoiceMode ? <VoicePanel /> : null}
|
||||
|
||||
{/* Footer */}
|
||||
<View style={styles.sidebarFooter}>
|
||||
<Pressable
|
||||
@@ -457,43 +324,6 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
|
||||
)}
|
||||
</Pressable>
|
||||
<View style={styles.footerIconRow}>
|
||||
<Pressable
|
||||
style={styles.footerIconButton}
|
||||
testID="sidebar-voice"
|
||||
nativeID="sidebar-voice"
|
||||
collapsable={false}
|
||||
accessible
|
||||
accessibilityLabel="Voice"
|
||||
accessibilityRole="button"
|
||||
disabled={isVoiceSwitching}
|
||||
onPress={handleToggleVoice}
|
||||
>
|
||||
{({ hovered }) => (
|
||||
isVoiceSwitching ? (
|
||||
<ActivityIndicator
|
||||
size="small"
|
||||
color={
|
||||
isVoiceMode
|
||||
? theme.colors.foreground
|
||||
: hovered
|
||||
? theme.colors.foreground
|
||||
: theme.colors.foregroundMuted
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<AudioLines
|
||||
size={20}
|
||||
color={
|
||||
isVoiceMode
|
||||
? theme.colors.foreground
|
||||
: hovered
|
||||
? theme.colors.foreground
|
||||
: theme.colors.foregroundMuted
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={styles.footerIconButton}
|
||||
testID="sidebar-settings"
|
||||
@@ -510,32 +340,6 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<AdaptiveModalSheet
|
||||
title="Choose a host"
|
||||
visible={showVoiceHostPicker}
|
||||
onClose={handleDismissVoiceHostPicker}
|
||||
snapPoints={["45%", "70%"]}
|
||||
testID="voice-host-picker"
|
||||
>
|
||||
<View style={styles.hostPickerList}>
|
||||
{voiceEligibleHosts.map((entry) => (
|
||||
<Pressable
|
||||
key={entry.daemon.serverId}
|
||||
style={styles.hostPickerOption}
|
||||
onPress={() => handleSelectVoiceHost(entry.daemon.serverId)}
|
||||
>
|
||||
<Text style={styles.hostPickerOptionText}>{entry.daemon.label}</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
<Pressable
|
||||
style={styles.hostPickerCancel}
|
||||
onPress={handleDismissVoiceHostPicker}
|
||||
>
|
||||
<Text style={styles.hostPickerCancelText}>Cancel</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</AdaptiveModalSheet>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,10 +18,12 @@ interface VoiceContextValue {
|
||||
isDetecting: boolean;
|
||||
isSpeaking: boolean;
|
||||
segmentDuration: number;
|
||||
startVoice: (serverId: string) => Promise<void>;
|
||||
startVoice: (serverId: string, agentId: string) => Promise<void>;
|
||||
stopVoice: () => Promise<void>;
|
||||
isVoiceModeForAgent: (serverId: string, agentId: string) => boolean;
|
||||
toggleMute: () => void;
|
||||
activeServerId: string | null;
|
||||
activeAgentId: string | null;
|
||||
}
|
||||
|
||||
const VoiceContext = createContext<VoiceContextValue | null>(null);
|
||||
@@ -45,6 +47,7 @@ interface VoiceProviderProps {
|
||||
export function VoiceProvider({ children }: VoiceProviderProps) {
|
||||
const getSession = useSessionStore((state) => state.getSession);
|
||||
const [activeServerId, setActiveServerId] = useState<string | null>(null);
|
||||
const [activeAgentId, setActiveAgentId] = useState<string | null>(null);
|
||||
const activeSession = useSessionStore(
|
||||
useCallback(
|
||||
(state: ReturnType<typeof useSessionStore.getState>) => {
|
||||
@@ -151,7 +154,7 @@ export function VoiceProvider({ children }: VoiceProviderProps) {
|
||||
voiceTransportReadyRef.current = false;
|
||||
}
|
||||
|
||||
if (!isVoiceMode || !activeServerId || !client) {
|
||||
if (!isVoiceMode || !activeServerId || !activeAgentId || !client) {
|
||||
wasVoiceSocketConnectedRef.current = connected;
|
||||
if (!isVoiceMode) {
|
||||
lastVoiceModeSyncedClientRef.current = null;
|
||||
@@ -167,7 +170,7 @@ export function VoiceProvider({ children }: VoiceProviderProps) {
|
||||
voiceResyncInFlightRef.current = true;
|
||||
voiceTransportReadyRef.current = false;
|
||||
setIsVoiceSwitching(true);
|
||||
void client.setVoiceMode(true).then(
|
||||
void client.setVoiceMode(true, activeAgentId).then(
|
||||
() => {
|
||||
console.log("[Voice] Re-synced voice mode after reconnect");
|
||||
lastVoiceModeSyncedClientRef.current = client;
|
||||
@@ -184,7 +187,7 @@ export function VoiceProvider({ children }: VoiceProviderProps) {
|
||||
}
|
||||
|
||||
wasVoiceSocketConnectedRef.current = connected;
|
||||
}, [activeServerId, activeSession?.client, activeSession?.connection.isConnected, isVoiceMode]);
|
||||
}, [activeAgentId, activeServerId, activeSession?.client, activeSession?.connection.isConnected, isVoiceMode]);
|
||||
|
||||
const isPlayingAudio = activeSession?.isPlayingAudio ?? false;
|
||||
|
||||
@@ -201,7 +204,7 @@ export function VoiceProvider({ children }: VoiceProviderProps) {
|
||||
}, [isPlayingAudio]);
|
||||
|
||||
const startVoice = useCallback(
|
||||
async (serverId: string) => {
|
||||
async (serverId: string, agentId: string) => {
|
||||
const session = getSession(serverId) ?? null;
|
||||
if (!session) {
|
||||
throw new Error(`Host ${serverId} is not connected`);
|
||||
@@ -210,13 +213,23 @@ export function VoiceProvider({ children }: VoiceProviderProps) {
|
||||
setIsVoiceSwitching(true);
|
||||
voiceTransportReadyRef.current = false;
|
||||
try {
|
||||
const previousSession = realtimeSessionRef.current;
|
||||
if (
|
||||
isVoiceMode &&
|
||||
previousSession?.client &&
|
||||
(activeServerId !== serverId || activeAgentId !== agentId)
|
||||
) {
|
||||
await previousSession.client.setVoiceMode(false);
|
||||
}
|
||||
|
||||
realtimeSessionRef.current = session;
|
||||
setActiveServerId(serverId);
|
||||
setActiveAgentId(agentId);
|
||||
await activateKeepAwakeAsync(KEEP_AWAKE_TAG).catch((error) => {
|
||||
console.warn("[Voice] Failed to activate keep-awake:", error);
|
||||
});
|
||||
if (session?.client) {
|
||||
await session.client.setVoiceMode(true);
|
||||
await session.client.setVoiceMode(true, agentId);
|
||||
} else {
|
||||
console.warn("[Voice] setVoiceMode skipped: daemon unavailable");
|
||||
}
|
||||
@@ -230,13 +243,14 @@ export function VoiceProvider({ children }: VoiceProviderProps) {
|
||||
console.error("[Voice] Failed to start:", error);
|
||||
await realtimeAudio.stop().catch(() => undefined);
|
||||
setActiveServerId((current) => (current === serverId ? null : current));
|
||||
setActiveAgentId((current) => (current === agentId ? null : current));
|
||||
await deactivateKeepAwake(KEEP_AWAKE_TAG).catch(() => undefined);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsVoiceSwitching(false);
|
||||
}
|
||||
},
|
||||
[getSession, realtimeAudio]
|
||||
[activeAgentId, activeServerId, getSession, isVoiceMode, realtimeAudio]
|
||||
);
|
||||
|
||||
const stopVoice = useCallback(async () => {
|
||||
@@ -254,6 +268,7 @@ export function VoiceProvider({ children }: VoiceProviderProps) {
|
||||
await realtimeAudio.stop();
|
||||
setIsVoiceMode(false);
|
||||
setActiveServerId(null);
|
||||
setActiveAgentId(null);
|
||||
await deactivateKeepAwake(KEEP_AWAKE_TAG).catch(() => undefined);
|
||||
console.log("[Voice] Mode disabled");
|
||||
} catch (error: any) {
|
||||
@@ -265,6 +280,12 @@ export function VoiceProvider({ children }: VoiceProviderProps) {
|
||||
}
|
||||
}, [realtimeAudio]);
|
||||
|
||||
const isVoiceModeForAgent = useCallback(
|
||||
(serverId: string, agentId: string) =>
|
||||
isVoiceMode && activeServerId === serverId && activeAgentId === agentId,
|
||||
[activeAgentId, activeServerId, isVoiceMode]
|
||||
);
|
||||
|
||||
const value: VoiceContextValue = {
|
||||
isVoiceMode,
|
||||
isVoiceSwitching,
|
||||
@@ -275,8 +296,10 @@ export function VoiceProvider({ children }: VoiceProviderProps) {
|
||||
segmentDuration: realtimeAudio.segmentDuration,
|
||||
startVoice,
|
||||
stopVoice,
|
||||
isVoiceModeForAgent,
|
||||
toggleMute: realtimeAudio.toggleMute,
|
||||
activeServerId,
|
||||
activeAgentId,
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user