fix(desktop): prevent transformCallback crash in browser mode
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

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()
This commit is contained in:
iven
2026-04-03 13:00:36 +08:00
parent 564c7ca28f
commit 5b1b747810
2 changed files with 107 additions and 2 deletions

View File

@@ -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;

View File

@@ -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<T>(
cmd: string,
args?: Record<string, unknown>,
): Promise<T | null> {
if (!isTauriRuntime()) {
log.debug(`invoke("${cmd}") skipped — not in Tauri runtime`);
return null;
}
const { invoke } = await import('@tauri-apps/api/core');
return invoke<T>(cmd, args);
}
/**
* Like `safeInvoke` but throws when not in Tauri runtime.
* Use for operations that MUST have a Tauri backend.
*/
export async function requireInvoke<T>(
cmd: string,
args?: Record<string, unknown>,
): Promise<T> {
if (!isTauriRuntime()) {
throw new Error(`invoke("${cmd}") failed — not running in Tauri WebView`);
}
const { invoke } = await import('@tauri-apps/api/core');
return invoke<T>(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<T>(
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<T>(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<T>(
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<T>(event, (e) => handler(e));
}