refactor(app): remove debug logging from voice and UI components
This commit is contained in:
@@ -71,18 +71,6 @@ polyfillCrypto();
|
|||||||
attachConsole();
|
attachConsole();
|
||||||
const HostRuntimeBootstrapContext = createContext(false);
|
const HostRuntimeBootstrapContext = createContext(false);
|
||||||
|
|
||||||
const IS_DEV = Boolean((globalThis as { __DEV__?: boolean }).__DEV__);
|
|
||||||
|
|
||||||
function logLeftSidebarOpenGesture(
|
|
||||||
event: string,
|
|
||||||
details: Record<string, unknown>
|
|
||||||
): void {
|
|
||||||
if (!IS_DEV) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log(`[LeftSidebarOpenGesture] ${event}`, details);
|
|
||||||
}
|
|
||||||
|
|
||||||
function PushNotificationRouter() {
|
function PushNotificationRouter() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const lastHandledIdRef = useRef<string | null>(null);
|
const lastHandledIdRef = useRef<string | null>(null);
|
||||||
@@ -339,10 +327,6 @@ function AppContainer({
|
|||||||
})
|
})
|
||||||
.onStart(() => {
|
.onStart(() => {
|
||||||
isGesturing.value = true;
|
isGesturing.value = true;
|
||||||
runOnJS(logLeftSidebarOpenGesture)("start", {
|
|
||||||
mobileView,
|
|
||||||
openGestureEnabled,
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.onUpdate((event) => {
|
.onUpdate((event) => {
|
||||||
// Start from closed position (-windowWidth) and move towards 0
|
// Start from closed position (-windowWidth) and move towards 0
|
||||||
@@ -359,13 +343,6 @@ function AppContainer({
|
|||||||
isGesturing.value = false;
|
isGesturing.value = false;
|
||||||
// Open if dragged more than 1/3 of sidebar or fast swipe
|
// Open if dragged more than 1/3 of sidebar or fast swipe
|
||||||
const shouldOpen = event.translationX > windowWidth / 3 || event.velocityX > 500;
|
const shouldOpen = event.translationX > windowWidth / 3 || event.velocityX > 500;
|
||||||
runOnJS(logLeftSidebarOpenGesture)("end", {
|
|
||||||
translationX: event.translationX,
|
|
||||||
velocityX: event.velocityX,
|
|
||||||
shouldOpen,
|
|
||||||
mobileView,
|
|
||||||
openGestureEnabled,
|
|
||||||
});
|
|
||||||
if (shouldOpen) {
|
if (shouldOpen) {
|
||||||
animateToOpen();
|
animateToOpen();
|
||||||
runOnJS(openAgentList)();
|
runOnJS(openAgentList)();
|
||||||
|
|||||||
@@ -103,23 +103,6 @@ export function AgentInputArea({
|
|||||||
const { client, isConnected, snapshot } = useHostRuntimeSession(serverId)
|
const { client, isConnected, snapshot } = useHostRuntimeSession(serverId)
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const voice = useVoiceOptional()
|
const voice = useVoiceOptional()
|
||||||
console.log("[AgentInputArea] render", {
|
|
||||||
serverId,
|
|
||||||
agentId,
|
|
||||||
isConnected,
|
|
||||||
isVoiceMode: voice?.isVoiceMode ?? false,
|
|
||||||
isVoiceSwitching: voice?.isVoiceSwitching ?? false,
|
|
||||||
isMuted: voice?.isMuted ?? false,
|
|
||||||
})
|
|
||||||
console.log("[AgentInputArea] voice_snapshot", {
|
|
||||||
serverId,
|
|
||||||
agentId,
|
|
||||||
isVoiceMode: voice?.isVoiceMode ?? false,
|
|
||||||
isVoiceSwitching: voice?.isVoiceSwitching ?? false,
|
|
||||||
isMuted: voice?.isMuted ?? false,
|
|
||||||
activeServerId: voice?.activeServerId ?? null,
|
|
||||||
activeAgentId: voice?.activeAgentId ?? null,
|
|
||||||
})
|
|
||||||
const isDictationReady =
|
const isDictationReady =
|
||||||
isConnected &&
|
isConnected &&
|
||||||
(snapshot?.agentDirectoryStatus === 'ready' ||
|
(snapshot?.agentDirectoryStatus === 'ready' ||
|
||||||
@@ -168,19 +151,6 @@ export function AgentInputArea({
|
|||||||
`message-input:${serverId}:${agentId}:${Math.random().toString(36).slice(2)}`
|
`message-input:${serverId}:${agentId}:${Math.random().toString(36).slice(2)}`
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log("[AgentInputArea] mount", {
|
|
||||||
serverId,
|
|
||||||
agentId,
|
|
||||||
})
|
|
||||||
return () => {
|
|
||||||
console.log("[AgentInputArea] unmount", {
|
|
||||||
serverId,
|
|
||||||
agentId,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [agentId, serverId])
|
|
||||||
|
|
||||||
const autocomplete = useAgentAutocomplete({
|
const autocomplete = useAgentAutocomplete({
|
||||||
userInput,
|
userInput,
|
||||||
cursorIndex,
|
cursorIndex,
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
Modal,
|
Modal,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { SafeAreaView } from "react-native-safe-area-context";
|
import { SafeAreaView } from "react-native-safe-area-context";
|
||||||
import { useEffect } from "react";
|
|
||||||
import { StyleSheet } from "react-native-unistyles";
|
import { StyleSheet } from "react-native-unistyles";
|
||||||
import { Fonts } from "@/constants/theme";
|
import { Fonts } from "@/constants/theme";
|
||||||
|
|
||||||
@@ -154,16 +153,6 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
export function ArtifactDrawer({ artifact, onClose }: ArtifactDrawerProps) {
|
export function ArtifactDrawer({ artifact, onClose }: ArtifactDrawerProps) {
|
||||||
useEffect(() => {
|
|
||||||
if (!artifact) return;
|
|
||||||
console.log(
|
|
||||||
"[ArtifactDrawer] Showing artifact:",
|
|
||||||
artifact.id,
|
|
||||||
artifact.type,
|
|
||||||
artifact.title
|
|
||||||
);
|
|
||||||
}, [artifact]);
|
|
||||||
|
|
||||||
if (!artifact) {
|
if (!artifact) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,13 +23,7 @@ import { FileExplorerPane } from "./file-explorer-pane";
|
|||||||
import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
|
import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
|
||||||
|
|
||||||
const MIN_CHAT_WIDTH = 400;
|
const MIN_CHAT_WIDTH = 400;
|
||||||
const IS_DEV = Boolean((globalThis as { __DEV__?: boolean }).__DEV__);
|
function logExplorerSidebar(_event: string, _details: Record<string, unknown>): void {
|
||||||
|
|
||||||
function logExplorerSidebar(event: string, details: Record<string, unknown>): void {
|
|
||||||
if (!IS_DEV) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log(`[ExplorerSidebar] ${event}`, details);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ExplorerSidebarProps {
|
interface ExplorerSidebarProps {
|
||||||
|
|||||||
@@ -32,14 +32,6 @@ import {
|
|||||||
import { useKeyboardShortcutsStore } from '@/stores/keyboard-shortcuts-store'
|
import { useKeyboardShortcutsStore } from '@/stores/keyboard-shortcuts-store'
|
||||||
|
|
||||||
const DESKTOP_SIDEBAR_WIDTH = 320
|
const DESKTOP_SIDEBAR_WIDTH = 320
|
||||||
const IS_DEV = Boolean((globalThis as { __DEV__?: boolean }).__DEV__)
|
|
||||||
|
|
||||||
function logLeftSidebarCloseGesture(event: string, details: Record<string, unknown>): void {
|
|
||||||
if (!IS_DEV) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
console.log(`[LeftSidebarCloseGesture] ${event}`, details)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LeftSidebarProps {
|
interface LeftSidebarProps {
|
||||||
selectedAgentId?: string
|
selectedAgentId?: string
|
||||||
@@ -144,70 +136,6 @@ export function LeftSidebar({ selectedAgentId: _selectedAgentId }: LeftSidebarPr
|
|||||||
// Track user-initiated refresh to avoid showing spinner on background revalidation
|
// Track user-initiated refresh to avoid showing spinner on background revalidation
|
||||||
const [isManualRefresh, setIsManualRefresh] = useState(false)
|
const [isManualRefresh, setIsManualRefresh] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log('[LeftSidebar] isOpen_changed', {
|
|
||||||
isOpen,
|
|
||||||
isMobile,
|
|
||||||
mobileView,
|
|
||||||
desktopAgentListOpen,
|
|
||||||
})
|
|
||||||
}, [desktopAgentListOpen, isMobile, isOpen, mobileView])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log('[LeftSidebar] pathname_changed', {
|
|
||||||
pathname,
|
|
||||||
activeServerIdFromPath,
|
|
||||||
activeServerId,
|
|
||||||
})
|
|
||||||
}, [activeServerId, activeServerIdFromPath, pathname])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log('[LeftSidebar] hosts_changed', {
|
|
||||||
hostCount: daemons.length,
|
|
||||||
serverIds: daemons.map((daemon) => daemon.serverId),
|
|
||||||
runtimeConnectionStatusSignature,
|
|
||||||
})
|
|
||||||
}, [daemons, runtimeConnectionStatusSignature])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log('[LeftSidebar] active_host_changed', {
|
|
||||||
activeServerId,
|
|
||||||
activeHostLabel,
|
|
||||||
activeHostStatus,
|
|
||||||
})
|
|
||||||
}, [activeHostLabel, activeHostStatus, activeServerId])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log('[LeftSidebar] projects_changed', {
|
|
||||||
activeServerId,
|
|
||||||
projectCount: projects.length,
|
|
||||||
projectKeys: projects.map((project) => project.projectKey),
|
|
||||||
workspaceCounts: projects.map((project) => ({
|
|
||||||
projectKey: project.projectKey,
|
|
||||||
workspaceCount: project.workspaces.length,
|
|
||||||
})),
|
|
||||||
isInitialLoad,
|
|
||||||
isRevalidating,
|
|
||||||
})
|
|
||||||
}, [activeServerId, isInitialLoad, isRevalidating, projects])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log('[LeftSidebar] collapsed_or_shortcuts_changed', {
|
|
||||||
collapsedProjectKeys: Array.from(collapsedProjectKeys),
|
|
||||||
shortcutCount: shortcutIndexByWorkspaceKey.size,
|
|
||||||
})
|
|
||||||
}, [collapsedProjectKeys, shortcutIndexByWorkspaceKey])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log('[LeftSidebar] animation_context_changed', {
|
|
||||||
windowWidth,
|
|
||||||
translateXValue: translateX.value,
|
|
||||||
backdropOpacityValue: backdropOpacity.value,
|
|
||||||
isGesturing: isGesturing.value,
|
|
||||||
hasCloseGestureRef: Boolean(closeGestureRef.current),
|
|
||||||
})
|
|
||||||
}, [backdropOpacity, closeGestureRef, isGesturing, translateX, windowWidth])
|
|
||||||
|
|
||||||
const handleRefresh = useCallback(() => {
|
const handleRefresh = useCallback(() => {
|
||||||
setIsManualRefresh(true)
|
setIsManualRefresh(true)
|
||||||
refreshAll()
|
refreshAll()
|
||||||
@@ -320,7 +248,6 @@ export function LeftSidebar({ selectedAgentId: _selectedAgentId }: LeftSidebarPr
|
|||||||
})
|
})
|
||||||
.onStart(() => {
|
.onStart(() => {
|
||||||
isGesturing.value = true
|
isGesturing.value = true
|
||||||
runOnJS(logLeftSidebarCloseGesture)('start', { isOpen, isMobile })
|
|
||||||
})
|
})
|
||||||
.onUpdate((event) => {
|
.onUpdate((event) => {
|
||||||
if (!isMobile) return
|
if (!isMobile) return
|
||||||
@@ -338,11 +265,6 @@ export function LeftSidebar({ selectedAgentId: _selectedAgentId }: LeftSidebarPr
|
|||||||
isGesturing.value = false
|
isGesturing.value = false
|
||||||
if (!isMobile) return
|
if (!isMobile) return
|
||||||
const shouldClose = event.translationX < -windowWidth / 3 || event.velocityX < -500
|
const shouldClose = event.translationX < -windowWidth / 3 || event.velocityX < -500
|
||||||
runOnJS(logLeftSidebarCloseGesture)('end', {
|
|
||||||
translationX: event.translationX,
|
|
||||||
velocityX: event.velocityX,
|
|
||||||
shouldClose,
|
|
||||||
})
|
|
||||||
if (shouldClose) {
|
if (shouldClose) {
|
||||||
animateToClose()
|
animateToClose()
|
||||||
runOnJS(handleClose)()
|
runOnJS(handleClose)()
|
||||||
|
|||||||
@@ -216,18 +216,6 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
|
|||||||
markScrollInvestigationRender(investigationComponentId)
|
markScrollInvestigationRender(investigationComponentId)
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const voice = useVoiceOptional()
|
const voice = useVoiceOptional()
|
||||||
console.log("[MessageInput] render", {
|
|
||||||
voiceServerId: voiceServerId ?? null,
|
|
||||||
voiceAgentId: voiceAgentId ?? null,
|
|
||||||
hasVoice: Boolean(voice),
|
|
||||||
isVoiceMode: voice?.isVoiceMode ?? false,
|
|
||||||
isVoiceSwitching: voice?.isVoiceSwitching ?? false,
|
|
||||||
isMuted: voice?.isMuted ?? false,
|
|
||||||
disabled,
|
|
||||||
isSubmitLoading,
|
|
||||||
valueLength: value.length,
|
|
||||||
imageCount: images.length,
|
|
||||||
})
|
|
||||||
const [inputHeight, setInputHeight] = useState(MIN_INPUT_HEIGHT)
|
const [inputHeight, setInputHeight] = useState(MIN_INPUT_HEIGHT)
|
||||||
const rootRef = useRef<View | null>(null)
|
const rootRef = useRef<View | null>(null)
|
||||||
const inputWrapperRef = useRef<View | null>(null)
|
const inputWrapperRef = useRef<View | null>(null)
|
||||||
@@ -301,19 +289,6 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log("[MessageInput] mount", {
|
|
||||||
voiceServerId: voiceServerId ?? null,
|
|
||||||
voiceAgentId: voiceAgentId ?? null,
|
|
||||||
})
|
|
||||||
return () => {
|
|
||||||
console.log("[MessageInput] unmount", {
|
|
||||||
voiceServerId: voiceServerId ?? null,
|
|
||||||
voiceAgentId: voiceAgentId ?? null,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [voiceAgentId, voiceServerId])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
valueRef.current = value
|
valueRef.current = value
|
||||||
}, [value])
|
}, [value])
|
||||||
@@ -428,25 +403,6 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
|
|||||||
const showRealtimeOverlay = isRealtimeVoiceForCurrentAgent
|
const showRealtimeOverlay = isRealtimeVoiceForCurrentAgent
|
||||||
const showOverlay = showDictationOverlay || showRealtimeOverlay
|
const showOverlay = showDictationOverlay || showRealtimeOverlay
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log("[MessageInput] overlay_state", {
|
|
||||||
showDictationOverlay,
|
|
||||||
showRealtimeOverlay,
|
|
||||||
showOverlay,
|
|
||||||
isDictating,
|
|
||||||
isDictationProcessing,
|
|
||||||
dictationStatus,
|
|
||||||
isRealtimeVoiceForCurrentAgent,
|
|
||||||
})
|
|
||||||
}, [
|
|
||||||
dictationStatus,
|
|
||||||
isDictating,
|
|
||||||
isDictationProcessing,
|
|
||||||
isRealtimeVoiceForCurrentAgent,
|
|
||||||
showDictationOverlay,
|
|
||||||
showOverlay,
|
|
||||||
showRealtimeOverlay,
|
|
||||||
])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isDictating || isDictationProcessing) {
|
if (isDictating || isDictationProcessing) {
|
||||||
@@ -484,11 +440,6 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
const handleVoicePress = useCallback(async () => {
|
const handleVoicePress = useCallback(async () => {
|
||||||
console.log("[MessageInput] handleVoicePress", {
|
|
||||||
isRealtimeVoiceForCurrentAgent,
|
|
||||||
isDictating,
|
|
||||||
hasVoice: Boolean(voice),
|
|
||||||
})
|
|
||||||
if (isRealtimeVoiceForCurrentAgent && voice) {
|
if (isRealtimeVoiceForCurrentAgent && voice) {
|
||||||
voice.toggleMute()
|
voice.toggleMute()
|
||||||
return
|
return
|
||||||
@@ -508,39 +459,28 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
|
|||||||
])
|
])
|
||||||
|
|
||||||
const handleCancelRecording = useCallback(async () => {
|
const handleCancelRecording = useCallback(async () => {
|
||||||
console.log("[MessageInput] handleCancelRecording")
|
|
||||||
await cancelDictation()
|
await cancelDictation()
|
||||||
}, [cancelDictation])
|
}, [cancelDictation])
|
||||||
|
|
||||||
const handleAcceptRecording = useCallback(async () => {
|
const handleAcceptRecording = useCallback(async () => {
|
||||||
console.log("[MessageInput] handleAcceptRecording")
|
|
||||||
sendAfterTranscriptRef.current = false
|
sendAfterTranscriptRef.current = false
|
||||||
await confirmDictation()
|
await confirmDictation()
|
||||||
}, [confirmDictation])
|
}, [confirmDictation])
|
||||||
|
|
||||||
const handleAcceptAndSendRecording = useCallback(async () => {
|
const handleAcceptAndSendRecording = useCallback(async () => {
|
||||||
console.log("[MessageInput] handleAcceptAndSendRecording")
|
|
||||||
sendAfterTranscriptRef.current = true
|
sendAfterTranscriptRef.current = true
|
||||||
await confirmDictation()
|
await confirmDictation()
|
||||||
}, [confirmDictation])
|
}, [confirmDictation])
|
||||||
|
|
||||||
const handleRetryFailedRecording = useCallback(() => {
|
const handleRetryFailedRecording = useCallback(() => {
|
||||||
console.log("[MessageInput] handleRetryFailedRecording")
|
|
||||||
void retryFailedDictation()
|
void retryFailedDictation()
|
||||||
}, [retryFailedDictation])
|
}, [retryFailedDictation])
|
||||||
|
|
||||||
const handleDiscardFailedRecording = useCallback(() => {
|
const handleDiscardFailedRecording = useCallback(() => {
|
||||||
console.log("[MessageInput] handleDiscardFailedRecording")
|
|
||||||
discardFailedDictation()
|
discardFailedDictation()
|
||||||
}, [discardFailedDictation])
|
}, [discardFailedDictation])
|
||||||
|
|
||||||
const handleStopRealtimeVoice = useCallback(async () => {
|
const handleStopRealtimeVoice = useCallback(async () => {
|
||||||
console.log("[MessageInput] handleStopRealtimeVoice", {
|
|
||||||
hasVoice: Boolean(voice),
|
|
||||||
isRealtimeVoiceForCurrentAgent,
|
|
||||||
isAgentRunning,
|
|
||||||
voiceAgentId: voiceAgentId ?? null,
|
|
||||||
})
|
|
||||||
if (!voice || !isRealtimeVoiceForCurrentAgent) {
|
if (!voice || !isRealtimeVoiceForCurrentAgent) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -560,18 +500,6 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
|
|||||||
}, [client, isAgentRunning, isRealtimeVoiceForCurrentAgent, voice, voiceAgentId])
|
}, [client, isAgentRunning, isRealtimeVoiceForCurrentAgent, voice, voiceAgentId])
|
||||||
|
|
||||||
const handleToggleRealtimeVoiceShortcut = useCallback(() => {
|
const handleToggleRealtimeVoiceShortcut = useCallback(() => {
|
||||||
console.log("[MessageInput] handleToggleRealtimeVoiceShortcut", {
|
|
||||||
hasVoice: Boolean(voice),
|
|
||||||
voiceServerId: voiceServerId ?? null,
|
|
||||||
voiceAgentId: voiceAgentId ?? null,
|
|
||||||
isConnected,
|
|
||||||
disabled,
|
|
||||||
isVoiceSwitching: voice?.isVoiceSwitching ?? false,
|
|
||||||
isVoiceModeForAgent:
|
|
||||||
voice && voiceServerId && voiceAgentId
|
|
||||||
? voice.isVoiceModeForAgent(voiceServerId, voiceAgentId)
|
|
||||||
: false,
|
|
||||||
})
|
|
||||||
if (!voice || !voiceServerId || !voiceAgentId || !isConnected || disabled) {
|
if (!voice || !voiceServerId || !voiceAgentId || !isConnected || disabled) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -593,11 +521,6 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
|
|||||||
}, [disabled, handleStopRealtimeVoice, isConnected, toast, voice, voiceAgentId, voiceServerId])
|
}, [disabled, handleStopRealtimeVoice, isConnected, toast, voice, voiceAgentId, voiceServerId])
|
||||||
|
|
||||||
const handleSendMessage = useCallback(() => {
|
const handleSendMessage = useCallback(() => {
|
||||||
console.log("[MessageInput] handleSendMessage", {
|
|
||||||
valueLength: value.length,
|
|
||||||
imageCount: images.length,
|
|
||||||
isAgentRunning,
|
|
||||||
})
|
|
||||||
const trimmed = value.trim()
|
const trimmed = value.trim()
|
||||||
if (!trimmed && images.length === 0) return
|
if (!trimmed && images.length === 0) return
|
||||||
const payload = {
|
const payload = {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { ActivityIndicator, Pressable, View } from "react-native";
|
import { ActivityIndicator, Pressable, View } from "react-native";
|
||||||
import { useEffect } from "react";
|
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import { Mic, MicOff, Square } from "lucide-react-native";
|
import { Mic, MicOff, Square } from "lucide-react-native";
|
||||||
import { FOOTER_HEIGHT } from "@/constants/layout";
|
import { FOOTER_HEIGHT } from "@/constants/layout";
|
||||||
@@ -24,21 +23,6 @@ export function RealtimeVoiceOverlay({
|
|||||||
}: RealtimeVoiceOverlayProps) {
|
}: RealtimeVoiceOverlayProps) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const { volume, isDetecting, isSpeaking } = useVoiceTelemetry();
|
const { volume, isDetecting, isSpeaking } = useVoiceTelemetry();
|
||||||
console.log("[RealtimeVoiceOverlay] render", {
|
|
||||||
isMuted,
|
|
||||||
isSwitching,
|
|
||||||
volume,
|
|
||||||
isDetecting,
|
|
||||||
isSpeaking,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log("[RealtimeVoiceOverlay] mount");
|
|
||||||
return () => {
|
|
||||||
console.log("[RealtimeVoiceOverlay] unmount");
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<View style={styles.meterContainer}>
|
<View style={styles.meterContainer}>
|
||||||
|
|||||||
@@ -257,7 +257,6 @@ function NewWorktreeButton({
|
|||||||
function useLongPressDragInteraction(input: {
|
function useLongPressDragInteraction(input: {
|
||||||
drag: () => void
|
drag: () => void
|
||||||
menuController: ReturnType<typeof useContextMenu> | null
|
menuController: ReturnType<typeof useContextMenu> | null
|
||||||
debugId: string
|
|
||||||
}) {
|
}) {
|
||||||
const didLongPressRef = useRef(false)
|
const didLongPressRef = useRef(false)
|
||||||
const dragArmedRef = useRef(false)
|
const dragArmedRef = useRef(false)
|
||||||
@@ -295,15 +294,11 @@ function useLongPressDragInteraction(input: {
|
|||||||
input.menuController.setOpen(true)
|
input.menuController.setOpen(true)
|
||||||
menuOpenedRef.current = true
|
menuOpenedRef.current = true
|
||||||
didLongPressRef.current = true
|
didLongPressRef.current = true
|
||||||
console.log('[sidebar-dnd-debug] context menu opened', { id: input.debugId })
|
}, [input.menuController])
|
||||||
}, [input.debugId, input.menuController])
|
|
||||||
|
|
||||||
const handleLongPress = useCallback(() => {
|
const handleLongPress = useCallback(() => {
|
||||||
// Manual timers own long-press behavior on mobile.
|
// Manual timers own long-press behavior on mobile.
|
||||||
console.log('[sidebar-dnd-debug] native onLongPress ignored (manual state machine active)', {
|
}, [])
|
||||||
id: input.debugId,
|
|
||||||
})
|
|
||||||
}, [input.debugId])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@@ -332,16 +327,11 @@ function useLongPressDragInteraction(input: {
|
|||||||
const dy = current.y - start.y
|
const dy = current.y - start.y
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy)
|
const distance = Math.sqrt(dx * dx + dy * dy)
|
||||||
if (distance > DRAG_ARM_STATIONARY_SLOP_PX) {
|
if (distance > DRAG_ARM_STATIONARY_SLOP_PX) {
|
||||||
console.log('[sidebar-dnd-debug] drag arm cancelled (movement)', {
|
|
||||||
id: input.debugId,
|
|
||||||
distance,
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
dragArmedRef.current = true
|
dragArmedRef.current = true
|
||||||
dragActivatedRef.current = true
|
dragActivatedRef.current = true
|
||||||
didLongPressRef.current = true
|
didLongPressRef.current = true
|
||||||
console.log('[sidebar-dnd-debug] drag armed', { id: input.debugId })
|
|
||||||
void Haptics.selectionAsync().catch(() => {})
|
void Haptics.selectionAsync().catch(() => {})
|
||||||
input.drag()
|
input.drag()
|
||||||
}, DRAG_ARM_DELAY_MS)
|
}, DRAG_ARM_DELAY_MS)
|
||||||
@@ -363,17 +353,12 @@ function useLongPressDragInteraction(input: {
|
|||||||
const dy = current.y - start.y
|
const dy = current.y - start.y
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy)
|
const distance = Math.sqrt(dx * dx + dy * dy)
|
||||||
if (distance > CONTEXT_MENU_STATIONARY_SLOP_PX) {
|
if (distance > CONTEXT_MENU_STATIONARY_SLOP_PX) {
|
||||||
console.log('[sidebar-dnd-debug] context menu cancelled (movement)', {
|
|
||||||
id: input.debugId,
|
|
||||||
distance,
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
console.log('[sidebar-dnd-debug] long-press armed', { id: input.debugId })
|
|
||||||
void Haptics.selectionAsync().catch(() => {})
|
void Haptics.selectionAsync().catch(() => {})
|
||||||
openContextMenuAtStartPoint()
|
openContextMenuAtStartPoint()
|
||||||
}, CONTEXT_MENU_DELAY_MS)
|
}, CONTEXT_MENU_DELAY_MS)
|
||||||
}, [clearTimers, input.debugId, input.menuController, openContextMenuAtStartPoint])
|
}, [clearTimers, input.menuController, openContextMenuAtStartPoint])
|
||||||
|
|
||||||
const handleDragIntent = useCallback(
|
const handleDragIntent = useCallback(
|
||||||
(details: { dx: number; dy: number; distance: number }) => {
|
(details: { dx: number; dy: number; distance: number }) => {
|
||||||
@@ -383,10 +368,9 @@ function useLongPressDragInteraction(input: {
|
|||||||
didStartDragRef.current = true
|
didStartDragRef.current = true
|
||||||
didLongPressRef.current = true
|
didLongPressRef.current = true
|
||||||
clearTimers()
|
clearTimers()
|
||||||
console.log('[sidebar-dnd-debug] drag movement detected', { id: input.debugId, ...details })
|
|
||||||
void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium).catch(() => {})
|
void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium).catch(() => {})
|
||||||
},
|
},
|
||||||
[clearTimers, input.debugId]
|
[clearTimers]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleScrollIntent = useCallback(
|
const handleScrollIntent = useCallback(
|
||||||
@@ -394,18 +378,16 @@ function useLongPressDragInteraction(input: {
|
|||||||
scrollIntentRef.current = true
|
scrollIntentRef.current = true
|
||||||
didLongPressRef.current = true
|
didLongPressRef.current = true
|
||||||
clearTimers()
|
clearTimers()
|
||||||
console.log('[sidebar-dnd-debug] scroll intent detected', { id: input.debugId, ...details })
|
|
||||||
},
|
},
|
||||||
[clearTimers, input.debugId]
|
[clearTimers]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleSwipeIntent = useCallback(
|
const handleSwipeIntent = useCallback(
|
||||||
(details: { dx: number; dy: number; distance: number }) => {
|
(details: { dx: number; dy: number; distance: number }) => {
|
||||||
didLongPressRef.current = true
|
didLongPressRef.current = true
|
||||||
clearTimers()
|
clearTimers()
|
||||||
console.log('[sidebar-dnd-debug] swipe intent detected', { id: input.debugId, ...details })
|
|
||||||
},
|
},
|
||||||
[clearTimers, input.debugId]
|
[clearTimers]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handlePressIn = useCallback((event: GestureResponderEvent) => {
|
const handlePressIn = useCallback((event: GestureResponderEvent) => {
|
||||||
@@ -423,13 +405,8 @@ function useLongPressDragInteraction(input: {
|
|||||||
x: event.nativeEvent.pageX,
|
x: event.nativeEvent.pageX,
|
||||||
y: event.nativeEvent.pageY,
|
y: event.nativeEvent.pageY,
|
||||||
}
|
}
|
||||||
console.log('[sidebar-dnd-debug] press-in', {
|
|
||||||
id: input.debugId,
|
|
||||||
x: event.nativeEvent.pageX,
|
|
||||||
y: event.nativeEvent.pageY,
|
|
||||||
})
|
|
||||||
armTimers()
|
armTimers()
|
||||||
}, [armTimers, input.debugId])
|
}, [armTimers])
|
||||||
|
|
||||||
const handleTouchMove = useCallback(
|
const handleTouchMove = useCallback(
|
||||||
(event: any) => {
|
(event: any) => {
|
||||||
@@ -476,20 +453,11 @@ function useLongPressDragInteraction(input: {
|
|||||||
|
|
||||||
const handlePressOut = useCallback(() => {
|
const handlePressOut = useCallback(() => {
|
||||||
clearTimers()
|
clearTimers()
|
||||||
console.log('[sidebar-dnd-debug] press-out no context-menu', {
|
|
||||||
id: input.debugId,
|
|
||||||
didLongPress: didLongPressRef.current,
|
|
||||||
didStartDrag: didStartDragRef.current,
|
|
||||||
dragActivated: dragActivatedRef.current,
|
|
||||||
scrollIntent: scrollIntentRef.current,
|
|
||||||
dragArmed: dragArmedRef.current,
|
|
||||||
menuOpened: menuOpenedRef.current,
|
|
||||||
})
|
|
||||||
dragArmedRef.current = false
|
dragArmedRef.current = false
|
||||||
dragActivatedRef.current = false
|
dragActivatedRef.current = false
|
||||||
touchStartRef.current = null
|
touchStartRef.current = null
|
||||||
touchCurrentRef.current = null
|
touchCurrentRef.current = null
|
||||||
}, [clearTimers, input.debugId])
|
}, [clearTimers])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
didLongPressRef,
|
didLongPressRef,
|
||||||
@@ -518,7 +486,6 @@ function ProjectHeaderRow({
|
|||||||
const interaction = useLongPressDragInteraction({
|
const interaction = useLongPressDragInteraction({
|
||||||
drag,
|
drag,
|
||||||
menuController,
|
menuController,
|
||||||
debugId: `project:${project.projectKey}`,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const handlePress = useCallback(() => {
|
const handlePress = useCallback(() => {
|
||||||
@@ -626,7 +593,6 @@ function WorkspaceRowInner({
|
|||||||
const interaction = useLongPressDragInteraction({
|
const interaction = useLongPressDragInteraction({
|
||||||
drag,
|
drag,
|
||||||
menuController,
|
menuController,
|
||||||
debugId: `workspace:${workspace.workspaceKey}`,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const handlePress = useCallback(() => {
|
const handlePress = useCallback(() => {
|
||||||
@@ -1474,9 +1440,7 @@ export function SidebarWorkspaceList({
|
|||||||
style={styles.list}
|
style={styles.list}
|
||||||
contentContainerStyle={styles.listContent}
|
contentContainerStyle={styles.listContent}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
onScrollBeginDrag={() =>
|
|
||||||
console.log('[sidebar-dnd-debug] outer scroll begin')
|
|
||||||
}
|
|
||||||
testID="sidebar-project-workspace-list-scroll"
|
testID="sidebar-project-workspace-list-scroll"
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
@@ -1486,9 +1450,7 @@ export function SidebarWorkspaceList({
|
|||||||
style={styles.list}
|
style={styles.list}
|
||||||
contentContainerStyle={styles.listContent}
|
contentContainerStyle={styles.listContent}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
onScrollBeginDrag={() =>
|
|
||||||
console.log('[sidebar-dnd-debug] outer scroll begin')
|
|
||||||
}
|
|
||||||
testID="sidebar-project-workspace-list-scroll"
|
testID="sidebar-project-workspace-list-scroll"
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
|
|||||||
@@ -170,10 +170,6 @@ export function useDropdownFloating({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const update = useCallback(async () => {
|
const update = useCallback(async () => {
|
||||||
console.log("[useDropdownFloating] update called", {
|
|
||||||
hasReferenceEl: !!referenceEl,
|
|
||||||
hasFloatingEl: !!floatingElRef.current,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!referenceEl || !floatingElRef.current) {
|
if (!referenceEl || !floatingElRef.current) {
|
||||||
return;
|
return;
|
||||||
@@ -185,12 +181,6 @@ export function useDropdownFloating({
|
|||||||
measureElement(floatingElRef.current),
|
measureElement(floatingElRef.current),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
console.log("[useDropdownFloating] measured", {
|
|
||||||
fromRect,
|
|
||||||
contentRect,
|
|
||||||
displayArea,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = computeGeometry({
|
const result = computeGeometry({
|
||||||
fromRect,
|
fromRect,
|
||||||
contentSize: { width: contentRect.width, height: contentRect.height },
|
contentSize: { width: contentRect.width, height: contentRect.height },
|
||||||
@@ -201,8 +191,6 @@ export function useDropdownFloating({
|
|||||||
padding,
|
padding,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("[useDropdownFloating] geometry result", result);
|
|
||||||
|
|
||||||
setGeometry(result);
|
setGeometry(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("[useDropdownFloating] measure failed:", e);
|
console.warn("[useDropdownFloating] measure failed:", e);
|
||||||
@@ -210,7 +198,6 @@ export function useDropdownFloating({
|
|||||||
}, [referenceEl, displayArea, placement, alignment, offset, padding]);
|
}, [referenceEl, displayArea, placement, alignment, offset, padding]);
|
||||||
|
|
||||||
const floatingRef = useCallback((el: View | null) => {
|
const floatingRef = useCallback((el: View | null) => {
|
||||||
console.log("[useDropdownFloating] floatingRef called", { hasEl: !!el });
|
|
||||||
floatingElRef.current = el;
|
floatingElRef.current = el;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -218,17 +205,10 @@ export function useDropdownFloating({
|
|||||||
const [floatingReady, setFloatingReady] = useState(false);
|
const [floatingReady, setFloatingReady] = useState(false);
|
||||||
|
|
||||||
const handleFloatingLayout = useCallback(() => {
|
const handleFloatingLayout = useCallback(() => {
|
||||||
console.log("[useDropdownFloating] handleFloatingLayout called");
|
|
||||||
setFloatingReady(true);
|
setFloatingReady(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("[useDropdownFloating] useEffect", {
|
|
||||||
open,
|
|
||||||
floatingReady,
|
|
||||||
hasReferenceEl: !!referenceEl,
|
|
||||||
hasFloatingEl: !!floatingElRef.current,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!open) {
|
if (!open) {
|
||||||
setGeometry(null);
|
setGeometry(null);
|
||||||
@@ -237,7 +217,6 @@ export function useDropdownFloating({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (floatingReady && referenceEl && floatingElRef.current) {
|
if (floatingReady && referenceEl && floatingElRef.current) {
|
||||||
console.log("[useDropdownFloating] calling update from useEffect");
|
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
}, [open, floatingReady, referenceEl, update]);
|
}, [open, floatingReady, referenceEl, update]);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { ActivityIndicator, Alert, Pressable, View } from "react-native";
|
import { ActivityIndicator, Alert, Pressable, View } from "react-native";
|
||||||
import { useEffect } from "react";
|
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import { Mic, MicOff, Square } from "lucide-react-native";
|
import { Mic, MicOff, Square } from "lucide-react-native";
|
||||||
import { VolumeMeter } from "@/components/volume-meter";
|
import { VolumeMeter } from "@/components/volume-meter";
|
||||||
@@ -15,22 +14,6 @@ export function VoiceCompactIndicator() {
|
|||||||
toggleMute,
|
toggleMute,
|
||||||
stopVoice,
|
stopVoice,
|
||||||
} = useVoice();
|
} = useVoice();
|
||||||
console.log("[VoiceCompactIndicator] render", {
|
|
||||||
isVoiceMode,
|
|
||||||
isVoiceSwitching,
|
|
||||||
isMuted,
|
|
||||||
volume,
|
|
||||||
isDetecting,
|
|
||||||
isSpeaking,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log("[VoiceCompactIndicator] mount");
|
|
||||||
return () => {
|
|
||||||
console.log("[VoiceCompactIndicator] unmount");
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!isVoiceMode) {
|
if (!isVoiceMode) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { View, Pressable } from "react-native";
|
import { View, Pressable } from "react-native";
|
||||||
import { useEffect } from "react";
|
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import { MicOff, Square } from "lucide-react-native";
|
import { MicOff, Square } from "lucide-react-native";
|
||||||
import { VolumeMeter } from "./volume-meter";
|
import { VolumeMeter } from "./volume-meter";
|
||||||
@@ -16,21 +15,6 @@ export function VoicePanel() {
|
|||||||
toggleMute,
|
toggleMute,
|
||||||
activeServerId,
|
activeServerId,
|
||||||
} = useVoice();
|
} = useVoice();
|
||||||
console.log("[VoicePanel] render", {
|
|
||||||
activeServerId,
|
|
||||||
isMuted,
|
|
||||||
volume,
|
|
||||||
isDetecting,
|
|
||||||
isSpeaking,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log("[VoicePanel] mount");
|
|
||||||
return () => {
|
|
||||||
console.log("[VoicePanel] unmount");
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const hostLabel = activeServerId
|
const hostLabel = activeServerId
|
||||||
? daemons.find((daemon) => daemon.serverId === activeServerId)?.label ?? null
|
? daemons.find((daemon) => daemon.serverId === activeServerId)?.label ?? null
|
||||||
: null;
|
: null;
|
||||||
|
|||||||
@@ -12,18 +12,6 @@ import { usePanelStore } from "@/stores/panel-store";
|
|||||||
|
|
||||||
const ANIMATION_DURATION = 220;
|
const ANIMATION_DURATION = 220;
|
||||||
const ANIMATION_EASING = Easing.bezier(0.25, 0.1, 0.25, 1);
|
const ANIMATION_EASING = Easing.bezier(0.25, 0.1, 0.25, 1);
|
||||||
const IS_DEV = Boolean((globalThis as { __DEV__?: boolean }).__DEV__);
|
|
||||||
|
|
||||||
function logExplorerAnimation(
|
|
||||||
event: string,
|
|
||||||
details: Record<string, unknown>
|
|
||||||
): void {
|
|
||||||
if (!IS_DEV) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log(`[ExplorerAnimation] ${event}`, details);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ExplorerSidebarAnimationContextValue {
|
interface ExplorerSidebarAnimationContextValue {
|
||||||
translateX: SharedValue<number>;
|
translateX: SharedValue<number>;
|
||||||
backdropOpacity: SharedValue<number>;
|
backdropOpacity: SharedValue<number>;
|
||||||
@@ -66,23 +54,9 @@ export function ExplorerSidebarAnimationProvider({ children }: { children: React
|
|||||||
|
|
||||||
// Don't animate if we're in the middle of a gesture - the gesture handler will handle it
|
// Don't animate if we're in the middle of a gesture - the gesture handler will handle it
|
||||||
if (isGesturing.value) {
|
if (isGesturing.value) {
|
||||||
logExplorerAnimation("sync-skipped-during-gesture", {
|
|
||||||
previousIsOpen,
|
|
||||||
nextIsOpen: isOpen,
|
|
||||||
mobileView,
|
|
||||||
desktopFileExplorerOpen,
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logExplorerAnimation("sync-state-change", {
|
|
||||||
previousIsOpen,
|
|
||||||
nextIsOpen: isOpen,
|
|
||||||
mobileView,
|
|
||||||
desktopFileExplorerOpen,
|
|
||||||
windowWidth,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
translateX.value = withTiming(0, {
|
translateX.value = withTiming(0, {
|
||||||
duration: ANIMATION_DURATION,
|
duration: ANIMATION_DURATION,
|
||||||
|
|||||||
@@ -127,17 +127,6 @@ function buildAudioPlaybackSource(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function logVoiceSession(
|
|
||||||
event: string,
|
|
||||||
details?: Record<string, unknown>
|
|
||||||
): void {
|
|
||||||
if (details) {
|
|
||||||
console.log(`[VoiceSession] ${event}`, details);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log(`[VoiceSession] ${event}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const findLatestAssistantMessageText = (items: StreamItem[]): string | null => {
|
const findLatestAssistantMessageText = (items: StreamItem[]): string | null => {
|
||||||
for (let i = items.length - 1; i >= 0; i -= 1) {
|
for (let i = items.length - 1; i >= 0; i -= 1) {
|
||||||
const item = items[i];
|
const item = items[i];
|
||||||
@@ -273,11 +262,6 @@ function SessionProviderInternal({
|
|||||||
}: SessionProviderClientProps) {
|
}: SessionProviderClientProps) {
|
||||||
const voiceRuntime = useVoiceRuntimeOptional();
|
const voiceRuntime = useVoiceRuntimeOptional();
|
||||||
const voiceAudioEngine = useVoiceAudioEngineOptional();
|
const voiceAudioEngine = useVoiceAudioEngineOptional();
|
||||||
console.log("[SessionProvider] render", {
|
|
||||||
serverId,
|
|
||||||
hasVoiceRuntime: Boolean(voiceRuntime),
|
|
||||||
hasVoiceAudioEngine: Boolean(voiceAudioEngine),
|
|
||||||
});
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const isConnected = useHostRuntimeIsConnected(serverId);
|
const isConnected = useHostRuntimeIsConnected(serverId);
|
||||||
|
|
||||||
@@ -519,11 +503,6 @@ function SessionProviderInternal({
|
|||||||
const queue = session?.queuedMessages.get(agent.id);
|
const queue = session?.queuedMessages.get(agent.id);
|
||||||
if (queue && queue.length > 0) {
|
if (queue && queue.length > 0) {
|
||||||
const [next, ...rest] = queue;
|
const [next, ...rest] = queue;
|
||||||
console.log(
|
|
||||||
"[Session] Flushing queued message for agent:",
|
|
||||||
agent.id,
|
|
||||||
next.text
|
|
||||||
);
|
|
||||||
if (sendAgentMessageRef.current) {
|
if (sendAgentMessageRef.current) {
|
||||||
void sendAgentMessageRef.current(agent.id, next.text, next.images);
|
void sendAgentMessageRef.current(agent.id, next.text, next.images);
|
||||||
}
|
}
|
||||||
@@ -684,13 +663,6 @@ function SessionProviderInternal({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Initialize session in store
|
// Initialize session in store
|
||||||
useEffect(() => {
|
|
||||||
console.log("[SessionProvider] mount", { serverId });
|
|
||||||
return () => {
|
|
||||||
console.log("[SessionProvider] unmount", { serverId });
|
|
||||||
};
|
|
||||||
}, [serverId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initializeSession(serverId, client);
|
initializeSession(serverId, client);
|
||||||
}, [serverId, client, initializeSession]);
|
}, [serverId, client, initializeSession]);
|
||||||
@@ -704,44 +676,27 @@ function SessionProviderInternal({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[SessionProvider] register_voice_session", { serverId });
|
|
||||||
return voiceRuntime.registerSession({
|
return voiceRuntime.registerSession({
|
||||||
serverId,
|
serverId,
|
||||||
setVoiceMode: async (enabled, agentId) => {
|
setVoiceMode: async (enabled, agentId) => {
|
||||||
console.log("[SessionProvider] setVoiceMode", {
|
|
||||||
serverId,
|
|
||||||
enabled,
|
|
||||||
agentId: agentId ?? null,
|
|
||||||
});
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
throw new Error("Daemon unavailable");
|
throw new Error("Daemon unavailable");
|
||||||
}
|
}
|
||||||
await client.setVoiceMode(enabled, agentId);
|
await client.setVoiceMode(enabled, agentId);
|
||||||
},
|
},
|
||||||
sendVoiceAudioChunk: async (audioData, mimeType, isLast) => {
|
sendVoiceAudioChunk: async (audioData, mimeType, isLast) => {
|
||||||
console.log("[SessionProvider] sendVoiceAudioChunk", {
|
|
||||||
serverId,
|
|
||||||
audioDataLength: audioData.length,
|
|
||||||
mimeType,
|
|
||||||
isLast,
|
|
||||||
});
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
throw new Error("Daemon unavailable");
|
throw new Error("Daemon unavailable");
|
||||||
}
|
}
|
||||||
await client.sendVoiceAudioChunk(audioData, mimeType, isLast);
|
await client.sendVoiceAudioChunk(audioData, mimeType, isLast);
|
||||||
},
|
},
|
||||||
abortRequest: async () => {
|
abortRequest: async () => {
|
||||||
console.log("[SessionProvider] abortRequest", { serverId });
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
throw new Error("Daemon unavailable");
|
throw new Error("Daemon unavailable");
|
||||||
}
|
}
|
||||||
await client.abortRequest();
|
await client.abortRequest();
|
||||||
},
|
},
|
||||||
setAssistantAudioPlaying: (isPlaying) => {
|
setAssistantAudioPlaying: (isPlaying) => {
|
||||||
console.log("[SessionProvider] setAssistantAudioPlaying", {
|
|
||||||
serverId,
|
|
||||||
isPlaying,
|
|
||||||
});
|
|
||||||
setIsPlayingAudio(serverId, isPlaying);
|
setIsPlayingAudio(serverId, isPlaying);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -753,10 +708,6 @@ function SessionProviderInternal({
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("[SessionProvider] updateSessionConnection", {
|
|
||||||
serverId,
|
|
||||||
isConnected,
|
|
||||||
});
|
|
||||||
voiceRuntime?.updateSessionConnection(serverId, isConnected);
|
voiceRuntime?.updateSessionConnection(serverId, isConnected);
|
||||||
}, [isConnected, serverId, voiceRuntime]);
|
}, [isConnected, serverId, voiceRuntime]);
|
||||||
|
|
||||||
@@ -1244,13 +1195,6 @@ function SessionProviderInternal({
|
|||||||
if (message.type !== "agent_permission_request") return;
|
if (message.type !== "agent_permission_request") return;
|
||||||
const { agentId, request } = message.payload;
|
const { agentId, request } = message.payload;
|
||||||
|
|
||||||
console.log(
|
|
||||||
"[Session] Permission request:",
|
|
||||||
request.id,
|
|
||||||
"for agent:",
|
|
||||||
agentId
|
|
||||||
);
|
|
||||||
|
|
||||||
setPendingPermissions(serverId, (prev) => {
|
setPendingPermissions(serverId, (prev) => {
|
||||||
const next = new Map(prev);
|
const next = new Map(prev);
|
||||||
const key = derivePendingPermissionKey(agentId, request);
|
const key = derivePendingPermissionKey(agentId, request);
|
||||||
@@ -1266,13 +1210,6 @@ function SessionProviderInternal({
|
|||||||
if (message.type !== "agent_permission_resolved") return;
|
if (message.type !== "agent_permission_resolved") return;
|
||||||
const { requestId, agentId } = message.payload;
|
const { requestId, agentId } = message.payload;
|
||||||
|
|
||||||
console.log(
|
|
||||||
"[Session] Permission resolved:",
|
|
||||||
requestId,
|
|
||||||
"for agent:",
|
|
||||||
agentId
|
|
||||||
);
|
|
||||||
|
|
||||||
setPendingPermissions(serverId, (prev) => {
|
setPendingPermissions(serverId, (prev) => {
|
||||||
const next = new Map(prev);
|
const next = new Map(prev);
|
||||||
const derivedKey = `${agentId}:${requestId}`;
|
const derivedKey = `${agentId}:${requestId}`;
|
||||||
@@ -1299,16 +1236,6 @@ function SessionProviderInternal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const payload: AudioOutputPayload = message.payload;
|
const payload: AudioOutputPayload = message.payload;
|
||||||
console.log("[SessionProvider] audio_output", {
|
|
||||||
serverId,
|
|
||||||
id: payload.id,
|
|
||||||
groupId: payload.groupId ?? null,
|
|
||||||
chunkIndex: payload.chunkIndex ?? 0,
|
|
||||||
isLastChunk: payload.isLastChunk ?? true,
|
|
||||||
format: payload.format,
|
|
||||||
isVoiceMode: payload.isVoiceMode ?? false,
|
|
||||||
audioLength: payload.audio.length,
|
|
||||||
});
|
|
||||||
const playbackGroupId = payload.groupId ?? payload.id;
|
const playbackGroupId = payload.groupId ?? payload.id;
|
||||||
const chunkIndex = payload.chunkIndex ?? 0;
|
const chunkIndex = payload.chunkIndex ?? 0;
|
||||||
const isFinalChunk = payload.isLastChunk ?? true;
|
const isFinalChunk = payload.isLastChunk ?? true;
|
||||||
@@ -1338,16 +1265,6 @@ function SessionProviderInternal({
|
|||||||
!payload.isVoiceMode ||
|
!payload.isVoiceMode ||
|
||||||
(voiceRuntime?.shouldPlayVoiceAudio(serverId) ?? false);
|
(voiceRuntime?.shouldPlayVoiceAudio(serverId) ?? false);
|
||||||
const audioBlob = buildAudioPlaybackSource(bufferedChunks);
|
const audioBlob = buildAudioPlaybackSource(bufferedChunks);
|
||||||
if (payload.isVoiceMode) {
|
|
||||||
logVoiceSession("audio_output_ready", {
|
|
||||||
serverId,
|
|
||||||
playbackGroupId,
|
|
||||||
chunkCount: bufferedChunks.length,
|
|
||||||
format: payload.format,
|
|
||||||
blobBytes: audioBlob.size,
|
|
||||||
shouldPlay,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const confirmAudioPlayed = async () => {
|
const confirmAudioPlayed = async () => {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
chunkIds.map((chunkId) =>
|
chunkIds.map((chunkId) =>
|
||||||
@@ -1366,18 +1283,6 @@ function SessionProviderInternal({
|
|||||||
voiceRuntime?.onAssistantAudioStarted(serverId);
|
voiceRuntime?.onAssistantAudioStarted(serverId);
|
||||||
}
|
}
|
||||||
await voiceAudioEngine.play(audioBlob);
|
await voiceAudioEngine.play(audioBlob);
|
||||||
if (payload.isVoiceMode) {
|
|
||||||
logVoiceSession("audio_output_played", {
|
|
||||||
serverId,
|
|
||||||
playbackGroupId,
|
|
||||||
chunkCount: bufferedChunks.length,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (payload.isVoiceMode) {
|
|
||||||
logVoiceSession("audio_output_suppressed", {
|
|
||||||
serverId,
|
|
||||||
playbackGroupId,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
await confirmAudioPlayed();
|
await confirmAudioPlayed();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1397,13 +1302,6 @@ function SessionProviderInternal({
|
|||||||
const unsubActivity = client.on("activity_log", (message) => {
|
const unsubActivity = client.on("activity_log", (message) => {
|
||||||
if (message.type !== "activity_log") return;
|
if (message.type !== "activity_log") return;
|
||||||
const data = message.payload;
|
const data = message.payload;
|
||||||
console.log("[SessionProvider] activity_log", {
|
|
||||||
serverId,
|
|
||||||
type: data.type,
|
|
||||||
contentPreview:
|
|
||||||
typeof data.content === "string" ? data.content.slice(0, 80) : null,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (data.type === "system" && data.content.includes("Transcribing")) {
|
if (data.type === "system" && data.content.includes("Transcribing")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1523,12 +1421,6 @@ function SessionProviderInternal({
|
|||||||
if (message.type !== "transcription_result") return;
|
if (message.type !== "transcription_result") return;
|
||||||
|
|
||||||
const transcriptText = message.payload.text.trim();
|
const transcriptText = message.payload.text.trim();
|
||||||
console.log("[SessionProvider] transcription_result", {
|
|
||||||
serverId,
|
|
||||||
textLength: transcriptText.length,
|
|
||||||
textPreview: transcriptText.slice(0, 80),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!transcriptText) {
|
if (!transcriptText) {
|
||||||
voiceRuntime?.onTranscriptionResult(serverId, transcriptText);
|
voiceRuntime?.onTranscriptionResult(serverId, transcriptText);
|
||||||
return;
|
return;
|
||||||
@@ -1542,7 +1434,6 @@ function SessionProviderInternal({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { agentId } = message.payload;
|
const { agentId } = message.payload;
|
||||||
console.log("[Session] Agent deleted:", agentId);
|
|
||||||
deletePendingAgentUpdate(serverId, agentId);
|
deletePendingAgentUpdate(serverId, agentId);
|
||||||
clearArchiveAgentPending({ queryClient, serverId, agentId });
|
clearArchiveAgentPending({ queryClient, serverId, agentId });
|
||||||
|
|
||||||
@@ -1618,7 +1509,6 @@ function SessionProviderInternal({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { agentId, archivedAt } = message.payload;
|
const { agentId, archivedAt } = message.payload;
|
||||||
console.log("[Session] Agent archived:", agentId);
|
|
||||||
clearArchiveAgentPending({ queryClient, serverId, agentId });
|
clearArchiveAgentPending({ queryClient, serverId, agentId });
|
||||||
|
|
||||||
setAgents(serverId, (prev) => {
|
setAgents(serverId, (prev) => {
|
||||||
@@ -1802,11 +1692,6 @@ function SessionProviderInternal({
|
|||||||
worktreeName?: string;
|
worktreeName?: string;
|
||||||
requestId?: string;
|
requestId?: string;
|
||||||
}) => {
|
}) => {
|
||||||
console.log(
|
|
||||||
"[Session] createAgent called with images:",
|
|
||||||
images?.length ?? 0,
|
|
||||||
images
|
|
||||||
);
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
console.warn("[Session] createAgent skipped: daemon unavailable");
|
console.warn("[Session] createAgent skipped: daemon unavailable");
|
||||||
return;
|
return;
|
||||||
@@ -1815,14 +1700,6 @@ function SessionProviderInternal({
|
|||||||
let imagesData: Array<{ data: string; mimeType: string }> | undefined;
|
let imagesData: Array<{ data: string; mimeType: string }> | undefined;
|
||||||
try {
|
try {
|
||||||
imagesData = await encodeImages(images);
|
imagesData = await encodeImages(images);
|
||||||
console.log(
|
|
||||||
"[Session] encodeImages result:",
|
|
||||||
imagesData?.length ?? 0,
|
|
||||||
imagesData?.map((img) => ({
|
|
||||||
dataLength: img.data?.length ?? 0,
|
|
||||||
mimeType: img.mimeType,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error(
|
||||||
"[Session] Failed to prepare images for agent creation:",
|
"[Session] Failed to prepare images for agent creation:",
|
||||||
|
|||||||
@@ -46,19 +46,6 @@ const VoiceAudioEngineContext = createContext<AudioEngine | null>(null);
|
|||||||
const noopSubscribe = () => () => {};
|
const noopSubscribe = () => () => {};
|
||||||
const getEmptySnapshot = () => EMPTY_SNAPSHOT;
|
const getEmptySnapshot = () => EMPTY_SNAPSHOT;
|
||||||
const getEmptyTelemetry = () => EMPTY_TELEMETRY;
|
const getEmptyTelemetry = () => EMPTY_TELEMETRY;
|
||||||
let nextVoiceProviderInstanceId = 1;
|
|
||||||
|
|
||||||
function getProviderTraceStack(): string | undefined {
|
|
||||||
const stack = new Error().stack;
|
|
||||||
if (!stack) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return stack
|
|
||||||
.split("\n")
|
|
||||||
.slice(2, 7)
|
|
||||||
.map((line) => line.trim())
|
|
||||||
.join(" | ");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useVoice() {
|
export function useVoice() {
|
||||||
const value = useVoiceOptional();
|
const value = useVoiceOptional();
|
||||||
@@ -121,47 +108,21 @@ interface VoiceProviderProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function VoiceProvider({ children }: VoiceProviderProps) {
|
export function VoiceProvider({ children }: VoiceProviderProps) {
|
||||||
const providerIdRef = useRef<number | null>(null);
|
|
||||||
if (providerIdRef.current === null) {
|
|
||||||
providerIdRef.current = nextVoiceProviderInstanceId++;
|
|
||||||
console.log("[VoiceProvider] instance_created", {
|
|
||||||
providerId: providerIdRef.current,
|
|
||||||
stack: getProviderTraceStack(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const providerId = providerIdRef.current;
|
|
||||||
const engineRef = useRef<AudioEngine | null>(null);
|
const engineRef = useRef<AudioEngine | null>(null);
|
||||||
const runtimeRef = useRef<VoiceRuntime | null>(null);
|
const runtimeRef = useRef<VoiceRuntime | null>(null);
|
||||||
console.log("[VoiceProvider] render", {
|
|
||||||
providerId,
|
|
||||||
hasEngine: Boolean(engineRef.current),
|
|
||||||
hasRuntime: Boolean(runtimeRef.current),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!engineRef.current) {
|
if (!engineRef.current) {
|
||||||
let runtime: VoiceRuntime | null = null;
|
let runtime: VoiceRuntime | null = null;
|
||||||
console.log("[VoiceProvider] create_engine_and_runtime");
|
|
||||||
const engine = createAudioEngine({
|
const engine = createAudioEngine({
|
||||||
onCaptureData: (pcm) => {
|
onCaptureData: (pcm) => {
|
||||||
console.log("[VoiceProvider] onCaptureData", {
|
|
||||||
providerId,
|
|
||||||
bytes: pcm.byteLength,
|
|
||||||
});
|
|
||||||
runtime?.handleCapturePcm(pcm);
|
runtime?.handleCapturePcm(pcm);
|
||||||
},
|
},
|
||||||
onVolumeLevel: (level) => {
|
onVolumeLevel: (level) => {
|
||||||
console.log("[VoiceProvider] onVolumeLevel", {
|
|
||||||
providerId,
|
|
||||||
level,
|
|
||||||
});
|
|
||||||
runtime?.handleCaptureVolume(level);
|
runtime?.handleCaptureVolume(level);
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
console.error("[VoiceEngine] Capture error:", error);
|
console.error("[VoiceEngine] Capture error:", error);
|
||||||
},
|
},
|
||||||
}, {
|
|
||||||
traceLabel: `voice-provider:${providerId}`,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
runtime = createVoiceRuntime({
|
runtime = createVoiceRuntime({
|
||||||
@@ -184,18 +145,12 @@ export function VoiceProvider({ children }: VoiceProviderProps) {
|
|||||||
const runtime = runtimeRef.current!;
|
const runtime = runtimeRef.current!;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("[VoiceProvider] mount", {
|
|
||||||
providerId,
|
|
||||||
});
|
|
||||||
return () => {
|
return () => {
|
||||||
console.log("[VoiceProvider] unmount", {
|
|
||||||
providerId,
|
|
||||||
});
|
|
||||||
void runtime.destroy().catch((error) => {
|
void runtime.destroy().catch((error) => {
|
||||||
console.error("[VoiceProvider] Failed to destroy voice runtime", error);
|
console.error("[VoiceProvider] Failed to destroy voice runtime", error);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}, [providerId, runtime]);
|
}, [runtime]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VoiceAudioEngineContext.Provider value={engine}>
|
<VoiceAudioEngineContext.Provider value={engine}>
|
||||||
|
|||||||
@@ -18,41 +18,27 @@ export interface AudioCaptureConfig {
|
|||||||
*/
|
*/
|
||||||
async function getActualRecordingUri(createdAt: Date): Promise<string | null> {
|
async function getActualRecordingUri(createdAt: Date): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
console.log('[AudioRecorder] Searching for recording file created at:', createdAt.toISOString());
|
|
||||||
|
|
||||||
const audioDir = new Directory(Paths.cache, 'Audio');
|
const audioDir = new Directory(Paths.cache, 'Audio');
|
||||||
console.log('[AudioRecorder] Audio cache directory URI:', audioDir.uri);
|
|
||||||
console.log('[AudioRecorder] Directory exists:', audioDir.exists);
|
|
||||||
|
|
||||||
if (!audioDir.exists) {
|
if (!audioDir.exists) {
|
||||||
console.log('[AudioRecorder] Audio cache directory does not exist');
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const files = audioDir.list();
|
const files = audioDir.list();
|
||||||
console.log('[AudioRecorder] Found files in Audio cache:', files.length);
|
|
||||||
|
|
||||||
if (!files.length) {
|
if (!files.length) {
|
||||||
console.log('[AudioRecorder] No files found in Audio cache directory');
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const validFiles = files
|
const validFiles = files
|
||||||
.map(file => {
|
.map(file => {
|
||||||
const info = file.info();
|
const info = file.info();
|
||||||
console.log('[AudioRecorder] File info:', {
|
|
||||||
uri: info.uri,
|
|
||||||
size: info.size,
|
|
||||||
creationTime: info.creationTime ? new Date(info.creationTime).toISOString() : null,
|
|
||||||
});
|
|
||||||
return info;
|
return info;
|
||||||
})
|
})
|
||||||
.filter(f => f.size && f.size > 0);
|
.filter(f => f.size && f.size > 0);
|
||||||
|
|
||||||
console.log('[AudioRecorder] Valid files (size > 0):', validFiles.length);
|
|
||||||
|
|
||||||
if (validFiles.length === 0) {
|
if (validFiles.length === 0) {
|
||||||
console.log('[AudioRecorder] No valid files found (all are zero-byte)');
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,10 +48,6 @@ async function getActualRecordingUri(createdAt: Date): Promise<string | null> {
|
|||||||
for (const file of validFiles) {
|
for (const file of validFiles) {
|
||||||
if (!file.creationTime || !file.uri) continue;
|
if (!file.creationTime || !file.uri) continue;
|
||||||
const diff = Math.abs(file.creationTime - createdAt.getTime());
|
const diff = Math.abs(file.creationTime - createdAt.getTime());
|
||||||
console.log('[AudioRecorder] Time diff for file:', {
|
|
||||||
uri: file.uri,
|
|
||||||
diffMs: diff,
|
|
||||||
});
|
|
||||||
if (diff < minDiff) {
|
if (diff < minDiff) {
|
||||||
closest = file;
|
closest = file;
|
||||||
minDiff = diff;
|
minDiff = diff;
|
||||||
@@ -74,15 +56,9 @@ async function getActualRecordingUri(createdAt: Date): Promise<string | null> {
|
|||||||
|
|
||||||
if (closest) {
|
if (closest) {
|
||||||
const resultUri = closest.uri?.slice(0, -1) ?? null;
|
const resultUri = closest.uri?.slice(0, -1) ?? null;
|
||||||
console.log('[AudioRecorder] Found closest file:', {
|
|
||||||
uri: resultUri,
|
|
||||||
size: closest.size,
|
|
||||||
timeDiffMs: minDiff,
|
|
||||||
});
|
|
||||||
return resultUri;
|
return resultUri;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[AudioRecorder] No closest file found');
|
|
||||||
return null;
|
return null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[AudioRecorder] Error finding actual recording file:', e);
|
console.error('[AudioRecorder] Error finding actual recording file:', e);
|
||||||
@@ -208,7 +184,6 @@ export function useAudioRecorder(config?: AudioCaptureConfig) {
|
|||||||
attemptGuardRef.current.assertCurrent(attemptId);
|
attemptGuardRef.current.assertCurrent(attemptId);
|
||||||
|
|
||||||
// Request microphone permissions
|
// Request microphone permissions
|
||||||
console.log('[AudioRecorder] Requesting recording permissions...');
|
|
||||||
const permissionResponse = await requestRecordingPermissionsAsync();
|
const permissionResponse = await requestRecordingPermissionsAsync();
|
||||||
attemptGuardRef.current.assertCurrent(attemptId);
|
attemptGuardRef.current.assertCurrent(attemptId);
|
||||||
|
|
||||||
@@ -217,38 +192,26 @@ export function useAudioRecorder(config?: AudioCaptureConfig) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Configure audio mode for recording
|
// Configure audio mode for recording
|
||||||
console.log('[AudioRecorder] Configuring audio mode...');
|
|
||||||
await setAudioModeAsync({
|
await setAudioModeAsync({
|
||||||
playsInSilentMode: true,
|
playsInSilentMode: true,
|
||||||
allowsRecording: true,
|
allowsRecording: true,
|
||||||
});
|
});
|
||||||
attemptGuardRef.current.assertCurrent(attemptId);
|
attemptGuardRef.current.assertCurrent(attemptId);
|
||||||
|
|
||||||
console.log('[AudioRecorder] Starting recording with options:', {
|
|
||||||
sampleRate: recordingOptions.sampleRate,
|
|
||||||
numberOfChannels: recordingOptions.numberOfChannels,
|
|
||||||
bitRate: recordingOptions.bitRate,
|
|
||||||
});
|
|
||||||
|
|
||||||
const startTime = new Date();
|
const startTime = new Date();
|
||||||
setRecordingStartTime(startTime);
|
setRecordingStartTime(startTime);
|
||||||
attemptGuardRef.current.assertCurrent(attemptId);
|
attemptGuardRef.current.assertCurrent(attemptId);
|
||||||
|
|
||||||
// Prepare the recorder before recording (required step)
|
// Prepare the recorder before recording (required step)
|
||||||
console.log('[AudioRecorder] Preparing recorder...');
|
|
||||||
await recorder.prepareToRecordAsync();
|
await recorder.prepareToRecordAsync();
|
||||||
attemptGuardRef.current.assertCurrent(attemptId);
|
attemptGuardRef.current.assertCurrent(attemptId);
|
||||||
|
|
||||||
console.log('[AudioRecorder] Starting recording...');
|
|
||||||
await recorder.record();
|
await recorder.record();
|
||||||
attemptGuardRef.current.assertCurrent(attemptId);
|
attemptGuardRef.current.assertCurrent(attemptId);
|
||||||
|
|
||||||
console.log('[AudioRecorder] Recording started at:', startTime.toISOString());
|
|
||||||
console.log('[AudioRecorder] Recorder isRecording:', recorder.isRecording);
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setRecordingStartTime(null);
|
setRecordingStartTime(null);
|
||||||
if (error instanceof AttemptCancelledError) {
|
if (error instanceof AttemptCancelledError) {
|
||||||
console.log('[AudioRecorder] Recording start cancelled.');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (error?.message !== 'Recording cancelled') {
|
if (error?.message !== 'Recording cancelled') {
|
||||||
@@ -278,15 +241,12 @@ export function useAudioRecorder(config?: AudioCaptureConfig) {
|
|||||||
|
|
||||||
// Get URI from recorder
|
// Get URI from recorder
|
||||||
let uri = recorder.uri;
|
let uri = recorder.uri;
|
||||||
console.log('[AudioRecorder] Initial URI from recorder:', uri);
|
|
||||||
|
|
||||||
// Workaround for Expo SDK 54 Android bug - find actual recording file
|
// Workaround for Expo SDK 54 Android bug - find actual recording file
|
||||||
if (recordingStartTime && (!uri || uri === '')) {
|
if (recordingStartTime && (!uri || uri === '')) {
|
||||||
console.log('[AudioRecorder] Using workaround to find actual recording file...');
|
|
||||||
const actualUri = await getActualRecordingUri(recordingStartTime);
|
const actualUri = await getActualRecordingUri(recordingStartTime);
|
||||||
if (actualUri) {
|
if (actualUri) {
|
||||||
uri = actualUri;
|
uri = actualUri;
|
||||||
console.log('[AudioRecorder] Found actual recording URI:', uri);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,7 +259,6 @@ export function useAudioRecorder(config?: AudioCaptureConfig) {
|
|||||||
// Get file info
|
// Get file info
|
||||||
const file = new File(uri);
|
const file = new File(uri);
|
||||||
const exists = file.exists;
|
const exists = file.exists;
|
||||||
console.log('[AudioRecorder] File exists:', exists);
|
|
||||||
|
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
setRecordingStartTime(null);
|
setRecordingStartTime(null);
|
||||||
@@ -309,11 +268,6 @@ export function useAudioRecorder(config?: AudioCaptureConfig) {
|
|||||||
// Convert URI to Blob
|
// Convert URI to Blob
|
||||||
const audioBlob = await uriToBlob(uri);
|
const audioBlob = await uriToBlob(uri);
|
||||||
|
|
||||||
console.log('[AudioRecorder] Recording converted to blob:', {
|
|
||||||
size: audioBlob.size,
|
|
||||||
type: audioBlob.type,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clean up the temporary file
|
// Clean up the temporary file
|
||||||
file.delete();
|
file.delete();
|
||||||
|
|
||||||
|
|||||||
@@ -163,16 +163,6 @@ export function useAudioRecorder(config?: AudioCaptureConfig) {
|
|||||||
throw new Error("Microphone capture is not supported in this environment");
|
throw new Error("Microphone capture is not supported in this environment");
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[AudioRecorder][Web] Microphone preflight", {
|
|
||||||
secureContext,
|
|
||||||
currentOrigin,
|
|
||||||
isTauri,
|
|
||||||
hasMediaDevices:
|
|
||||||
typeof navigator !== "undefined" &&
|
|
||||||
!!navigator.mediaDevices &&
|
|
||||||
typeof navigator.mediaDevices.getUserMedia === "function",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!secureContext && !isTauri) {
|
if (!secureContext && !isTauri) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Microphone access requires HTTPS or localhost. Current origin: ${currentOrigin}`
|
`Microphone access requires HTTPS or localhost. Current origin: ${currentOrigin}`
|
||||||
|
|||||||
@@ -6,53 +6,18 @@ import { createAudioEngine } from "@/voice/audio-engine";
|
|||||||
|
|
||||||
import type { DictationAudioSource, DictationAudioSourceConfig } from "./use-dictation-audio-source.types";
|
import type { DictationAudioSource, DictationAudioSourceConfig } from "./use-dictation-audio-source.types";
|
||||||
|
|
||||||
let nextDictationAudioSourceInstanceId = 1;
|
|
||||||
|
|
||||||
function getDictationTraceStack(): string | undefined {
|
|
||||||
const stack = new Error().stack;
|
|
||||||
if (!stack) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return stack
|
|
||||||
.split("\n")
|
|
||||||
.slice(2, 7)
|
|
||||||
.map((line) => line.trim())
|
|
||||||
.join(" | ");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useDictationAudioSource(config: DictationAudioSourceConfig): DictationAudioSource {
|
export function useDictationAudioSource(config: DictationAudioSourceConfig): DictationAudioSource {
|
||||||
const onPcmSegmentRef = useRef(config.onPcmSegment);
|
const onPcmSegmentRef = useRef(config.onPcmSegment);
|
||||||
const onErrorRef = useRef(config.onError);
|
const onErrorRef = useRef(config.onError);
|
||||||
const [volume, setVolume] = useState(0);
|
const [volume, setVolume] = useState(0);
|
||||||
const sourceIdRef = useRef<number | null>(null);
|
|
||||||
const engineRef = useRef<ReturnType<typeof createAudioEngine> | null>(null);
|
const engineRef = useRef<ReturnType<typeof createAudioEngine> | null>(null);
|
||||||
|
|
||||||
if (sourceIdRef.current === null) {
|
|
||||||
sourceIdRef.current = nextDictationAudioSourceInstanceId++;
|
|
||||||
console.log("[DictationAudioSource] instance_created", {
|
|
||||||
sourceId: sourceIdRef.current,
|
|
||||||
stack: getDictationTraceStack(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const sourceId = sourceIdRef.current;
|
|
||||||
|
|
||||||
console.log("[DictationAudioSource] render", {
|
|
||||||
sourceId,
|
|
||||||
hasEngine: Boolean(engineRef.current),
|
|
||||||
});
|
|
||||||
|
|
||||||
const getOrCreateEngine = useCallback(() => {
|
const getOrCreateEngine = useCallback(() => {
|
||||||
if (engineRef.current) {
|
if (engineRef.current) {
|
||||||
return engineRef.current;
|
return engineRef.current;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[DictationAudioSource] create_engine", {
|
engineRef.current = createAudioEngine({
|
||||||
sourceId,
|
|
||||||
stack: getDictationTraceStack(),
|
|
||||||
});
|
|
||||||
engineRef.current = createAudioEngine(
|
|
||||||
{
|
|
||||||
onCaptureData: (pcm) => {
|
onCaptureData: (pcm) => {
|
||||||
onPcmSegmentRef.current(Buffer.from(pcm).toString("base64"));
|
onPcmSegmentRef.current(Buffer.from(pcm).toString("base64"));
|
||||||
},
|
},
|
||||||
@@ -62,59 +27,33 @@ export function useDictationAudioSource(config: DictationAudioSourceConfig): Dic
|
|||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
onErrorRef.current?.(error);
|
onErrorRef.current?.(error);
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
{
|
|
||||||
traceLabel: `dictation:${sourceId}`,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return engineRef.current;
|
return engineRef.current;
|
||||||
}, [sourceId]);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onPcmSegmentRef.current = config.onPcmSegment;
|
onPcmSegmentRef.current = config.onPcmSegment;
|
||||||
onErrorRef.current = config.onError;
|
onErrorRef.current = config.onError;
|
||||||
}, [config.onPcmSegment, config.onError]);
|
}, [config.onPcmSegment, config.onError]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log("[DictationAudioSource] mount", {
|
|
||||||
sourceId,
|
|
||||||
});
|
|
||||||
return () => {
|
|
||||||
console.log("[DictationAudioSource] unmount", {
|
|
||||||
sourceId,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}, [sourceId]);
|
|
||||||
|
|
||||||
const start = useCallback(async () => {
|
const start = useCallback(async () => {
|
||||||
console.log("[DictationAudioSource] start", {
|
|
||||||
sourceId,
|
|
||||||
});
|
|
||||||
const engine = getOrCreateEngine();
|
const engine = getOrCreateEngine();
|
||||||
await engine.initialize();
|
await engine.initialize();
|
||||||
await engine.startCapture();
|
await engine.startCapture();
|
||||||
}, [getOrCreateEngine, sourceId]);
|
}, [getOrCreateEngine]);
|
||||||
|
|
||||||
const stop = useCallback(async () => {
|
const stop = useCallback(async () => {
|
||||||
console.log("[DictationAudioSource] stop", {
|
|
||||||
sourceId,
|
|
||||||
hasEngine: Boolean(engineRef.current),
|
|
||||||
});
|
|
||||||
await engineRef.current?.stopCapture();
|
await engineRef.current?.stopCapture();
|
||||||
setVolume(0);
|
setVolume(0);
|
||||||
}, [sourceId]);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
const engine = engineRef.current;
|
const engine = engineRef.current;
|
||||||
engineRef.current = null;
|
engineRef.current = null;
|
||||||
console.log("[DictationAudioSource] destroy_engine", {
|
|
||||||
sourceId,
|
|
||||||
hadEngine: Boolean(engine),
|
|
||||||
});
|
|
||||||
void engine?.destroy().catch(() => undefined);
|
void engine?.destroy().catch(() => undefined);
|
||||||
};
|
};
|
||||||
}, [sourceId]);
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
start,
|
start,
|
||||||
|
|||||||
@@ -163,15 +163,6 @@ export function useDictationAudioSource(config: DictationAudioSourceConfig): Dic
|
|||||||
if (missingNavigator) {
|
if (missingNavigator) {
|
||||||
throw new Error("Microphone capture is not supported in this environment");
|
throw new Error("Microphone capture is not supported in this environment");
|
||||||
}
|
}
|
||||||
console.log("[DictationAudio][Web] Microphone preflight", {
|
|
||||||
secureContext,
|
|
||||||
currentOrigin,
|
|
||||||
isTauri,
|
|
||||||
hasMediaDevices:
|
|
||||||
typeof navigator !== "undefined" &&
|
|
||||||
!!navigator.mediaDevices &&
|
|
||||||
typeof navigator.mediaDevices.getUserMedia === "function",
|
|
||||||
});
|
|
||||||
if (!secureContext && !isTauri) {
|
if (!secureContext && !isTauri) {
|
||||||
throw new Error(`Microphone access requires HTTPS or localhost. Current origin: ${currentOrigin}`);
|
throw new Error(`Microphone access requires HTTPS or localhost. Current origin: ${currentOrigin}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,18 +13,6 @@ interface UseExplorerOpenGestureParams {
|
|||||||
onOpen: () => void;
|
onOpen: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const IS_DEV = Boolean((globalThis as { __DEV__?: boolean }).__DEV__);
|
|
||||||
|
|
||||||
function logExplorerOpenGesture(
|
|
||||||
event: string,
|
|
||||||
details: Record<string, unknown>
|
|
||||||
): void {
|
|
||||||
if (!IS_DEV) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log(`[ExplorerOpenGesture] ${event}`, details);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useExplorerOpenGesture({
|
export function useExplorerOpenGesture({
|
||||||
enabled,
|
enabled,
|
||||||
onOpen,
|
onOpen,
|
||||||
@@ -82,7 +70,6 @@ export function useExplorerOpenGesture({
|
|||||||
})
|
})
|
||||||
.onStart(() => {
|
.onStart(() => {
|
||||||
isGesturing.value = true;
|
isGesturing.value = true;
|
||||||
runOnJS(logExplorerOpenGesture)("start", { enabled });
|
|
||||||
})
|
})
|
||||||
.onUpdate((event) => {
|
.onUpdate((event) => {
|
||||||
// Right sidebar: start from closed position (+windowWidth) and move towards 0.
|
// Right sidebar: start from closed position (+windowWidth) and move towards 0.
|
||||||
@@ -103,15 +90,6 @@ export function useExplorerOpenGesture({
|
|||||||
const shouldOpenByPosition = translateX.value < (windowWidth * 2) / 3;
|
const shouldOpenByPosition = translateX.value < (windowWidth * 2) / 3;
|
||||||
const shouldOpenByVelocity = event.velocityX < -500;
|
const shouldOpenByVelocity = event.velocityX < -500;
|
||||||
const shouldOpen = shouldOpenByPosition || shouldOpenByVelocity;
|
const shouldOpen = shouldOpenByPosition || shouldOpenByVelocity;
|
||||||
runOnJS(logExplorerOpenGesture)("end", {
|
|
||||||
translationX: event.translationX,
|
|
||||||
velocityX: event.velocityX,
|
|
||||||
panelTranslateX: translateX.value,
|
|
||||||
windowWidth,
|
|
||||||
shouldOpenByPosition,
|
|
||||||
shouldOpenByVelocity,
|
|
||||||
shouldOpen,
|
|
||||||
});
|
|
||||||
if (shouldOpen) {
|
if (shouldOpen) {
|
||||||
animateToOpen();
|
animateToOpen();
|
||||||
runOnJS(onOpen)();
|
runOnJS(onOpen)();
|
||||||
|
|||||||
@@ -302,13 +302,6 @@ export function useSidebarWorkspacesList(options?: {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const projects = useMemo(() => {
|
const projects = useMemo(() => {
|
||||||
console.log('[useSidebarWorkspacesList] build_projects', {
|
|
||||||
serverId,
|
|
||||||
hasSessionWorkspaces: Boolean(sessionWorkspaces),
|
|
||||||
workspaceCount: sessionWorkspaces?.size ?? 0,
|
|
||||||
projectOrderLength: persistedProjectOrder.length,
|
|
||||||
workspaceOrderScopeCount: Object.keys(persistedWorkspaceOrderByScope).length,
|
|
||||||
})
|
|
||||||
if (!sessionWorkspaces || sessionWorkspaces.size === 0 || !serverId) {
|
if (!sessionWorkspaces || sessionWorkspaces.size === 0 || !serverId) {
|
||||||
return EMPTY_PROJECTS
|
return EMPTY_PROJECTS
|
||||||
}
|
}
|
||||||
@@ -320,36 +313,6 @@ export function useSidebarWorkspacesList(options?: {
|
|||||||
})
|
})
|
||||||
}, [persistedProjectOrder, persistedWorkspaceOrderByScope, serverId, sessionWorkspaces])
|
}, [persistedProjectOrder, persistedWorkspaceOrderByScope, serverId, sessionWorkspaces])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log('[useSidebarWorkspacesList] inputs_changed', {
|
|
||||||
serverId,
|
|
||||||
connectionStatus,
|
|
||||||
hasHydratedWorkspaces,
|
|
||||||
workspaceCount: sessionWorkspaces?.size ?? 0,
|
|
||||||
projectOrderLength: persistedProjectOrder.length,
|
|
||||||
workspaceOrderScopeCount: Object.keys(persistedWorkspaceOrderByScope).length,
|
|
||||||
})
|
|
||||||
}, [
|
|
||||||
connectionStatus,
|
|
||||||
hasHydratedWorkspaces,
|
|
||||||
persistedProjectOrder,
|
|
||||||
persistedWorkspaceOrderByScope,
|
|
||||||
serverId,
|
|
||||||
sessionWorkspaces,
|
|
||||||
])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log('[useSidebarWorkspacesList] projects_changed', {
|
|
||||||
serverId,
|
|
||||||
projectCount: projects.length,
|
|
||||||
projectKeys: projects.map((project) => project.projectKey),
|
|
||||||
workspaceCounts: projects.map((project) => ({
|
|
||||||
projectKey: project.projectKey,
|
|
||||||
workspaceCount: project.workspaces.length,
|
|
||||||
})),
|
|
||||||
})
|
|
||||||
}, [projects, serverId])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!serverId || projects.length === 0) {
|
if (!serverId || projects.length === 0) {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -103,29 +103,6 @@ function toErrorMessage(error: unknown): string {
|
|||||||
return error instanceof Error ? error.message : String(error);
|
return error instanceof Error ? error.message : String(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toReasonCode(reason: string | null): string | null {
|
|
||||||
if (!reason) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const normalized = reason.toLowerCase();
|
|
||||||
if (normalized.includes("timed out")) {
|
|
||||||
return "connect_timeout";
|
|
||||||
}
|
|
||||||
if (normalized.includes("disposed")) {
|
|
||||||
return "disposed";
|
|
||||||
}
|
|
||||||
if (normalized.includes("client closed") || normalized.includes("client_closed")) {
|
|
||||||
return "client_closed";
|
|
||||||
}
|
|
||||||
if (normalized.includes("transport")) {
|
|
||||||
return "transport_error";
|
|
||||||
}
|
|
||||||
if (normalized.includes("failed to connect")) {
|
|
||||||
return "connect_failed";
|
|
||||||
}
|
|
||||||
return "unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
function hashForLog(value: string): string {
|
function hashForLog(value: string): string {
|
||||||
let hash = 0;
|
let hash = 0;
|
||||||
for (let index = 0; index < value.length; index += 1) {
|
for (let index = 0; index < value.length; index += 1) {
|
||||||
@@ -915,35 +892,12 @@ export class HostRuntimeController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private logConnectionTransition(input: {
|
private logConnectionTransition(_input: {
|
||||||
from: HostRuntimeConnectionMachineState["tag"];
|
from: HostRuntimeConnectionMachineState["tag"];
|
||||||
to: HostRuntimeConnectionMachineState["tag"];
|
to: HostRuntimeConnectionMachineState["tag"];
|
||||||
event: HostRuntimeConnectionMachineEvent;
|
event: HostRuntimeConnectionMachineEvent;
|
||||||
}): void {
|
}): void {
|
||||||
const { event } = input;
|
// Intentionally empty - logging removed.
|
||||||
const reason =
|
|
||||||
event.type === "connect_failed"
|
|
||||||
? event.message
|
|
||||||
: event.type === "client_state"
|
|
||||||
? event.state.status === "disconnected"
|
|
||||||
? event.state.reason ?? event.lastError ?? null
|
|
||||||
: null
|
|
||||||
: null;
|
|
||||||
const reasonCode =
|
|
||||||
event.type === "connect_failed"
|
|
||||||
? "connect_failed"
|
|
||||||
: toReasonCode(reason);
|
|
||||||
console.info("[HostRuntimeTransition]", {
|
|
||||||
serverId: this.host.serverId,
|
|
||||||
clientIdHash: this.clientIdHash,
|
|
||||||
from: input.from,
|
|
||||||
to: input.to,
|
|
||||||
event: event.type,
|
|
||||||
connectionPath: this.snapshot.activeConnection?.type ?? null,
|
|
||||||
generation: this.snapshot.clientGeneration,
|
|
||||||
reasonCode,
|
|
||||||
reason,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private trackConnectionFirstSeen(): void {
|
private trackConnectionFirstSeen(): void {
|
||||||
|
|||||||
@@ -60,15 +60,6 @@ import {
|
|||||||
} from "./agent-ready-screen-bottom-anchor";
|
} from "./agent-ready-screen-bottom-anchor";
|
||||||
|
|
||||||
const EMPTY_STREAM_ITEMS: StreamItem[] = [];
|
const EMPTY_STREAM_ITEMS: StreamItem[] = [];
|
||||||
const IS_DEV = Boolean((globalThis as { __DEV__?: boolean }).__DEV__);
|
|
||||||
|
|
||||||
function logAgentExplorer(event: string, details: Record<string, unknown>): void {
|
|
||||||
if (!IS_DEV) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log(`[AgentExplorer] ${event}`, details);
|
|
||||||
}
|
|
||||||
|
|
||||||
function logWebStickyBottom(
|
function logWebStickyBottom(
|
||||||
_event: string,
|
_event: string,
|
||||||
_details: Record<string, unknown>
|
_details: Record<string, unknown>
|
||||||
@@ -264,10 +255,6 @@ function AgentScreenContent({
|
|||||||
}, [resolveCachedCheckoutIsGit, resolvedAgentId, checkout?.isGit, serverId]);
|
}, [resolveCachedCheckoutIsGit, resolvedAgentId, checkout?.isGit, serverId]);
|
||||||
const openExplorerForActiveCheckout = useCallback(() => {
|
const openExplorerForActiveCheckout = useCallback(() => {
|
||||||
const checkoutContext = resolveCurrentExplorerCheckout();
|
const checkoutContext = resolveCurrentExplorerCheckout();
|
||||||
logAgentExplorer("openExplorerForActiveCheckout", {
|
|
||||||
hasCheckoutContext: Boolean(checkoutContext),
|
|
||||||
checkoutContext,
|
|
||||||
});
|
|
||||||
if (checkoutContext) {
|
if (checkoutContext) {
|
||||||
activateExplorerTabForCheckout(checkoutContext);
|
activateExplorerTabForCheckout(checkoutContext);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,23 +51,6 @@ export const DEFAULT_EXPLORER_FILES_SPLIT_RATIO = 0.38;
|
|||||||
export const MIN_EXPLORER_FILES_SPLIT_RATIO = 0.2;
|
export const MIN_EXPLORER_FILES_SPLIT_RATIO = 0.2;
|
||||||
export const MAX_EXPLORER_FILES_SPLIT_RATIO = 0.8;
|
export const MAX_EXPLORER_FILES_SPLIT_RATIO = 0.8;
|
||||||
|
|
||||||
const IS_DEV = Boolean((globalThis as { __DEV__?: boolean }).__DEV__);
|
|
||||||
|
|
||||||
function logPanelTransition(
|
|
||||||
action: string,
|
|
||||||
details: Record<string, unknown>
|
|
||||||
): void {
|
|
||||||
if (!IS_DEV) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const stack =
|
|
||||||
new Error().stack
|
|
||||||
?.split("\n")
|
|
||||||
.slice(2, 8)
|
|
||||||
.join("\n") ?? "stack unavailable";
|
|
||||||
console.log(`[PanelStore] ${action}`, details, stack);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PanelState {
|
interface PanelState {
|
||||||
// Mobile: which panel is currently shown
|
// Mobile: which panel is currently shown
|
||||||
mobileView: MobilePanelView;
|
mobileView: MobilePanelView;
|
||||||
@@ -151,103 +134,52 @@ export const usePanelStore = create<PanelState>()(
|
|||||||
explorerFilesSplitRatio: DEFAULT_EXPLORER_FILES_SPLIT_RATIO,
|
explorerFilesSplitRatio: DEFAULT_EXPLORER_FILES_SPLIT_RATIO,
|
||||||
|
|
||||||
openAgentList: () =>
|
openAgentList: () =>
|
||||||
set((state) => {
|
set((state) => ({
|
||||||
const nextState = {
|
mobileView: "agent-list" as const,
|
||||||
mobileView: "agent-list" as const,
|
desktop: { ...state.desktop, agentListOpen: true },
|
||||||
desktop: { ...state.desktop, agentListOpen: true },
|
})),
|
||||||
};
|
|
||||||
logPanelTransition("openAgentList", {
|
|
||||||
fromMobileView: state.mobileView,
|
|
||||||
toMobileView: nextState.mobileView,
|
|
||||||
fromDesktopAgentListOpen: state.desktop.agentListOpen,
|
|
||||||
toDesktopAgentListOpen: nextState.desktop.agentListOpen,
|
|
||||||
});
|
|
||||||
return nextState;
|
|
||||||
}),
|
|
||||||
|
|
||||||
openFileExplorer: () =>
|
openFileExplorer: () =>
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const resolvedTab = resolveExplorerTabFromActiveCheckout(state);
|
const resolvedTab = resolveExplorerTabFromActiveCheckout(state);
|
||||||
const nextMobileView: MobilePanelView = "file-explorer";
|
return {
|
||||||
const nextDesktop = { ...state.desktop, fileExplorerOpen: true };
|
mobileView: "file-explorer" as MobilePanelView,
|
||||||
const nextState = {
|
desktop: { ...state.desktop, fileExplorerOpen: true },
|
||||||
mobileView: nextMobileView,
|
|
||||||
desktop: nextDesktop,
|
|
||||||
...(resolvedTab ? { explorerTab: resolvedTab } : {}),
|
...(resolvedTab ? { explorerTab: resolvedTab } : {}),
|
||||||
};
|
};
|
||||||
logPanelTransition("openFileExplorer", {
|
|
||||||
fromMobileView: state.mobileView,
|
|
||||||
toMobileView: nextMobileView,
|
|
||||||
fromDesktopFileExplorerOpen: state.desktop.fileExplorerOpen,
|
|
||||||
toDesktopFileExplorerOpen: nextDesktop.fileExplorerOpen,
|
|
||||||
resolvedTab: resolvedTab ?? null,
|
|
||||||
activeCheckout: state.activeExplorerCheckout,
|
|
||||||
});
|
|
||||||
return nextState;
|
|
||||||
}),
|
}),
|
||||||
closeFileExplorer: () =>
|
closeFileExplorer: () =>
|
||||||
set((state) => {
|
set((state) => ({
|
||||||
const nextState = {
|
mobileView:
|
||||||
mobileView:
|
state.mobileView === "file-explorer" ? ("agent" as const) : state.mobileView,
|
||||||
state.mobileView === "file-explorer" ? ("agent" as const) : state.mobileView,
|
desktop: {
|
||||||
desktop: {
|
...state.desktop,
|
||||||
...state.desktop,
|
fileExplorerOpen: false,
|
||||||
fileExplorerOpen: false,
|
},
|
||||||
},
|
})),
|
||||||
};
|
|
||||||
logPanelTransition("closeFileExplorer", {
|
|
||||||
fromMobileView: state.mobileView,
|
|
||||||
toMobileView: nextState.mobileView,
|
|
||||||
fromDesktopFileExplorerOpen: state.desktop.fileExplorerOpen,
|
|
||||||
toDesktopFileExplorerOpen: nextState.desktop.fileExplorerOpen,
|
|
||||||
});
|
|
||||||
return nextState;
|
|
||||||
}),
|
|
||||||
|
|
||||||
closeToAgent: () =>
|
closeToAgent: () =>
|
||||||
set((state) => {
|
set((state) => ({
|
||||||
const nextState = {
|
mobileView: "agent" as const,
|
||||||
mobileView: "agent" as const,
|
// On desktop, closing depends on which panel triggered it
|
||||||
// On desktop, closing depends on which panel triggered it
|
// This is called when closing via gesture/backdrop, so we close the currently active mobile panel
|
||||||
// This is called when closing via gesture/backdrop, so we close the currently active mobile panel
|
desktop: {
|
||||||
desktop: {
|
agentListOpen:
|
||||||
agentListOpen:
|
state.mobileView === "agent-list" ? false : state.desktop.agentListOpen,
|
||||||
state.mobileView === "agent-list" ? false : state.desktop.agentListOpen,
|
fileExplorerOpen:
|
||||||
fileExplorerOpen:
|
state.mobileView === "file-explorer" ? false : state.desktop.fileExplorerOpen,
|
||||||
state.mobileView === "file-explorer" ? false : state.desktop.fileExplorerOpen,
|
},
|
||||||
},
|
})),
|
||||||
};
|
|
||||||
logPanelTransition("closeToAgent", {
|
|
||||||
fromMobileView: state.mobileView,
|
|
||||||
toMobileView: nextState.mobileView,
|
|
||||||
fromDesktopAgentListOpen: state.desktop.agentListOpen,
|
|
||||||
toDesktopAgentListOpen: nextState.desktop.agentListOpen,
|
|
||||||
fromDesktopFileExplorerOpen: state.desktop.fileExplorerOpen,
|
|
||||||
toDesktopFileExplorerOpen: nextState.desktop.fileExplorerOpen,
|
|
||||||
});
|
|
||||||
return nextState;
|
|
||||||
}),
|
|
||||||
|
|
||||||
toggleAgentList: () =>
|
toggleAgentList: () =>
|
||||||
set((state) => {
|
set((state) => ({
|
||||||
// Mobile: toggle between agent and agent-list
|
// Mobile: toggle between agent and agent-list
|
||||||
const newMobileView: MobilePanelView =
|
mobileView: (state.mobileView === "agent-list" ? "agent" : "agent-list") as MobilePanelView,
|
||||||
state.mobileView === "agent-list" ? "agent" : "agent-list";
|
desktop: {
|
||||||
const nextState = {
|
...state.desktop,
|
||||||
mobileView: newMobileView,
|
agentListOpen: !state.desktop.agentListOpen,
|
||||||
desktop: {
|
},
|
||||||
...state.desktop,
|
})),
|
||||||
agentListOpen: !state.desktop.agentListOpen,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
logPanelTransition("toggleAgentList", {
|
|
||||||
fromMobileView: state.mobileView,
|
|
||||||
toMobileView: nextState.mobileView,
|
|
||||||
fromDesktopAgentListOpen: state.desktop.agentListOpen,
|
|
||||||
toDesktopAgentListOpen: nextState.desktop.agentListOpen,
|
|
||||||
});
|
|
||||||
return nextState;
|
|
||||||
}),
|
|
||||||
|
|
||||||
toggleFileExplorer: () =>
|
toggleFileExplorer: () =>
|
||||||
set((state) => {
|
set((state) => {
|
||||||
@@ -266,23 +198,12 @@ export const usePanelStore = create<PanelState>()(
|
|||||||
mobileView: nextMobileView,
|
mobileView: nextMobileView,
|
||||||
desktop: nextDesktop,
|
desktop: nextDesktop,
|
||||||
};
|
};
|
||||||
let resolvedTab: ExplorerTab | null = null;
|
|
||||||
if (willOpenMobile || willOpenDesktop) {
|
if (willOpenMobile || willOpenDesktop) {
|
||||||
resolvedTab = resolveExplorerTabFromActiveCheckout(state);
|
const resolvedTab = resolveExplorerTabFromActiveCheckout(state);
|
||||||
if (resolvedTab) {
|
if (resolvedTab) {
|
||||||
nextState.explorerTab = resolvedTab;
|
nextState.explorerTab = resolvedTab;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logPanelTransition("toggleFileExplorer", {
|
|
||||||
fromMobileView: state.mobileView,
|
|
||||||
toMobileView: nextMobileView,
|
|
||||||
fromDesktopFileExplorerOpen: state.desktop.fileExplorerOpen,
|
|
||||||
toDesktopFileExplorerOpen: nextDesktop.fileExplorerOpen,
|
|
||||||
willOpenMobile,
|
|
||||||
willOpenDesktop,
|
|
||||||
resolvedTab: resolvedTab ?? null,
|
|
||||||
activeCheckout: state.activeExplorerCheckout,
|
|
||||||
});
|
|
||||||
return nextState;
|
return nextState;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,6 @@ import { StyleSheet } from "react-native-unistyles";
|
|||||||
// import { UnistylesRuntime } from "react-native-unistyles";
|
// import { UnistylesRuntime } from "react-native-unistyles";
|
||||||
import { lightTheme, darkTheme } from "./theme";
|
import { lightTheme, darkTheme } from "./theme";
|
||||||
|
|
||||||
console.log("[Unistyles] Configuring...");
|
|
||||||
|
|
||||||
// // Configure Unistyles with adaptive themes
|
|
||||||
StyleSheet.configure({
|
StyleSheet.configure({
|
||||||
themes: {
|
themes: {
|
||||||
light: lightTheme,
|
light: lightTheme,
|
||||||
@@ -22,8 +19,6 @@ StyleSheet.configure({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("[Unistyles] Configuration complete!");
|
|
||||||
|
|
||||||
// Type augmentation for TypeScript
|
// Type augmentation for TypeScript
|
||||||
type AppThemes = {
|
type AppThemes = {
|
||||||
light: typeof lightTheme;
|
light: typeof lightTheme;
|
||||||
@@ -43,6 +38,4 @@ declare module "react-native-unistyles" {
|
|||||||
export interface UnistylesBreakpoints extends AppBreakpoints {}
|
export interface UnistylesBreakpoints extends AppBreakpoints {}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(lightTheme.colors.background);
|
|
||||||
|
|
||||||
// UnistylesRuntime.setRootViewBackgroundColor(lightTheme.colors.background);
|
// UnistylesRuntime.setRootViewBackgroundColor(lightTheme.colors.background);
|
||||||
|
|||||||
@@ -86,7 +86,6 @@ async function ensureTauriNotificationPermission(
|
|||||||
try {
|
try {
|
||||||
const granted = await notificationModule.isPermissionGranted();
|
const granted = await notificationModule.isPermissionGranted();
|
||||||
if (granted) {
|
if (granted) {
|
||||||
console.log("[OSNotifications][Tauri] Permission already granted");
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -106,7 +105,6 @@ async function ensureTauriNotificationPermission(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await notificationModule.requestPermission();
|
const result = await notificationModule.requestPermission();
|
||||||
console.log("[OSNotifications][Tauri] requestPermission result:", result);
|
|
||||||
return result === "granted";
|
return result === "granted";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(
|
console.warn(
|
||||||
@@ -210,12 +208,7 @@ export async function sendOsNotification(
|
|||||||
const NotificationConstructor = getWebNotificationConstructor();
|
const NotificationConstructor = getWebNotificationConstructor();
|
||||||
if (NotificationConstructor) {
|
if (NotificationConstructor) {
|
||||||
const granted = await ensureNotificationPermission();
|
const granted = await ensureNotificationPermission();
|
||||||
if (!granted) {
|
if (granted) {
|
||||||
console.log(
|
|
||||||
"[OSNotifications][Web] Permission not granted:",
|
|
||||||
NotificationConstructor.permission
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const notification = new NotificationConstructor(payload.title, {
|
const notification = new NotificationConstructor(payload.title, {
|
||||||
body: payload.body,
|
body: payload.body,
|
||||||
data: payload.data,
|
data: payload.data,
|
||||||
|
|||||||
@@ -34,34 +34,10 @@ interface CuePcm {
|
|||||||
durationMs: number;
|
durationMs: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StreamDebugState {
|
|
||||||
count: number;
|
|
||||||
totalValue: number;
|
|
||||||
maxValue: number;
|
|
||||||
startedAtMs: number;
|
|
||||||
lastAtMs: number;
|
|
||||||
maxGapMs: number;
|
|
||||||
nextLogAtMs: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AudioEngineTraceOptions {
|
interface AudioEngineTraceOptions {
|
||||||
traceLabel?: string;
|
traceLabel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let nextAudioEngineInstanceId = 1;
|
|
||||||
|
|
||||||
function getTraceStack(): string | undefined {
|
|
||||||
const stack = new Error().stack;
|
|
||||||
if (!stack) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return stack
|
|
||||||
.split("\n")
|
|
||||||
.slice(2, 7)
|
|
||||||
.map((line) => line.trim())
|
|
||||||
.join(" | ");
|
|
||||||
}
|
|
||||||
|
|
||||||
function resamplePcm16(
|
function resamplePcm16(
|
||||||
pcm: Uint8Array,
|
pcm: Uint8Array,
|
||||||
fromRate: number,
|
fromRate: number,
|
||||||
@@ -118,33 +94,10 @@ function parsePcmSampleRate(mimeType: string): number | null {
|
|||||||
return Number.isFinite(rate) && rate > 0 ? rate : null;
|
return Number.isFinite(rate) && rate > 0 ? rate : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function logVoiceNative(event: string, details?: Record<string, unknown>): void {
|
|
||||||
if (details) {
|
|
||||||
console.log(`[VoiceNative] ${event}`, details);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log(`[VoiceNative] ${event}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createStreamDebugState(nowMs: number): StreamDebugState {
|
|
||||||
return {
|
|
||||||
count: 0,
|
|
||||||
totalValue: 0,
|
|
||||||
maxValue: 0,
|
|
||||||
startedAtMs: nowMs,
|
|
||||||
lastAtMs: nowMs,
|
|
||||||
maxGapMs: 0,
|
|
||||||
nextLogAtMs: nowMs + 1000,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createAudioEngine(
|
export function createAudioEngine(
|
||||||
callbacks: AudioEngineCallbacks,
|
callbacks: AudioEngineCallbacks,
|
||||||
options?: AudioEngineTraceOptions
|
_options?: AudioEngineTraceOptions
|
||||||
): AudioEngine {
|
): AudioEngine {
|
||||||
const engineId = nextAudioEngineInstanceId++;
|
|
||||||
const traceLabel = options?.traceLabel ?? "unknown";
|
|
||||||
const traceStack = getTraceStack();
|
|
||||||
const refs: {
|
const refs: {
|
||||||
initialized: boolean;
|
initialized: boolean;
|
||||||
captureActive: boolean;
|
captureActive: boolean;
|
||||||
@@ -163,8 +116,6 @@ export function createAudioEngine(
|
|||||||
timeout: ReturnType<typeof setTimeout> | null;
|
timeout: ReturnType<typeof setTimeout> | null;
|
||||||
};
|
};
|
||||||
thinkingTone: CuePcm | null;
|
thinkingTone: CuePcm | null;
|
||||||
captureDebug: StreamDebugState | null;
|
|
||||||
volumeDebug: StreamDebugState | null;
|
|
||||||
destroyed: boolean;
|
destroyed: boolean;
|
||||||
} = {
|
} = {
|
||||||
initialized: false,
|
initialized: false,
|
||||||
@@ -180,135 +131,36 @@ export function createAudioEngine(
|
|||||||
timeout: null,
|
timeout: null,
|
||||||
},
|
},
|
||||||
thinkingTone: null,
|
thinkingTone: null,
|
||||||
captureDebug: null,
|
|
||||||
volumeDebug: null,
|
|
||||||
destroyed: false,
|
destroyed: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
logVoiceNative("engine_created", {
|
|
||||||
engineId,
|
|
||||||
traceLabel,
|
|
||||||
stack: traceStack,
|
|
||||||
});
|
|
||||||
|
|
||||||
function resetCaptureDebug(): void {
|
|
||||||
refs.captureDebug = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetVolumeDebug(): void {
|
|
||||||
refs.volumeDebug = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function recordCaptureChunk(byteLength: number): void {
|
|
||||||
const nowMs = Date.now();
|
|
||||||
const debug = refs.captureDebug ?? createStreamDebugState(nowMs);
|
|
||||||
const gapMs = debug.count === 0 ? 0 : nowMs - debug.lastAtMs;
|
|
||||||
debug.count += 1;
|
|
||||||
debug.totalValue += byteLength;
|
|
||||||
debug.maxValue = Math.max(debug.maxValue, byteLength);
|
|
||||||
debug.maxGapMs = Math.max(debug.maxGapMs, gapMs);
|
|
||||||
debug.lastAtMs = nowMs;
|
|
||||||
|
|
||||||
if (nowMs >= debug.nextLogAtMs) {
|
|
||||||
const elapsedMs = Math.max(1, nowMs - debug.startedAtMs);
|
|
||||||
logVoiceNative("capture_summary", {
|
|
||||||
chunks: debug.count,
|
|
||||||
totalBytes: debug.totalValue,
|
|
||||||
averageChunkBytes: Math.round(debug.totalValue / debug.count),
|
|
||||||
maxChunkBytes: debug.maxValue,
|
|
||||||
chunksPerSecond: Number(((debug.count * 1000) / elapsedMs).toFixed(1)),
|
|
||||||
maxGapMs: debug.maxGapMs,
|
|
||||||
muted: refs.muted,
|
|
||||||
});
|
|
||||||
refs.captureDebug = createStreamDebugState(nowMs);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
refs.captureDebug = debug;
|
|
||||||
}
|
|
||||||
|
|
||||||
function recordVolumeLevel(level: number): void {
|
|
||||||
const nowMs = Date.now();
|
|
||||||
const debug = refs.volumeDebug ?? createStreamDebugState(nowMs);
|
|
||||||
const gapMs = debug.count === 0 ? 0 : nowMs - debug.lastAtMs;
|
|
||||||
debug.count += 1;
|
|
||||||
debug.totalValue += level;
|
|
||||||
debug.maxValue = Math.max(debug.maxValue, level);
|
|
||||||
debug.maxGapMs = Math.max(debug.maxGapMs, gapMs);
|
|
||||||
debug.lastAtMs = nowMs;
|
|
||||||
|
|
||||||
if (nowMs >= debug.nextLogAtMs) {
|
|
||||||
const elapsedMs = Math.max(1, nowMs - debug.startedAtMs);
|
|
||||||
logVoiceNative("volume_summary", {
|
|
||||||
samples: debug.count,
|
|
||||||
averageLevel: Number((debug.totalValue / debug.count).toFixed(3)),
|
|
||||||
peakLevel: Number(debug.maxValue.toFixed(3)),
|
|
||||||
samplesPerSecond: Number(((debug.count * 1000) / elapsedMs).toFixed(1)),
|
|
||||||
maxGapMs: debug.maxGapMs,
|
|
||||||
muted: refs.muted,
|
|
||||||
});
|
|
||||||
refs.volumeDebug = createStreamDebugState(nowMs);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
refs.volumeDebug = debug;
|
|
||||||
}
|
|
||||||
|
|
||||||
const microphoneSubscription = addExpoTwoWayAudioEventListener(
|
const microphoneSubscription = addExpoTwoWayAudioEventListener(
|
||||||
"onMicrophoneData",
|
"onMicrophoneData",
|
||||||
(event) => {
|
(event) => {
|
||||||
console.log("[VoiceNative] onMicrophoneData", {
|
|
||||||
engineId,
|
|
||||||
traceLabel,
|
|
||||||
captureActive: refs.captureActive,
|
|
||||||
muted: refs.muted,
|
|
||||||
bytes: event.data.byteLength,
|
|
||||||
});
|
|
||||||
if (!refs.captureActive || refs.muted) {
|
if (!refs.captureActive || refs.muted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
recordCaptureChunk(event.data.byteLength);
|
|
||||||
callbacks.onCaptureData(event.data);
|
callbacks.onCaptureData(event.data);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
logVoiceNative("listener_added", {
|
|
||||||
engineId,
|
|
||||||
traceLabel,
|
|
||||||
event: "onMicrophoneData",
|
|
||||||
});
|
|
||||||
|
|
||||||
const volumeSubscription = addExpoTwoWayAudioEventListener(
|
const volumeSubscription = addExpoTwoWayAudioEventListener(
|
||||||
"onInputVolumeLevelData",
|
"onInputVolumeLevelData",
|
||||||
(event) => {
|
(event) => {
|
||||||
console.log("[VoiceNative] onInputVolumeLevelData", {
|
|
||||||
engineId,
|
|
||||||
traceLabel,
|
|
||||||
captureActive: refs.captureActive,
|
|
||||||
muted: refs.muted,
|
|
||||||
level: event.data,
|
|
||||||
});
|
|
||||||
if (!refs.captureActive) {
|
if (!refs.captureActive) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const level = refs.muted ? 0 : event.data;
|
const level = refs.muted ? 0 : event.data;
|
||||||
recordVolumeLevel(level);
|
|
||||||
callbacks.onVolumeLevel(level);
|
callbacks.onVolumeLevel(level);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
logVoiceNative("listener_added", {
|
|
||||||
engineId,
|
|
||||||
traceLabel,
|
|
||||||
event: "onInputVolumeLevelData",
|
|
||||||
});
|
|
||||||
|
|
||||||
async function ensureInitialized(): Promise<void> {
|
async function ensureInitialized(): Promise<void> {
|
||||||
if (refs.initialized) {
|
if (refs.initialized) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
logVoiceNative("initialize_start");
|
|
||||||
await initialize();
|
await initialize();
|
||||||
refs.initialized = true;
|
refs.initialized = true;
|
||||||
logVoiceNative("initialize_complete");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureMicrophonePermission(): Promise<void> {
|
async function ensureMicrophonePermission(): Promise<void> {
|
||||||
@@ -330,10 +182,6 @@ export function createAudioEngine(
|
|||||||
const pcm16k = Buffer.from(THINKING_TONE_NATIVE_PCM_BASE64, "base64");
|
const pcm16k = Buffer.from(THINKING_TONE_NATIVE_PCM_BASE64, "base64");
|
||||||
const durationMs = THINKING_TONE_NATIVE_PCM_DURATION_MS;
|
const durationMs = THINKING_TONE_NATIVE_PCM_DURATION_MS;
|
||||||
refs.thinkingTone = { pcm16k, durationMs };
|
refs.thinkingTone = { pcm16k, durationMs };
|
||||||
logVoiceNative("thinking_tone_loaded", {
|
|
||||||
bytes: pcm16k.byteLength,
|
|
||||||
durationMs,
|
|
||||||
});
|
|
||||||
return refs.thinkingTone;
|
return refs.thinkingTone;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,13 +205,6 @@ export function createAudioEngine(
|
|||||||
const inputRate = parsePcmSampleRate(audio.type || "") ?? 24000;
|
const inputRate = parsePcmSampleRate(audio.type || "") ?? 24000;
|
||||||
const pcm16k = resamplePcm16(pcm, inputRate, 16000);
|
const pcm16k = resamplePcm16(pcm, inputRate, 16000);
|
||||||
const durationSec = pcm16k.length / 2 / 16000;
|
const durationSec = pcm16k.length / 2 / 16000;
|
||||||
logVoiceNative("playback_start", {
|
|
||||||
inputBytes: pcm.byteLength,
|
|
||||||
outputBytes: pcm16k.byteLength,
|
|
||||||
inputRate,
|
|
||||||
durationMs: Math.round(durationSec * 1000),
|
|
||||||
queueLength: refs.queue.length,
|
|
||||||
});
|
|
||||||
|
|
||||||
playPCMData(pcm16k);
|
playPCMData(pcm16k);
|
||||||
clearPlaybackTimeout();
|
clearPlaybackTimeout();
|
||||||
@@ -375,9 +216,6 @@ export function createAudioEngine(
|
|||||||
}
|
}
|
||||||
active.settled = true;
|
active.settled = true;
|
||||||
refs.activePlayback = null;
|
refs.activePlayback = null;
|
||||||
logVoiceNative("playback_complete", {
|
|
||||||
durationMs: Math.round(durationSec * 1000),
|
|
||||||
});
|
|
||||||
resolve(durationSec);
|
resolve(durationSec);
|
||||||
}, durationSec * 1000);
|
}, durationSec * 1000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -386,9 +224,6 @@ export function createAudioEngine(
|
|||||||
if (active && !active.settled) {
|
if (active && !active.settled) {
|
||||||
active.settled = true;
|
active.settled = true;
|
||||||
refs.activePlayback = null;
|
refs.activePlayback = null;
|
||||||
logVoiceNative("playback_failed", {
|
|
||||||
error: error instanceof Error ? error.message : String(error),
|
|
||||||
});
|
|
||||||
reject(error instanceof Error ? error : new Error(String(error)));
|
reject(error instanceof Error ? error : new Error(String(error)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -401,9 +236,6 @@ export function createAudioEngine(
|
|||||||
}
|
}
|
||||||
|
|
||||||
refs.processingQueue = true;
|
refs.processingQueue = true;
|
||||||
logVoiceNative("queue_drain_start", {
|
|
||||||
queueLength: refs.queue.length,
|
|
||||||
});
|
|
||||||
while (refs.queue.length > 0) {
|
while (refs.queue.length > 0) {
|
||||||
const item = refs.queue.shift()!;
|
const item = refs.queue.shift()!;
|
||||||
try {
|
try {
|
||||||
@@ -414,13 +246,9 @@ export function createAudioEngine(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
refs.processingQueue = false;
|
refs.processingQueue = false;
|
||||||
logVoiceNative("queue_drain_complete");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopLooping(): void {
|
function stopLooping(): void {
|
||||||
if (refs.looping.active) {
|
|
||||||
logVoiceNative("thinking_tone_stop");
|
|
||||||
}
|
|
||||||
refs.looping.active = false;
|
refs.looping.active = false;
|
||||||
refs.looping.token += 1;
|
refs.looping.token += 1;
|
||||||
if (refs.looping.timeout) {
|
if (refs.looping.timeout) {
|
||||||
@@ -438,17 +266,9 @@ export function createAudioEngine(
|
|||||||
|
|
||||||
async destroy() {
|
async destroy() {
|
||||||
if (refs.destroyed) {
|
if (refs.destroyed) {
|
||||||
logVoiceNative("engine_destroy_skipped", {
|
|
||||||
engineId,
|
|
||||||
traceLabel,
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
refs.destroyed = true;
|
refs.destroyed = true;
|
||||||
logVoiceNative("engine_destroy_start", {
|
|
||||||
engineId,
|
|
||||||
traceLabel,
|
|
||||||
});
|
|
||||||
stopLooping();
|
stopLooping();
|
||||||
this.stop();
|
this.stop();
|
||||||
this.clearQueue();
|
this.clearQueue();
|
||||||
@@ -458,29 +278,13 @@ export function createAudioEngine(
|
|||||||
}
|
}
|
||||||
clearPlaybackTimeout();
|
clearPlaybackTimeout();
|
||||||
refs.muted = false;
|
refs.muted = false;
|
||||||
resetCaptureDebug();
|
|
||||||
resetVolumeDebug();
|
|
||||||
callbacks.onVolumeLevel(0);
|
callbacks.onVolumeLevel(0);
|
||||||
if (refs.initialized) {
|
if (refs.initialized) {
|
||||||
tearDown();
|
tearDown();
|
||||||
refs.initialized = false;
|
refs.initialized = false;
|
||||||
}
|
}
|
||||||
microphoneSubscription.remove();
|
microphoneSubscription.remove();
|
||||||
logVoiceNative("listener_removed", {
|
|
||||||
engineId,
|
|
||||||
traceLabel,
|
|
||||||
event: "onMicrophoneData",
|
|
||||||
});
|
|
||||||
volumeSubscription.remove();
|
volumeSubscription.remove();
|
||||||
logVoiceNative("listener_removed", {
|
|
||||||
engineId,
|
|
||||||
traceLabel,
|
|
||||||
event: "onInputVolumeLevelData",
|
|
||||||
});
|
|
||||||
logVoiceNative("engine_destroy_complete", {
|
|
||||||
engineId,
|
|
||||||
traceLabel,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async startCapture() {
|
async startCapture() {
|
||||||
@@ -489,23 +293,12 @@ export function createAudioEngine(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logVoiceNative("capture_start_requested");
|
|
||||||
logVoiceNative("capture_trace", {
|
|
||||||
engineId,
|
|
||||||
traceLabel,
|
|
||||||
});
|
|
||||||
await ensureMicrophonePermission();
|
await ensureMicrophonePermission();
|
||||||
await ensureInitialized();
|
await ensureInitialized();
|
||||||
toggleRecording(true);
|
toggleRecording(true);
|
||||||
refs.captureActive = true;
|
refs.captureActive = true;
|
||||||
resetCaptureDebug();
|
|
||||||
resetVolumeDebug();
|
|
||||||
logVoiceNative("capture_started");
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const wrapped = error instanceof Error ? error : new Error(String(error));
|
const wrapped = error instanceof Error ? error : new Error(String(error));
|
||||||
logVoiceNative("capture_start_failed", {
|
|
||||||
error: wrapped.message,
|
|
||||||
});
|
|
||||||
callbacks.onError?.(wrapped);
|
callbacks.onError?.(wrapped);
|
||||||
throw wrapped;
|
throw wrapped;
|
||||||
}
|
}
|
||||||
@@ -517,21 +310,11 @@ export function createAudioEngine(
|
|||||||
}
|
}
|
||||||
refs.captureActive = false;
|
refs.captureActive = false;
|
||||||
refs.muted = false;
|
refs.muted = false;
|
||||||
resetCaptureDebug();
|
|
||||||
resetVolumeDebug();
|
|
||||||
logVoiceNative("capture_stopped");
|
|
||||||
logVoiceNative("capture_trace", {
|
|
||||||
engineId,
|
|
||||||
traceLabel,
|
|
||||||
});
|
|
||||||
callbacks.onVolumeLevel(0);
|
callbacks.onVolumeLevel(0);
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleMute() {
|
toggleMute() {
|
||||||
refs.muted = !refs.muted;
|
refs.muted = !refs.muted;
|
||||||
logVoiceNative("mute_toggled", {
|
|
||||||
muted: refs.muted,
|
|
||||||
});
|
|
||||||
if (refs.muted) {
|
if (refs.muted) {
|
||||||
callbacks.onVolumeLevel(0);
|
callbacks.onVolumeLevel(0);
|
||||||
}
|
}
|
||||||
@@ -545,11 +328,6 @@ export function createAudioEngine(
|
|||||||
async play(audio: AudioPlaybackSource) {
|
async play(audio: AudioPlaybackSource) {
|
||||||
return await new Promise<number>((resolve, reject) => {
|
return await new Promise<number>((resolve, reject) => {
|
||||||
refs.queue.push({ audio, resolve, reject });
|
refs.queue.push({ audio, resolve, reject });
|
||||||
logVoiceNative("queue_enqueue", {
|
|
||||||
queueLength: refs.queue.length,
|
|
||||||
blobBytes: audio.size,
|
|
||||||
mimeType: audio.type || null,
|
|
||||||
});
|
|
||||||
if (!refs.processingQueue) {
|
if (!refs.processingQueue) {
|
||||||
void processQueue();
|
void processQueue();
|
||||||
}
|
}
|
||||||
@@ -557,9 +335,6 @@ export function createAudioEngine(
|
|||||||
},
|
},
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
if (refs.activePlayback) {
|
|
||||||
logVoiceNative("playback_stopped");
|
|
||||||
}
|
|
||||||
stopPlayback();
|
stopPlayback();
|
||||||
clearPlaybackTimeout();
|
clearPlaybackTimeout();
|
||||||
const active = refs.activePlayback;
|
const active = refs.activePlayback;
|
||||||
@@ -571,11 +346,6 @@ export function createAudioEngine(
|
|||||||
},
|
},
|
||||||
|
|
||||||
clearQueue() {
|
clearQueue() {
|
||||||
if (refs.queue.length > 0) {
|
|
||||||
logVoiceNative("queue_cleared", {
|
|
||||||
queueLength: refs.queue.length,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
while (refs.queue.length > 0) {
|
while (refs.queue.length > 0) {
|
||||||
refs.queue.shift()!.reject(new Error("Playback stopped"));
|
refs.queue.shift()!.reject(new Error("Playback stopped"));
|
||||||
}
|
}
|
||||||
@@ -605,11 +375,6 @@ export function createAudioEngine(
|
|||||||
durationMs: (audio.byteLength / 2 / 16000) * 1000,
|
durationMs: (audio.byteLength / 2 / 16000) * 1000,
|
||||||
}
|
}
|
||||||
: await ensureThinkingTone();
|
: await ensureThinkingTone();
|
||||||
logVoiceNative("thinking_tone_start", {
|
|
||||||
bytes: cue.pcm16k.byteLength,
|
|
||||||
durationMs: Math.round(cue.durationMs),
|
|
||||||
gapMs: gapMs || THINKING_TONE_REPEAT_GAP_MS,
|
|
||||||
});
|
|
||||||
|
|
||||||
const loop = () => {
|
const loop = () => {
|
||||||
if (!refs.looping.active || refs.looping.token !== token) {
|
if (!refs.looping.active || refs.looping.token !== token) {
|
||||||
@@ -617,9 +382,6 @@ export function createAudioEngine(
|
|||||||
}
|
}
|
||||||
resumePlayback();
|
resumePlayback();
|
||||||
playPCMData(cue.pcm16k);
|
playPCMData(cue.pcm16k);
|
||||||
logVoiceNative("thinking_tone_tick", {
|
|
||||||
durationMs: Math.round(cue.durationMs),
|
|
||||||
});
|
|
||||||
refs.looping.timeout = setTimeout(
|
refs.looping.timeout = setTimeout(
|
||||||
loop,
|
loop,
|
||||||
cue.durationMs + (gapMs || THINKING_TONE_REPEAT_GAP_MS)
|
cue.durationMs + (gapMs || THINKING_TONE_REPEAT_GAP_MS)
|
||||||
|
|||||||
@@ -139,11 +139,6 @@ export class SpeechSegmenter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
flush(isLast: boolean): void {
|
flush(isLast: boolean): void {
|
||||||
console.log("[SpeechSegmenter] flush", {
|
|
||||||
isLast,
|
|
||||||
audioBufferLength: this.audioBuffer.length,
|
|
||||||
bufferedBytes: this.bufferedBytes,
|
|
||||||
});
|
|
||||||
if (this.audioBuffer.length === 0) {
|
if (this.audioBuffer.length === 0) {
|
||||||
this.bufferedBytes = 0;
|
this.bufferedBytes = 0;
|
||||||
return;
|
return;
|
||||||
@@ -162,13 +157,6 @@ export class SpeechSegmenter {
|
|||||||
* In non-continuous mode, chunk buffering is gated by the VAD state.
|
* In non-continuous mode, chunk buffering is gated by the VAD state.
|
||||||
*/
|
*/
|
||||||
pushPcmChunk(chunk: Uint8Array): void {
|
pushPcmChunk(chunk: Uint8Array): void {
|
||||||
console.log("[SpeechSegmenter] pushPcmChunk", {
|
|
||||||
bytes: chunk.length,
|
|
||||||
speechDetectionStartMs: this.speechDetectionStartMs,
|
|
||||||
isSpeaking: this.isSpeaking,
|
|
||||||
speechConfirmed: this.speechConfirmed,
|
|
||||||
bufferedBytes: this.bufferedBytes,
|
|
||||||
});
|
|
||||||
if (chunk.length === 0) {
|
if (chunk.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -198,16 +186,6 @@ export class SpeechSegmenter {
|
|||||||
* Called with a normalized volume level (0..1). Drives VAD transitions.
|
* Called with a normalized volume level (0..1). Drives VAD transitions.
|
||||||
*/
|
*/
|
||||||
pushVolumeLevel(volume: number, nowMs: number): void {
|
pushVolumeLevel(volume: number, nowMs: number): void {
|
||||||
console.log("[SpeechSegmenter] pushVolumeLevel", {
|
|
||||||
volume,
|
|
||||||
nowMs,
|
|
||||||
speechDetectionStartMs: this.speechDetectionStartMs,
|
|
||||||
isSpeaking: this.isSpeaking,
|
|
||||||
isDetecting: this.isDetecting,
|
|
||||||
speechConfirmed: this.speechConfirmed,
|
|
||||||
silenceStartMs: this.silenceStartMs,
|
|
||||||
confirmedDropStartMs: this.confirmedDropStartMs,
|
|
||||||
});
|
|
||||||
if (this.config.enableContinuousStreaming) {
|
if (this.config.enableContinuousStreaming) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,17 +96,6 @@ const INITIAL_TELEMETRY: VoiceRuntimeTelemetrySnapshot = {
|
|||||||
segmentDuration: 0,
|
segmentDuration: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
function logVoiceRuntime(
|
|
||||||
event: string,
|
|
||||||
details?: Record<string, unknown>
|
|
||||||
): void {
|
|
||||||
if (details) {
|
|
||||||
console.log(`[VoiceRuntime] ${event}`, details);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log(`[VoiceRuntime] ${event}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function snapshotsEqual(
|
function snapshotsEqual(
|
||||||
left: VoiceRuntimeSnapshot,
|
left: VoiceRuntimeSnapshot,
|
||||||
right: VoiceRuntimeSnapshot
|
right: VoiceRuntimeSnapshot
|
||||||
@@ -196,15 +185,6 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
|||||||
}
|
}
|
||||||
const previous = state.snapshot;
|
const previous = state.snapshot;
|
||||||
state.snapshot = next;
|
state.snapshot = next;
|
||||||
logVoiceRuntime("snapshot_changed", {
|
|
||||||
previousPhase: previous.phase,
|
|
||||||
phase: next.phase,
|
|
||||||
isVoiceMode: next.isVoiceMode,
|
|
||||||
isVoiceSwitching: next.isVoiceSwitching,
|
|
||||||
isMuted: next.isMuted,
|
|
||||||
activeServerId: next.activeServerId,
|
|
||||||
activeAgentId: next.activeAgentId,
|
|
||||||
});
|
|
||||||
emit();
|
emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,11 +274,6 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
|||||||
stopCue();
|
stopCue();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
logVoiceRuntime("thinking_tone_requested", {
|
|
||||||
phase: state.snapshot.phase,
|
|
||||||
isDetecting: state.telemetry.isDetecting,
|
|
||||||
isSpeaking: state.telemetry.isSpeaking,
|
|
||||||
});
|
|
||||||
deps.engine.playLooping(new Uint8Array(0), THINKING_TONE_REPEAT_GAP_MS);
|
deps.engine.playLooping(new Uint8Array(0), THINKING_TONE_REPEAT_GAP_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,12 +321,6 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
|||||||
if (isLast) {
|
if (isLast) {
|
||||||
state.turnInProgress = true;
|
state.turnInProgress = true;
|
||||||
}
|
}
|
||||||
logVoiceRuntime("audio_segment_ready", {
|
|
||||||
isLast,
|
|
||||||
base64Bytes: audioData.length,
|
|
||||||
phase: state.snapshot.phase,
|
|
||||||
serverId: activeSession.adapter.serverId,
|
|
||||||
});
|
|
||||||
|
|
||||||
void activeSession.adapter
|
void activeSession.adapter
|
||||||
.sendVoiceAudioChunk(audioData, PCM_MIME_TYPE, isLast)
|
.sendVoiceAudioChunk(audioData, PCM_MIME_TYPE, isLast)
|
||||||
@@ -365,10 +334,6 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
logVoiceRuntime("audio_segment_sent", {
|
|
||||||
isLast,
|
|
||||||
serverId: activeSession.adapter.serverId,
|
|
||||||
});
|
|
||||||
patchSnapshot((prev) => ({ ...prev, phase: "waiting" }));
|
patchSnapshot((prev) => ({ ...prev, phase: "waiting" }));
|
||||||
reconcileCue();
|
reconcileCue();
|
||||||
})
|
})
|
||||||
@@ -377,11 +342,9 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
onSpeechStart: () => {
|
onSpeechStart: () => {
|
||||||
logVoiceRuntime("speech_started");
|
|
||||||
stopCue();
|
stopCue();
|
||||||
},
|
},
|
||||||
onSpeechEnd: () => {
|
onSpeechEnd: () => {
|
||||||
logVoiceRuntime("speech_ended");
|
|
||||||
clearSpeechInterruptTimer();
|
clearSpeechInterruptTimer();
|
||||||
state.speechInterruptSent = false;
|
state.speechInterruptSent = false;
|
||||||
reconcileCue();
|
reconcileCue();
|
||||||
@@ -390,10 +353,6 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
|||||||
const previous = state.telemetry;
|
const previous = state.telemetry;
|
||||||
patchTelemetry((prev) => ({ ...prev, isDetecting }));
|
patchTelemetry((prev) => ({ ...prev, isDetecting }));
|
||||||
if (previous.isDetecting !== isDetecting) {
|
if (previous.isDetecting !== isDetecting) {
|
||||||
logVoiceRuntime("detecting_changed", {
|
|
||||||
isDetecting,
|
|
||||||
volume: Number(state.telemetry.volume.toFixed(3)),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (!previous.isDetecting && isDetecting) {
|
if (!previous.isDetecting && isDetecting) {
|
||||||
stopCue();
|
stopCue();
|
||||||
@@ -405,11 +364,6 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
|||||||
const previous = state.telemetry;
|
const previous = state.telemetry;
|
||||||
patchTelemetry((prev) => ({ ...prev, isSpeaking }));
|
patchTelemetry((prev) => ({ ...prev, isSpeaking }));
|
||||||
if (previous.isSpeaking !== isSpeaking) {
|
if (previous.isSpeaking !== isSpeaking) {
|
||||||
logVoiceRuntime("speaking_changed", {
|
|
||||||
isSpeaking,
|
|
||||||
phase: state.snapshot.phase,
|
|
||||||
volume: Number(state.telemetry.volume.toFixed(3)),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!previous.isSpeaking && isSpeaking) {
|
if (!previous.isSpeaking && isSpeaking) {
|
||||||
@@ -429,15 +383,8 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state.speechInterruptSent = true;
|
state.speechInterruptSent = true;
|
||||||
logVoiceRuntime("barge_in_interrupt_triggered", {
|
|
||||||
phase: state.snapshot.phase,
|
|
||||||
});
|
|
||||||
void interruptActiveTurn();
|
void interruptActiveTurn();
|
||||||
}, REALTIME_VOICE_VAD_CONFIG.interruptGracePeriodMs);
|
}, REALTIME_VOICE_VAD_CONFIG.interruptGracePeriodMs);
|
||||||
logVoiceRuntime("barge_in_interrupt_scheduled", {
|
|
||||||
gracePeriodMs: REALTIME_VOICE_VAD_CONFIG.interruptGracePeriodMs,
|
|
||||||
phase: state.snapshot.phase,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -543,9 +490,6 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
|||||||
},
|
},
|
||||||
|
|
||||||
registerSession(adapter) {
|
registerSession(adapter) {
|
||||||
logVoiceRuntime("session_registered", {
|
|
||||||
serverId: adapter.serverId,
|
|
||||||
});
|
|
||||||
sessions.set(adapter.serverId, {
|
sessions.set(adapter.serverId, {
|
||||||
adapter,
|
adapter,
|
||||||
connected: true,
|
connected: true,
|
||||||
@@ -566,10 +510,6 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
session.connected = connected;
|
session.connected = connected;
|
||||||
logVoiceRuntime("session_connection_changed", {
|
|
||||||
serverId,
|
|
||||||
connected,
|
|
||||||
});
|
|
||||||
if (state.snapshot.activeServerId !== serverId) {
|
if (state.snapshot.activeServerId !== serverId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -581,12 +521,6 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
|||||||
},
|
},
|
||||||
|
|
||||||
handleCapturePcm(chunk) {
|
handleCapturePcm(chunk) {
|
||||||
console.log("[VoiceRuntime] handleCapturePcm", {
|
|
||||||
bytes: chunk.byteLength,
|
|
||||||
isVoiceMode: state.snapshot.isVoiceMode,
|
|
||||||
isMuted: state.snapshot.isMuted,
|
|
||||||
phase: state.snapshot.phase,
|
|
||||||
});
|
|
||||||
if (!state.snapshot.isVoiceMode || state.snapshot.isMuted) {
|
if (!state.snapshot.isVoiceMode || state.snapshot.isMuted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -595,12 +529,6 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
|||||||
|
|
||||||
handleCaptureVolume(level) {
|
handleCaptureVolume(level) {
|
||||||
const nowMs = Date.now();
|
const nowMs = Date.now();
|
||||||
console.log("[VoiceRuntime] handleCaptureVolume", {
|
|
||||||
level,
|
|
||||||
isVoiceMode: state.snapshot.isVoiceMode,
|
|
||||||
isMuted: state.snapshot.isMuted,
|
|
||||||
phase: state.snapshot.phase,
|
|
||||||
});
|
|
||||||
const displayLevel = state.snapshot.isMuted ? 0 : level;
|
const displayLevel = state.snapshot.isMuted ? 0 : level;
|
||||||
publishDisplayVolume(displayLevel, nowMs);
|
publishDisplayVolume(displayLevel, nowMs);
|
||||||
if (!state.snapshot.isVoiceMode || state.snapshot.isMuted) {
|
if (!state.snapshot.isVoiceMode || state.snapshot.isMuted) {
|
||||||
@@ -610,10 +538,6 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async startVoice(serverId, agentId) {
|
async startVoice(serverId, agentId) {
|
||||||
logVoiceRuntime("start_requested", {
|
|
||||||
serverId,
|
|
||||||
agentId,
|
|
||||||
});
|
|
||||||
const session = sessions.get(serverId);
|
const session = sessions.get(serverId);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
throw new Error(`Voice runtime is not ready for host ${serverId}`);
|
throw new Error(`Voice runtime is not ready for host ${serverId}`);
|
||||||
@@ -678,26 +602,13 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
|||||||
phase: "listening",
|
phase: "listening",
|
||||||
isMuted: deps.engine.isMuted(),
|
isMuted: deps.engine.isMuted(),
|
||||||
}));
|
}));
|
||||||
logVoiceRuntime("start_completed", {
|
|
||||||
serverId,
|
|
||||||
agentId,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logVoiceRuntime("start_failed", {
|
|
||||||
serverId,
|
|
||||||
agentId,
|
|
||||||
error: error instanceof Error ? error.message : String(error),
|
|
||||||
});
|
|
||||||
await performLocalStop();
|
await performLocalStop();
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async stopVoice() {
|
async stopVoice() {
|
||||||
logVoiceRuntime("stop_requested", {
|
|
||||||
activeServerId: state.snapshot.activeServerId,
|
|
||||||
activeAgentId: state.snapshot.activeAgentId,
|
|
||||||
});
|
|
||||||
const activeSession = getActiveSession();
|
const activeSession = getActiveSession();
|
||||||
const generation = state.generation + 1;
|
const generation = state.generation + 1;
|
||||||
state.generation = generation;
|
state.generation = generation;
|
||||||
@@ -722,7 +633,6 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
|||||||
if (state.generation === generation) {
|
if (state.generation === generation) {
|
||||||
resetToDisabledState();
|
resetToDisabledState();
|
||||||
}
|
}
|
||||||
logVoiceRuntime("stop_completed");
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -736,9 +646,6 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
|||||||
|
|
||||||
toggleMute() {
|
toggleMute() {
|
||||||
const nextMuted = deps.engine.toggleMute();
|
const nextMuted = deps.engine.toggleMute();
|
||||||
logVoiceRuntime("mute_toggled", {
|
|
||||||
muted: nextMuted,
|
|
||||||
});
|
|
||||||
if (nextMuted) {
|
if (nextMuted) {
|
||||||
resetSegmenter(segmenter);
|
resetSegmenter(segmenter);
|
||||||
patchSnapshot((prev) => ({
|
patchSnapshot((prev) => ({
|
||||||
@@ -778,9 +685,6 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
logVoiceRuntime("assistant_audio_started", {
|
|
||||||
serverId,
|
|
||||||
});
|
|
||||||
stopCue();
|
stopCue();
|
||||||
getActiveSession()?.adapter.setAssistantAudioPlaying(true);
|
getActiveSession()?.adapter.setAssistantAudioPlaying(true);
|
||||||
patchSnapshot((prev) => ({ ...prev, phase: "playing" }));
|
patchSnapshot((prev) => ({ ...prev, phase: "playing" }));
|
||||||
@@ -791,10 +695,6 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logVoiceRuntime("assistant_audio_finished", {
|
|
||||||
serverId,
|
|
||||||
turnInProgress: state.turnInProgress,
|
|
||||||
});
|
|
||||||
getActiveSession()?.adapter.setAssistantAudioPlaying(false);
|
getActiveSession()?.adapter.setAssistantAudioPlaying(false);
|
||||||
if (!state.snapshot.isVoiceMode) {
|
if (!state.snapshot.isVoiceMode) {
|
||||||
return;
|
return;
|
||||||
@@ -822,9 +722,6 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logVoiceRuntime("empty_transcription_result", {
|
|
||||||
serverId,
|
|
||||||
});
|
|
||||||
state.turnInProgress = false;
|
state.turnInProgress = false;
|
||||||
patchSnapshot((prev) => ({ ...prev, phase: "listening" }));
|
patchSnapshot((prev) => ({ ...prev, phase: "listening" }));
|
||||||
stopCue();
|
stopCue();
|
||||||
@@ -838,11 +735,6 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
logVoiceRuntime("turn_event", {
|
|
||||||
serverId,
|
|
||||||
agentId,
|
|
||||||
eventType,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (eventType === "turn_started") {
|
if (eventType === "turn_started") {
|
||||||
state.turnInProgress = true;
|
state.turnInProgress = true;
|
||||||
|
|||||||
Reference in New Issue
Block a user