Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 10 additions & 11 deletions desktop/src/main/ipc-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export function registerIpcHandlers() {
(SELECT content FROM messages WHERE conversation_id = c.id ORDER BY created_at ASC LIMIT 1) as preview,
(SELECT COUNT(*) FROM messages WHERE conversation_id = c.id) as message_count
FROM conversations c
ORDER BY c.updated_at DESC`,
ORDER BY c.updated_at DESC`
)
.all()
})
Expand All @@ -57,7 +57,7 @@ export function registerIpcHandlers() {
db.prepare('UPDATE conversations SET title = ?, updated_at = ? WHERE id = ?').run(
title,
Date.now(),
id,
id
)
})

Expand All @@ -76,14 +76,14 @@ export function registerIpcHandlers() {
conversationId: number,
role: string,
content: string,
latency?: { sttLatencyMs?: number, llmLatencyMs?: number, ttsLatencyMs?: number },
providers?: { sttProvider?: string, llmProvider?: string, ttsProvider?: string },
latency?: { sttLatencyMs?: number; llmLatencyMs?: number; ttsLatencyMs?: number },
providers?: { sttProvider?: string; llmProvider?: string; ttsProvider?: string }
) => {
const db = getDb()
const now = Date.now()
const result = db
.prepare(
'INSERT INTO messages (conversation_id, role, content, created_at, stt_latency_ms, llm_latency_ms, tts_latency_ms, stt_provider, llm_provider, tts_provider) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
'INSERT INTO messages (conversation_id, role, content, created_at, stt_latency_ms, llm_latency_ms, tts_latency_ms, stt_provider, llm_provider, tts_provider) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
)
.run(
conversationId,
Expand All @@ -95,7 +95,7 @@ export function registerIpcHandlers() {
latency?.ttsLatencyMs ?? null,
providers?.sttProvider ?? null,
providers?.llmProvider ?? null,
providers?.ttsProvider ?? null,
providers?.ttsProvider ?? null
)
db.prepare('UPDATE conversations SET updated_at = ? WHERE id = ?').run(now, conversationId)
return {
Expand All @@ -111,7 +111,7 @@ export function registerIpcHandlers() {
llm_provider: providers?.llmProvider ?? null,
tts_provider: providers?.ttsProvider ?? null,
}
},
}
)

