Update files
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useMemo, useState, useEffect } from "react";
|
||||
import { View, Pressable, Text, Platform, Alert } from "react-native";
|
||||
import { View, Pressable, Text, Platform, Alert, ActivityIndicator } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import Animated, {
|
||||
useAnimatedStyle,
|
||||
@@ -57,7 +57,7 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
|
||||
const trafficLightPadding = useTrafficLightPadding();
|
||||
const dragHandlers = useTauriDragHandlers();
|
||||
const { connectionStates } = useDaemonConnections();
|
||||
const { isVoiceMode, startVoice, stopVoice } = useVoice();
|
||||
const { isVoiceMode, isVoiceSwitching, startVoice, stopVoice } = useVoice();
|
||||
const [showVoiceHostPicker, setShowVoiceHostPicker] = useState(false);
|
||||
|
||||
// Track user-initiated refresh to avoid showing spinner on background revalidation
|
||||
@@ -148,6 +148,9 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
|
||||
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);
|
||||
@@ -181,7 +184,7 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
|
||||
}
|
||||
|
||||
setShowVoiceHostPicker(true);
|
||||
}, [hasAnyConfiguredHosts, isVoiceMode, startVoice, stopVoice, voiceEligibleHosts]);
|
||||
}, [hasAnyConfiguredHosts, isVoiceMode, isVoiceSwitching, startVoice, stopVoice, voiceEligibleHosts]);
|
||||
|
||||
const handleSelectVoiceHost = useCallback(
|
||||
(serverId: string) => {
|
||||
@@ -324,11 +327,13 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
|
||||
accessible
|
||||
accessibilityLabel="Voice"
|
||||
accessibilityRole="button"
|
||||
disabled={isVoiceSwitching}
|
||||
onPress={handleToggleVoice}
|
||||
>
|
||||
{({ hovered }) => (
|
||||
<AudioLines
|
||||
size={20}
|
||||
isVoiceSwitching ? (
|
||||
<ActivityIndicator
|
||||
size="small"
|
||||
color={
|
||||
isVoiceMode
|
||||
? theme.colors.foreground
|
||||
@@ -337,7 +342,19 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
|
||||
: theme.colors.foregroundMuted
|
||||
}
|
||||
/>
|
||||
)}
|
||||
) : (
|
||||
<AudioLines
|
||||
size={20}
|
||||
color={
|
||||
isVoiceMode
|
||||
? theme.colors.foreground
|
||||
: hovered
|
||||
? theme.colors.foreground
|
||||
: theme.colors.foregroundMuted
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={styles.footerIconButton}
|
||||
@@ -448,19 +465,33 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
|
||||
accessible
|
||||
accessibilityLabel="Voice"
|
||||
accessibilityRole="button"
|
||||
disabled={isVoiceSwitching}
|
||||
onPress={handleToggleVoice}
|
||||
>
|
||||
{({ hovered }) => (
|
||||
<AudioLines
|
||||
size={20}
|
||||
color={
|
||||
isVoiceMode
|
||||
? theme.colors.foreground
|
||||
: hovered
|
||||
isVoiceSwitching ? (
|
||||
<ActivityIndicator
|
||||
size="small"
|
||||
color={
|
||||
isVoiceMode
|
||||
? theme.colors.foreground
|
||||
: theme.colors.foregroundMuted
|
||||
}
|
||||
/>
|
||||
: hovered
|
||||
? theme.colors.foreground
|
||||
: theme.colors.foregroundMuted
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<AudioLines
|
||||
size={20}
|
||||
color={
|
||||
isVoiceMode
|
||||
? theme.colors.foreground
|
||||
: hovered
|
||||
? theme.colors.foreground
|
||||
: theme.colors.foregroundMuted
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</Pressable>
|
||||
<Pressable
|
||||
|
||||
@@ -872,10 +872,7 @@ export function SessionProvider({
|
||||
const { agentId, event, timestamp } = message.payload;
|
||||
const parsedTimestamp = new Date(timestamp);
|
||||
|
||||
console.log("[Session] agent_stream", { agentId, event, timestamp });
|
||||
|
||||
if (event.type === "attention_required") {
|
||||
console.log("[Session] attention_required", { agentId, shouldNotify: event.shouldNotify, reason: event.reason });
|
||||
if (event.shouldNotify) {
|
||||
notifyAgentAttention({
|
||||
agentId,
|
||||
@@ -926,11 +923,6 @@ export function SessionProvider({
|
||||
if (message.type !== "agent_stream_snapshot") return;
|
||||
const { agentId, events } = message.payload;
|
||||
|
||||
console.log("[Session] agent_stream_snapshot", {
|
||||
agentId,
|
||||
eventCount: events.length,
|
||||
});
|
||||
|
||||
const hydrated = hydrateStreamState(
|
||||
events.map(({ event, timestamp }) => ({
|
||||
event: event as AgentStreamEventPayload,
|
||||
@@ -1059,15 +1051,8 @@ export function SessionProvider({
|
||||
});
|
||||
|
||||
if (!isFinalChunk) {
|
||||
console.log(
|
||||
`[Session] Buffered chunk ${chunkIndex} for group ${playbackGroupId}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[Session] Received final chunk for group ${playbackGroupId}, total chunks: ${buffer.length}`
|
||||
);
|
||||
buffer.sort((a, b) => a.chunkIndex - b.chunkIndex);
|
||||
|
||||
let playbackFailed = false;
|
||||
@@ -1109,10 +1094,6 @@ export function SessionProvider({
|
||||
offset += chunk.length;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[Session] Playing concatenated audio: ${buffer.length} chunks, ${totalSize} bytes`
|
||||
);
|
||||
|
||||
const audioBlob = {
|
||||
type: mimeType,
|
||||
size: totalSize,
|
||||
@@ -1264,11 +1245,7 @@ export function SessionProvider({
|
||||
const transcriptText = message.payload.text.trim();
|
||||
|
||||
if (!transcriptText) {
|
||||
console.log(
|
||||
"[Session] Empty transcription (false positive) - ignoring"
|
||||
);
|
||||
} else {
|
||||
console.log("[Session] Transcription received - stopping playback");
|
||||
audioPlayer.stop();
|
||||
setIsPlayingAudio(serverId, false);
|
||||
setCurrentAssistantMessage(serverId, "");
|
||||
|
||||
@@ -12,6 +12,7 @@ const VOICE_VAD_DETECTION_GRACE_PERIOD_MS = 700;
|
||||
|
||||
interface VoiceContextValue {
|
||||
isVoiceMode: boolean;
|
||||
isVoiceSwitching: boolean;
|
||||
volume: number;
|
||||
isMuted: boolean;
|
||||
isDetecting: boolean;
|
||||
@@ -57,7 +58,12 @@ export function VoiceProvider({ children }: VoiceProviderProps) {
|
||||
);
|
||||
const realtimeSessionRef = useRef<SessionState | null>(null);
|
||||
const [isVoiceMode, setIsVoiceMode] = useState(false);
|
||||
const [isVoiceSwitching, setIsVoiceSwitching] = useState(false);
|
||||
const bargeInPlaybackStopRef = useRef<number | null>(null);
|
||||
const wasVoiceSocketConnectedRef = useRef(false);
|
||||
const lastVoiceModeSyncedClientRef = useRef<SessionState["client"] | null>(null);
|
||||
const voiceTransportReadyRef = useRef(false);
|
||||
const voiceResyncInFlightRef = useRef(false);
|
||||
|
||||
const realtimeAudio = useSpeechmaticsAudio({
|
||||
onSpeechStart: () => {
|
||||
@@ -91,6 +97,10 @@ export function VoiceProvider({ children }: VoiceProviderProps) {
|
||||
console.log("[Voice] Speech ended");
|
||||
},
|
||||
onAudioSegment: ({ audioData, isLast }) => {
|
||||
if (!voiceTransportReadyRef.current) {
|
||||
console.log("[Voice] Skipping audio segment: voice transport not ready");
|
||||
return;
|
||||
}
|
||||
console.log(
|
||||
"[Voice] Sending audio segment, length:",
|
||||
audioData.length,
|
||||
@@ -134,6 +144,48 @@ export function VoiceProvider({ children }: VoiceProviderProps) {
|
||||
realtimeSessionRef.current = activeSession;
|
||||
}, [activeSession]);
|
||||
|
||||
useEffect(() => {
|
||||
const connected = activeSession?.connection.isConnected ?? false;
|
||||
const client = activeSession?.client ?? null;
|
||||
if (!connected) {
|
||||
voiceTransportReadyRef.current = false;
|
||||
}
|
||||
|
||||
if (!isVoiceMode || !activeServerId || !client) {
|
||||
wasVoiceSocketConnectedRef.current = connected;
|
||||
if (!isVoiceMode) {
|
||||
lastVoiceModeSyncedClientRef.current = null;
|
||||
}
|
||||
voiceTransportReadyRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const connectionRecovered = connected && !wasVoiceSocketConnectedRef.current;
|
||||
const clientChanged = lastVoiceModeSyncedClientRef.current !== client;
|
||||
if (connected && (connectionRecovered || clientChanged)) {
|
||||
if (!voiceResyncInFlightRef.current) {
|
||||
voiceResyncInFlightRef.current = true;
|
||||
voiceTransportReadyRef.current = false;
|
||||
setIsVoiceSwitching(true);
|
||||
void client.setVoiceMode(true).then(
|
||||
() => {
|
||||
console.log("[Voice] Re-synced voice mode after reconnect");
|
||||
lastVoiceModeSyncedClientRef.current = client;
|
||||
voiceTransportReadyRef.current = true;
|
||||
},
|
||||
(error) => {
|
||||
console.error("[Voice] Failed to re-sync voice mode:", error);
|
||||
}
|
||||
).finally(() => {
|
||||
voiceResyncInFlightRef.current = false;
|
||||
setIsVoiceSwitching(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
wasVoiceSocketConnectedRef.current = connected;
|
||||
}, [activeServerId, activeSession?.client, activeSession?.connection.isConnected, isVoiceMode]);
|
||||
|
||||
const isPlayingAudio = activeSession?.isPlayingAudio ?? false;
|
||||
|
||||
useEffect(() => {
|
||||
@@ -155,56 +207,67 @@ export function VoiceProvider({ children }: VoiceProviderProps) {
|
||||
throw new Error(`Host ${serverId} is not connected`);
|
||||
}
|
||||
|
||||
setIsVoiceSwitching(true);
|
||||
voiceTransportReadyRef.current = false;
|
||||
try {
|
||||
realtimeSessionRef.current = session;
|
||||
setActiveServerId(serverId);
|
||||
await activateKeepAwakeAsync(KEEP_AWAKE_TAG).catch((error) => {
|
||||
console.warn("[Voice] Failed to activate keep-awake:", error);
|
||||
});
|
||||
await session.audioPlayer?.warmup?.();
|
||||
await realtimeAudio.start();
|
||||
setIsVoiceMode(true);
|
||||
console.log("[Voice] Mode enabled");
|
||||
|
||||
if (session?.client) {
|
||||
await session.client.setVoiceMode(true);
|
||||
} else {
|
||||
console.warn("[Voice] setVoiceMode skipped: daemon unavailable");
|
||||
}
|
||||
await session.audioPlayer?.warmup?.();
|
||||
await realtimeAudio.start();
|
||||
voiceTransportReadyRef.current = true;
|
||||
setIsVoiceMode(true);
|
||||
lastVoiceModeSyncedClientRef.current = session.client;
|
||||
console.log("[Voice] Mode enabled");
|
||||
} catch (error: any) {
|
||||
console.error("[Voice] Failed to start:", error);
|
||||
await realtimeAudio.stop().catch(() => undefined);
|
||||
setActiveServerId((current) => (current === serverId ? null : current));
|
||||
await deactivateKeepAwake(KEEP_AWAKE_TAG).catch(() => undefined);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsVoiceSwitching(false);
|
||||
}
|
||||
},
|
||||
[getSession, realtimeAudio]
|
||||
);
|
||||
|
||||
const stopVoice = useCallback(async () => {
|
||||
setIsVoiceSwitching(true);
|
||||
voiceTransportReadyRef.current = false;
|
||||
try {
|
||||
const session = realtimeSessionRef.current;
|
||||
session?.audioPlayer?.stop();
|
||||
if (session?.client) {
|
||||
await session.client.setVoiceMode(false);
|
||||
lastVoiceModeSyncedClientRef.current = session.client;
|
||||
} else {
|
||||
console.warn("[Voice] setVoiceMode skipped: daemon unavailable");
|
||||
}
|
||||
await realtimeAudio.stop();
|
||||
setIsVoiceMode(false);
|
||||
setActiveServerId(null);
|
||||
await deactivateKeepAwake(KEEP_AWAKE_TAG).catch(() => undefined);
|
||||
console.log("[Voice] Mode disabled");
|
||||
|
||||
if (session?.client) {
|
||||
await session.client.setVoiceMode(false);
|
||||
} else {
|
||||
console.warn("[Voice] setVoiceMode skipped: daemon unavailable");
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("[Voice] Failed to stop:", error);
|
||||
await deactivateKeepAwake(KEEP_AWAKE_TAG).catch(() => undefined);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsVoiceSwitching(false);
|
||||
}
|
||||
}, [realtimeAudio]);
|
||||
|
||||
const value: VoiceContextValue = {
|
||||
isVoiceMode,
|
||||
isVoiceSwitching,
|
||||
volume: realtimeAudio.volume,
|
||||
isMuted: realtimeAudio.isMuted,
|
||||
isDetecting: realtimeAudio.isDetecting,
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useDaemonConnections } from "@/contexts/daemon-connections-context";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import type { AgentDirectoryEntry } from "@/types/agent-directory";
|
||||
import type { Agent } from "@/stores/session-store";
|
||||
import { isPerfLoggingEnabled } from "@/utils/perf";
|
||||
import { derivePendingPermissionKey, normalizeAgentSnapshot } from "@/utils/agent-snapshots";
|
||||
|
||||
export interface AggregatedAgent extends AgentDirectoryEntry {
|
||||
@@ -44,14 +43,12 @@ export function useAggregatedAgents(): AggregatedAgentsResult {
|
||||
);
|
||||
|
||||
const refreshAll = useCallback(() => {
|
||||
console.log('[useAggregatedAgents] Manual refresh triggered for all sessions');
|
||||
for (const [serverId, client] of Object.entries(sessionClients)) {
|
||||
if (!client) {
|
||||
continue;
|
||||
}
|
||||
void (async () => {
|
||||
try {
|
||||
console.log(`[useAggregatedAgents] Refreshing session ${serverId}`);
|
||||
const agentsList = await client.fetchAgents({
|
||||
filter: { labels: { ui: "true" } },
|
||||
});
|
||||
@@ -131,27 +128,20 @@ export function useAggregatedAgents(): AggregatedAgentsResult {
|
||||
const hasAnyData = allAgents.length > 0;
|
||||
|
||||
// Check if any connection is currently loading
|
||||
const connectingReasons: string[] = [];
|
||||
const isConnecting = Array.from(connectionStates.entries()).some(([id, c]) => {
|
||||
const shortId = id.substring(0, 20);
|
||||
|
||||
const isConnecting = Array.from(connectionStates.entries()).some(([, c]) => {
|
||||
// First-time connection (never received agent list)
|
||||
if (c.status === 'connecting' && !c.hasEverReceivedAgentList) {
|
||||
connectingReasons.push(`${shortId}: first-time connecting`);
|
||||
return true;
|
||||
}
|
||||
if (c.status === 'online' && !c.hasEverReceivedAgentList) {
|
||||
connectingReasons.push(`${shortId}: online but no fetch_agents yet`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Reconnecting (have received agent list before)
|
||||
if (c.status === 'connecting' && c.hasEverReceivedAgentList) {
|
||||
connectingReasons.push(`${shortId}: reconnecting`);
|
||||
return true;
|
||||
}
|
||||
if (c.status === 'online' && !c.agentListReady && c.hasEverReceivedAgentList) {
|
||||
connectingReasons.push(`${shortId}: online but agentListReady=false (waiting for fetch_agents)`);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -167,28 +157,6 @@ export function useAggregatedAgents(): AggregatedAgentsResult {
|
||||
// isLoading: Generic loading flag (either initial or revalidating)
|
||||
const isLoading = isConnecting;
|
||||
|
||||
if (isPerfLoggingEnabled()) {
|
||||
const connectionStatesArray = Array.from(connectionStates.entries()).map(([id, state]) => ({
|
||||
id: id.substring(0, 20) + (id.length > 20 ? '...' : ''),
|
||||
status: state.status,
|
||||
agentListReady: state.agentListReady,
|
||||
hasEverReceivedAgentList: state.hasEverReceivedAgentList,
|
||||
}));
|
||||
|
||||
console.log('[useAggregatedAgents] States:', {
|
||||
hasAnyData,
|
||||
isConnecting,
|
||||
isInitialLoad,
|
||||
isRevalidating,
|
||||
totalConnectionStates: connectionStates.size,
|
||||
connectingReasons: connectingReasons.length > 0 ? connectingReasons : 'none',
|
||||
});
|
||||
|
||||
console.log('[useAggregatedAgents] Connection States Detail:',
|
||||
JSON.stringify(connectionStatesArray, null, 2)
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
agents: allAgents,
|
||||
isLoading,
|
||||
|
||||
@@ -210,9 +210,6 @@ export interface GroupedAgents {
|
||||
|
||||
const ACTIVE_GRACE_PERIOD_MS = 2 * 24 * 60 * 60 * 1000; // 2 days (temporary for screenshots)
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('[agent-grouping] ACTIVE_GRACE_PERIOD_MS:', ACTIVE_GRACE_PERIOD_MS, 'ms =', ACTIVE_GRACE_PERIOD_MS / (1000 * 60 * 60), 'hours');
|
||||
|
||||
interface GroupAgentsOptions {
|
||||
/**
|
||||
* Optional function to read a remote URL for an agent.
|
||||
@@ -252,17 +249,6 @@ export function groupAgents(
|
||||
const isRecentlyActive = ageDiff < ACTIVE_GRACE_PERIOD_MS;
|
||||
const isActive = isRunningOrAttention || isRecentlyActive;
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('[agent-grouping] agent:', agent.title || agent.id, {
|
||||
status: agent.status,
|
||||
lastActivityAt: agent.lastActivityAt,
|
||||
ageDiffHours: ageDiff / (1000 * 60 * 60),
|
||||
gracePeriodHours: ACTIVE_GRACE_PERIOD_MS / (1000 * 60 * 60),
|
||||
isRunningOrAttention,
|
||||
isRecentlyActive,
|
||||
isActive,
|
||||
});
|
||||
|
||||
if (isActive) {
|
||||
activeAgents.push(agent);
|
||||
} else {
|
||||
|
||||
@@ -1,21 +1,4 @@
|
||||
const getIsDevEnvironment = (): boolean => {
|
||||
const globalDev = (globalThis as { __DEV__?: boolean } | undefined)?.__DEV__;
|
||||
if (typeof globalDev === "boolean") {
|
||||
return globalDev;
|
||||
}
|
||||
if (typeof process !== "undefined" && process.env?.NODE_ENV) {
|
||||
return process.env.NODE_ENV !== "production";
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const shouldDisablePerfLogging =
|
||||
typeof process !== "undefined" && process.env?.EXPO_PUBLIC_DISABLE_PERF_LOGGING === "1";
|
||||
const shouldForcePerfLogging =
|
||||
typeof process !== "undefined" && process.env?.EXPO_PUBLIC_ENABLE_PERF_LOGGING === "1";
|
||||
|
||||
const PERF_LOGGING_ENABLED =
|
||||
(shouldForcePerfLogging || getIsDevEnvironment()) && !shouldDisablePerfLogging;
|
||||
const PERF_LOGGING_ENABLED = false;
|
||||
|
||||
export const isPerfLoggingEnabled = (): boolean => PERF_LOGGING_ENABLED;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user