feat: integrate Tauri v2 with Android widget and voice assistant

- 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
This commit is contained in:
2026-02-23 15:33:43 -06:00
parent 6dc0c5ff6f
commit e1aa8b1bdb
108 changed files with 8155 additions and 151 deletions

85
frontend/src/lib/tauri.ts Normal file
View File

@@ -0,0 +1,85 @@
/**
* Tauri detection and fetch wrapper for Agent UI.
* In Tauri mode, relative /api/... paths are resolved against the configured server URL.
* In web mode, this is a transparent pass-through to native fetch.
*/
// Detect if running inside a Tauri webview
export const isTauri = '__TAURI_INTERNALS__' in window
// Detect mobile Tauri (Android/iOS)
export function isMobileTauri(): boolean {
if (!isTauri) return false
const ua = navigator.userAgent.toLowerCase()
return /android|iphone|ipad|ipod/.test(ua)
}
// Server URL storage (in-memory, loaded from Tauri store on init)
let _serverUrl = ''
export function getServerUrl(): string {
return _serverUrl
}
export function setServerUrl(url: string) {
// Normalize: remove trailing slash
_serverUrl = url.replace(/\/+$/, '')
}
/**
* Resolve a path to a full URL.
* - In web mode: returns the path as-is (relative URLs work with Vite proxy / reverse proxy)
* - In Tauri mode: prepends the configured server URL
*/
export function resolveUrl(path: string): string {
if (!isTauri) return path
if (!_serverUrl) {
console.warn('[Tauri] No server URL configured, using path as-is:', path)
return path
}
// If already absolute, return as-is
if (path.startsWith('http://') || path.startsWith('https://')) return path
return `${_serverUrl}${path}`
}
/**
* Fetch wrapper that resolves relative API paths in Tauri mode.
* In Tauri, uses @tauri-apps/plugin-http for proper CORS-free requests.
* In web, delegates to native fetch.
*/
export async function apiFetch(input: string | URL | Request, init?: RequestInit): Promise<Response> {
if (!isTauri) {
return fetch(input, init)
}
// Resolve URL
const url = typeof input === 'string' ? resolveUrl(input) : input
// Use Tauri HTTP plugin for cross-origin requests
try {
const { fetch: tauriFetch } = await import('@tauri-apps/plugin-http')
return tauriFetch(url, init)
} catch (e) {
// Fallback to native fetch if plugin fails
console.warn('[Tauri] HTTP plugin failed, falling back to fetch:', e)
return fetch(url, init)
}
}
// Dynamic plugin imports (only used behind isTauri checks)
export async function getTauriStore() {
const { LazyStore } = await import('@tauri-apps/plugin-store')
return new LazyStore('settings.json')
}
export async function getTauriNotification() {
return import('@tauri-apps/plugin-notification')
}
export async function getTauriClipboard() {
return import('@tauri-apps/plugin-clipboard-manager')
}
export async function getTauriDialog() {
return import('@tauri-apps/plugin-dialog')
}