ipcMain.handle('db:getMessages', (_e, conversationId: number) => {
Expand All @@ -133,13 +133,13 @@ export function registerIpcHandlers() {
ipcMain.handle('db:setSetting', (_e, key: string, value: string) => {
const db = getDb()
db.prepare(
'INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = ?',
'INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = ?'
).run(key, value, value)
})

ipcMain.handle('db:getAllSettings', () => {
const db = getDb()
const rows = db.prepare('SELECT * FROM settings').all() as { key: string, value: string }[]
const rows = db.prepare('SELECT * FROM settings').all() as { key: string; value: string }[]
return Object.fromEntries(rows.map((r) => [r.key, r.value]))
})

Expand Down Expand Up @@ -172,8 +172,7 @@ function toHealthUrl(url: string): string | null {
if (parsed.protocol === 'ws:') parsed.protocol = 'http:'
else if (parsed.protocol === 'wss:') parsed.protocol = 'https:'
else return null
// Strip /ws path and append /health
parsed.pathname = parsed.pathname.replace(/\/ws\/?$/, '')
parsed.pathname = parsed.pathname.replace(/\/(?:ws|voiceclaw\/realtime)\/?$/, '')
parsed.pathname = parsed.pathname.replace(/\/$/, '') + '/health'
return parsed.toString()
} catch {
Expand Down
38 changes: 38 additions & 0 deletions desktop/src/renderer/src/lib/realtime-settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export type RealtimeConnectionMode = 'relay' | 'realtime-brain'

export const RELAY_DEFAULT_SERVER_URL = 'ws://localhost:8080/ws'
export const REALTIME_BRAIN_DEFAULT_SERVER_URL = 'ws://localhost:19789/voiceclaw/realtime'
export const LEGACY_REALTIME_SERVER_URL_SETTING = 'realtime_server_url'
export const REALTIME_CONNECTION_MODE_SETTING = 'realtime_connection_mode'

const SERVER_URL_SETTING_BY_MODE: Record<RealtimeConnectionMode, string> = {
relay: 'realtime_server_url_relay',
'realtime-brain': 'realtime_server_url_realtime_brain',
}

export function isRealtimeConnectionMode(value: string | null): value is RealtimeConnectionMode {
return value === 'relay' || value === 'realtime-brain'
}

export function defaultServerUrlForMode(mode: RealtimeConnectionMode): string {
return mode === 'realtime-brain' ? REALTIME_BRAIN_DEFAULT_SERVER_URL : RELAY_DEFAULT_SERVER_URL
}

export function serverUrlSettingKeyForMode(mode: RealtimeConnectionMode): string {
return SERVER_URL_SETTING_BY_MODE[mode]
}

export function resolveServerUrlForMode(
mode: RealtimeConnectionMode,
savedUrl: string | null
): string {
const trimmed = savedUrl?.trim()
if (!trimmed) return defaultServerUrlForMode(mode)
if (mode === 'realtime-brain' && trimmed === RELAY_DEFAULT_SERVER_URL) {
return REALTIME_BRAIN_DEFAULT_SERVER_URL
}
if (mode === 'relay' && trimmed === REALTIME_BRAIN_DEFAULT_SERVER_URL) {
return RELAY_DEFAULT_SERVER_URL
}
return trimmed
}
22 changes: 13 additions & 9 deletions desktop/src/renderer/src/lib/use-realtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@

import { useCallback, useEffect, useRef, useState } from 'react'
import { AudioEngine } from './audio-engine'
import type { RealtimeConnectionMode } from './realtime-settings'

export interface RealtimeConfig {
connectionMode?: RealtimeConnectionMode
serverUrl: string
voice: string
model?: string
brainAgent: 'enabled' | 'none'
apiKey: string
apiKey?: string
sessionKey?: string
volume?: number
inputDeviceId?: string
Expand All @@ -20,7 +22,7 @@ export interface RealtimeConfig {
deviceModel?: string
}
instructionsOverride?: string
conversationHistory?: { role: 'user' | 'assistant', text: string }[]
conversationHistory?: { role: 'user' | 'assistant'; text: string }[]
tracingEnabled?: boolean
}

Expand Down Expand Up @@ -72,7 +74,7 @@ export function useRealtime(callbacks: RealtimeCallbacks): RealtimeControls {
if (!configRef.current?.tracingEnabled) return
if (wsRef.current?.readyState !== WebSocket.OPEN) return
wsRef.current.send(
JSON.stringify({ type: 'client.timing', phase, ms, turnId: turnId ?? undefined }),
JSON.stringify({ type: 'client.timing', phase, ms, turnId: turnId ?? undefined })
)
}, [])

Expand Down Expand Up @@ -160,7 +162,7 @@ export function useRealtime(callbacks: RealtimeCallbacks): RealtimeControls {
break
}
},
[sendTiming],
[sendTiming]
)

const start = useCallback(
Expand Down Expand Up @@ -208,12 +210,12 @@ export function useRealtime(callbacks: RealtimeCallbacks): RealtimeControls {
voice: config.voice,
model: config.model,
brainAgent: config.brainAgent,
apiKey: config.apiKey,
apiKey: config.apiKey ?? '',
sessionKey: config.sessionKey,
deviceContext: config.deviceContext,
instructionsOverride: config.instructionsOverride,
conversationHistory: config.conversationHistory,
}),
})
)

// Start mic capture — audio data flows to WebSocket
Expand Down Expand Up @@ -243,7 +245,9 @@ export function useRealtime(callbacks: RealtimeCallbacks): RealtimeControls {
// Only surface the error on the initial connection attempt.
// During reconnect, onclose handles retry logic.
if (!hasConnectedRef.current && reconnectAttemptsRef.current === 0) {
callbacksRef.current.onError?.('Could not connect to relay server. Is it running?', 0)
const label =
config.connectionMode === 'realtime-brain' ? 'OpenClaw real-time brain' : 'relay server'
callbacksRef.current.onError?.(`Could not connect to ${label}. Is it running?`, 0)
}
}

Expand All @@ -263,7 +267,7 @@ export function useRealtime(callbacks: RealtimeCallbacks): RealtimeControls {
reconnectAttemptsRef.current += 1
setIsReconnecting(true)
console.log(
`[useRealtime] Unexpected disconnect — reconnect attempt ${reconnectAttemptsRef.current}/${MAX_RECONNECT_ATTEMPTS} in ${delay}ms`,
`[useRealtime] Unexpected disconnect — reconnect attempt ${reconnectAttemptsRef.current}/${MAX_RECONNECT_ATTEMPTS} in ${delay}ms`
)
reconnectTimerRef.current = setTimeout(() => {
reconnectTimerRef.current = null
Expand All @@ -279,7 +283,7 @@ export function useRealtime(callbacks: RealtimeCallbacks): RealtimeControls {
}
}
},
[handleMessage],
[handleMessage]
)

const stop = useCallback(() => {
Expand Down
Loading
Loading