- Add Tauri v2 shell (Cargo, tauri.conf.json, capabilities, plugins) - Migrate all fetch() calls to apiFetch() for Tauri-aware HTTP - Migrate WebSocket endpoints to resolveEndpoints() for dynamic URLs - Add ServerConfigDialog for remote server URL configuration - Add tauri.ts lib with isTauri detection, apiFetch wrapper, plugin helpers - Add server-config Pinia store with persistence via plugin-store - Conditional PWA (disabled in Tauri builds) - Android: home screen transcript widget (last 5 messages, 30s refresh) - Android: voice command / share activity (SpeechRecognizer + WebSocket) - Android: signed release APK with auto-copy to installers/ - Remove stale frontend/src-tauri directory
77 lines
2.0 KiB
TypeScript
77 lines
2.0 KiB
TypeScript
import { resolveEndpoints } from '@/config/endpoints'
|
|
import { useSessionState } from '@/stores/session-state'
|
|
|
|
let ws: WebSocket | null = null
|
|
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
|
let reconnectDelay = 1000
|
|
const MAX_RECONNECT_DELAY = 15000
|
|
|
|
function connect() {
|
|
const store = useSessionState()
|
|
|
|
// Connect to terminal server (4103) — same base as terminal WS
|
|
const url = resolveEndpoints().terminal
|
|
console.log('[SessionStateWS] Connecting to', url)
|
|
|
|
ws = new WebSocket(url)
|
|
|
|
ws.onopen = () => {
|
|
console.log('[SessionStateWS] Connected')
|
|
store.setConnected(true)
|
|
reconnectDelay = 1000
|
|
}
|
|
|
|
ws.onmessage = (event) => {
|
|
try {
|
|
const msg = JSON.parse(event.data)
|
|
if (msg.type === 'session-state-snapshot' || msg.type === 'session-state-patch' || msg.type === 'terminal-registry-change') {
|
|
store.handleMessage(msg)
|
|
}
|
|
// Ignore other message types (terminal output, legacy broadcasts, etc.)
|
|
} catch {
|
|
// Non-JSON or malformed — ignore
|
|
}
|
|
}
|
|
|
|
ws.onclose = () => {
|
|
console.log('[SessionStateWS] Disconnected, reconnecting in', reconnectDelay, 'ms')
|
|
store.setConnected(false)
|
|
ws = null
|
|
scheduleReconnect()
|
|
}
|
|
|
|
ws.onerror = () => {
|
|
// onclose will fire after onerror
|
|
}
|
|
}
|
|
|
|
function scheduleReconnect() {
|
|
if (reconnectTimer) clearTimeout(reconnectTimer)
|
|
reconnectTimer = setTimeout(() => {
|
|
reconnectTimer = null
|
|
connect()
|
|
// Exponential backoff
|
|
reconnectDelay = Math.min(reconnectDelay * 1.5, MAX_RECONNECT_DELAY)
|
|
}, reconnectDelay)
|
|
}
|
|
|
|
/** Initialize the session state WebSocket. Call once on app mount. */
|
|
export function initSessionStateWS() {
|
|
if (ws) return // Already connected
|
|
connect()
|
|
}
|
|
|
|
/** Disconnect and stop reconnecting. */
|
|
export function destroySessionStateWS() {
|
|
if (reconnectTimer) {
|
|
clearTimeout(reconnectTimer)
|
|
reconnectTimer = null
|
|
}
|
|
if (ws) {
|
|
ws.onclose = null // Prevent reconnect
|
|
ws.close()
|
|
ws = null
|
|
}
|
|
useSessionState().setConnected(false)
|
|
}
|