From 5b1b747810ff2c7e9ff7967cba9f3d67b0e4831e Mon Sep 17 00:00:00 2001 From: iven Date: Fri, 3 Apr 2026 13:00:36 +0800 Subject: [PATCH] fix(desktop): prevent transformCallback crash in browser mode Root cause: ChatArea.tsx called listen() from @tauri-apps/api/event directly on component mount without checking isTauriRuntime(). When accessed from a regular browser (not Tauri WebView), window.__TAURI_INTERNALS__ is undefined, causing "Cannot read properties of undefined (reading 'transformCallback')". Solution: - Created lib/safe-tauri.ts with safe wrappers (safeInvoke, safeListen, safeListenEvent, requireInvoke) that gracefully degrade when Tauri IPC is unavailable - Replaced direct listen() call in ChatArea.tsx with safeListenEvent() --- desktop/src/components/ChatArea.tsx | 5 +- desktop/src/lib/safe-tauri.ts | 104 ++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 desktop/src/lib/safe-tauri.ts diff --git a/desktop/src/components/ChatArea.tsx b/desktop/src/components/ChatArea.tsx index e399411..6b6c20a 100644 --- a/desktop/src/components/ChatArea.tsx +++ b/desktop/src/components/ChatArea.tsx @@ -7,7 +7,8 @@ import { useArtifactStore } from '../store/chat/artifactStore'; import { useConnectionStore } from '../store/connectionStore'; import { useAgentStore } from '../store/agentStore'; import { useConfigStore } from '../store/configStore'; -import { listen, type UnlistenFn } from '@tauri-apps/api/event'; +import { type UnlistenFn } from '@tauri-apps/api/event'; +import { safeListenEvent } from '../lib/safe-tauri'; import { Paperclip, SquarePen, ArrowUp, MessageSquare, Download, X, FileText, Image as ImageIcon } from 'lucide-react'; import { Button, EmptyState, MessageListSkeleton, LoadingDots } from './ui'; import { ResizableChatLayout } from './ai/ResizableChatLayout'; @@ -160,7 +161,7 @@ export function ChatArea() { // Listen for hand-execution-complete Tauri events useEffect(() => { let unlisten: UnlistenFn | undefined; - listen<{ approvalId: string; handId: string; success: boolean; error?: string | null }>( + safeListenEvent<{ approvalId: string; handId: string; success: boolean; error?: string | null }>( 'hand-execution-complete', (event) => { const { handId, success, error } = event.payload; diff --git a/desktop/src/lib/safe-tauri.ts b/desktop/src/lib/safe-tauri.ts new file mode 100644 index 0000000..12d21d3 --- /dev/null +++ b/desktop/src/lib/safe-tauri.ts @@ -0,0 +1,104 @@ +/** + * Safe Tauri API Wrappers + * + * All Tauri APIs (invoke, listen, etc.) depend on `window.__TAURI_INTERNALS__` + * which only exists inside the Tauri WebView. When the frontend is accessed + * from a regular browser (e.g. http://localhost:1420/ for debugging), these + * APIs throw cryptic errors like: + * + * TypeError: Cannot read properties of undefined (reading 'transformCallback') + * + * This module provides drop-in replacements that gracefully degrade when + * the Tauri runtime is not available. + */ + +import { isTauriRuntime } from './tauri-gateway'; +import { createLogger } from './logger'; + +const log = createLogger('safe-tauri'); + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export type { UnlistenFn } from '@tauri-apps/api/event'; + +// --------------------------------------------------------------------------- +// Safe invoke +// --------------------------------------------------------------------------- + +/** + * Type-safe wrapper around Tauri `invoke`. + * Returns `null` (with a debug log) when not in Tauri runtime. + */ +export async function safeInvoke( + cmd: string, + args?: Record, +): Promise { + if (!isTauriRuntime()) { + log.debug(`invoke("${cmd}") skipped — not in Tauri runtime`); + return null; + } + + const { invoke } = await import('@tauri-apps/api/core'); + return invoke(cmd, args); +} + +/** + * Like `safeInvoke` but throws when not in Tauri runtime. + * Use for operations that MUST have a Tauri backend. + */ +export async function requireInvoke( + cmd: string, + args?: Record, +): Promise { + if (!isTauriRuntime()) { + throw new Error(`invoke("${cmd}") failed — not running in Tauri WebView`); + } + + const { invoke } = await import('@tauri-apps/api/core'); + return invoke(cmd, args); +} + +// --------------------------------------------------------------------------- +// Safe listen +// --------------------------------------------------------------------------- + +/** + * Wrapper around Tauri `listen`. + * Returns a no-op `UnlistenFn` when not in Tauri runtime. + * + * Usage — replace: + * import { listen } from '@tauri-apps/api/event'; + * With: + * import { safeListen } from '../lib/safe-tauri'; + */ +export async function safeListen( + event: string, + handler: (payload: T) => void, +): Promise<() => void> { + if (!isTauriRuntime()) { + log.debug(`listen("${event}") skipped — not in Tauri runtime`); + return () => {}; + } + + const { listen } = await import('@tauri-apps/api/event'); + return listen(event, (e) => handler(e.payload)); +} + +/** + * Wrapper around Tauri `listen` that provides the full Event object. + * Returns a no-op `UnlistenFn` when not in Tauri runtime. + */ +export async function safeListenEvent( + event: string, + handler: (event: { event: string; payload: T }) => void, +): Promise<() => void> { + if (!isTauriRuntime()) { + log.debug(`listen("${event}") skipped — not in Tauri runtime`); + return () => {}; + } + + const { listen } = await import('@tauri-apps/api/event'); + return listen(event, (e) => handler(e)); +}