diff --git a/desktop/src/features/agents/lib/useAgentsDataRefresh.ts b/desktop/src/features/agents/lib/useAgentsDataRefresh.ts index 174fb9c92c..b65a816776 100644 --- a/desktop/src/features/agents/lib/useAgentsDataRefresh.ts +++ b/desktop/src/features/agents/lib/useAgentsDataRefresh.ts @@ -1,3 +1,4 @@ +import { isTauri } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; import { useQueryClient } from "@tanstack/react-query"; import { useEffect } from "react"; @@ -26,6 +27,11 @@ export function useAgentsDataRefresh(): void { const queryClient = useQueryClient(); useEffect(() => { + // Both events come from the local Rust agents backend — no on-disk agents + // data exists in a browser (web) runtime, and `listen()` throws without + // Tauri internals. + if (!isTauri()) return; + let timer: ReturnType | undefined; const unlistenRuntime = listen("managed-agent-runtime-status", () => { diff --git a/desktop/src/features/agents/usePreventSleep.ts b/desktop/src/features/agents/usePreventSleep.ts index 4c2d48b97d..5dc4db5c25 100644 --- a/desktop/src/features/agents/usePreventSleep.ts +++ b/desktop/src/features/agents/usePreventSleep.ts @@ -7,6 +7,7 @@ import { import { createPreventSleepActivityTracker } from "@/features/agents/preventSleepActivity"; import { setPreventSleepActive } from "@/shared/api/tauri"; import { normalizePubkey } from "@/shared/lib/pubkey"; +import { isTauri } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; // Intentionally not scoped per-pubkey — multi-user desktop is rare and the @@ -85,6 +86,10 @@ function usePreventSleepInternal() { void setPreventSleepActive(active); }, [active]); React.useEffect(() => { + // Sleep prevention is a native OS concept — no such event exists in a + // browser (web) runtime, and `listen()` throws without Tauri internals. + if (!isTauri()) return; + const unlisten = listen("prevent-sleep-expired", () => { setExpired(true); }); diff --git a/desktop/src/features/communities/legacyCommunityStorage.ts b/desktop/src/features/communities/legacyCommunityStorage.ts index d140fa4c62..2bc51c26ca 100644 --- a/desktop/src/features/communities/legacyCommunityStorage.ts +++ b/desktop/src/features/communities/legacyCommunityStorage.ts @@ -1,3 +1,5 @@ +import { isTauri } from "@tauri-apps/api/core"; + import { invokeTauri } from "@/shared/api/tauri"; import { migrateLegacyCommunityStorage } from "./communityStorage"; @@ -115,6 +117,12 @@ export async function migrateLegacyCommunityStorageBeforeRender(): Promise return; } + // The legacy Sprout WebKit SQLite database only exists in the native shell + // — there is nothing to migrate from in a browser (web) runtime. + if (!isTauri()) { + return; + } + migrateLegacyCommunityStorage(window.localStorage); const currentCommunitiesRaw = window.localStorage.getItem(BUZZ_COMMUNITIES_KEY); diff --git a/desktop/src/features/communities/useNestNotifications.ts b/desktop/src/features/communities/useNestNotifications.ts index d93bb89ad2..41ebd75ee6 100644 --- a/desktop/src/features/communities/useNestNotifications.ts +++ b/desktop/src/features/communities/useNestNotifications.ts @@ -1,3 +1,4 @@ +import { isTauri } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; import { useEffect } from "react"; import { toast } from "sonner"; @@ -22,6 +23,10 @@ const MIGRATION_TOAST_KEY = "buzz-legacy-nest-migrated-notified"; */ export function useNestNotifications(): void { useEffect(() => { + // Both events come from the Rust nest backend — no local nest exists in a + // browser (web) runtime, and `listen()` throws without Tauri internals. + if (!isTauri()) return; + const unlistenReposError = listen("repos-dir-error", (event) => { toast.error("Repos directory not applied", { description: event.payload, diff --git a/desktop/src/features/huddle/HuddleContext.tsx b/desktop/src/features/huddle/HuddleContext.tsx index 804eb4bc0e..7874b6f996 100644 --- a/desktop/src/features/huddle/HuddleContext.tsx +++ b/desktop/src/features/huddle/HuddleContext.tsx @@ -1,4 +1,4 @@ -import { invoke } from "@tauri-apps/api/core"; +import { invoke, isTauri } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; import * as React from "react"; @@ -616,6 +616,10 @@ export function HuddleProvider({ children }: { children: React.ReactNode }) { // in-flight guard collapses duplicate disconnect events from failed dials. const audioReconnectInFlightRef = React.useRef(false); React.useEffect(() => { + // Huddle audio is a native Tauri pipeline — no such event exists in a + // browser (web) runtime, and `listen()` throws without Tauri internals. + if (!isTauri()) return; + let cancelled = false; let unlisten: (() => void) | null = null; listen("huddle-audio-disconnected", () => { diff --git a/desktop/src/features/huddle/components/HuddleBar.tsx b/desktop/src/features/huddle/components/HuddleBar.tsx index 35d660e475..eab1a95e76 100644 --- a/desktop/src/features/huddle/components/HuddleBar.tsx +++ b/desktop/src/features/huddle/components/HuddleBar.tsx @@ -1,4 +1,4 @@ -import { invoke } from "@tauri-apps/api/core"; +import { invoke, isTauri } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; import { Bot, @@ -216,6 +216,11 @@ export function HuddleBar({ }, []); // Huddle state: event-driven + slow fallback poll. React.useEffect(() => { + // The huddle backend (state + audio pipeline) is native Tauri-only — none + // of this works in a browser (web) runtime, and `listen()` throws without + // Tauri internals. + if (!isTauri()) return; + let cancelled = false; let unlisten: (() => void) | null = null; diff --git a/desktop/src/features/huddle/components/HuddleIndicator.tsx b/desktop/src/features/huddle/components/HuddleIndicator.tsx index 19b504cf28..bf11d78417 100644 --- a/desktop/src/features/huddle/components/HuddleIndicator.tsx +++ b/desktop/src/features/huddle/components/HuddleIndicator.tsx @@ -1,3 +1,4 @@ +import { isTauri } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; import { Headphones } from "lucide-react"; import * as React from "react"; @@ -190,6 +191,11 @@ export function HuddleIndicator({ // waiting for the relay's 48103 event (which may arrive late or not at all // if the relay connection tears down first). React.useEffect(() => { + // Huddle state is emitted by the native Tauri audio pipeline — no such + // event exists in a browser (web) runtime, and `listen()` throws without + // Tauri internals. + if (!isTauri()) return; + let unlisten: (() => void) | null = null; let cancelled = false; diff --git a/desktop/src/features/settings/ui/MobilePairingCard.tsx b/desktop/src/features/settings/ui/MobilePairingCard.tsx index e15f54316b..2719f24397 100644 --- a/desktop/src/features/settings/ui/MobilePairingCard.tsx +++ b/desktop/src/features/settings/ui/MobilePairingCard.tsx @@ -8,6 +8,7 @@ import { TriangleAlert, X, } from "lucide-react"; +import { isTauri } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; import { toast } from "sonner"; @@ -220,6 +221,13 @@ export function MobilePairingCard({ return; } + // Mobile pairing is a native Tauri feature (QR/SAS handshake with a + // phone) — none of these events exist in a browser (web) runtime, and + // `listen()` throws without Tauri internals. + if (!isTauri()) { + return; + } + let cancelled = false; const unlisteners: (() => void)[] = []; diff --git a/desktop/src/shared/api/tauri.ts b/desktop/src/shared/api/tauri.ts index e8a8b885cf..8ae7cec8e5 100644 --- a/desktop/src/shared/api/tauri.ts +++ b/desktop/src/shared/api/tauri.ts @@ -1,4 +1,4 @@ -import { invoke as tauriInvoke } from "@tauri-apps/api/core"; +import { invoke as tauriInvoke, isTauri } from "@tauri-apps/api/core"; import { activateRateLimit, parseRateLimitHint, @@ -366,15 +366,26 @@ export async function getPresence(pubkeys: string[]): Promise { ); } +// Called unconditionally on every boot by useCommunityInit's "no active +// community yet" branch, which already wraps both calls in its own +// try/catch and falls back to needsSetup:true — so this guard is a +// console-noise reduction, not a behavior fix. export function getDefaultRelayUrl(): Promise { + if (!isTauri()) return Promise.reject(new Error("no Tauri runtime")); return invokeTauri("get_default_relay_url"); } export function autoConnectDefaultRelayEnabled(): Promise { + if (!isTauri()) return Promise.resolve(false); return invokeTauri("auto_connect_default_relay_enabled"); } +// Called unconditionally at app boot, before any Tauri-availability gate — +// unlike most of this file's exports, which only run once the app has +// already reached a native-only screen. Guard so it resolves cleanly to +// `false` in a browser (web) runtime instead of throwing. export function isSharedIdentity(): Promise { + if (!isTauri()) return Promise.resolve(false); return invokeTauri("is_shared_identity"); } @@ -1188,8 +1199,14 @@ export async function validateReposDir(dir: string): Promise { await invokeTauri("validate_repos_dir", { dir }); } +// Sleep prevention is a native OS concept with no browser equivalent. +// usePreventSleep.ts calls this fire-and-forget (`void setPreventSleepActive(...)`) +// with no .catch of its own, so an unguarded call here was a genuine unhandled +// promise rejection in a web (non-Tauri) runtime — not just console noise. export const setPreventSleepActive = (active: boolean) => - invokeTauri("set_prevent_sleep_active", { active }); + isTauri() + ? invokeTauri("set_prevent_sleep_active", { active }) + : Promise.resolve(); export const setAgentManagedProfiles = (enabled: boolean) => invokeTauri("set_agent_managed_profiles", { enabled }); diff --git a/desktop/src/shared/deep-link.ts b/desktop/src/shared/deep-link.ts index c62a8bec3b..e491ee09a9 100644 --- a/desktop/src/shared/deep-link.ts +++ b/desktop/src/shared/deep-link.ts @@ -1,5 +1,5 @@ import { listen, type UnlistenFn } from "@tauri-apps/api/event"; -import { invoke } from "@tauri-apps/api/core"; +import { invoke, isTauri } from "@tauri-apps/api/core"; import type { StartCommunityOnboardingInput } from "@/features/onboarding/communityOnboarding"; export type AddCommunityDeepLinkPayload = { @@ -84,6 +84,11 @@ function acceptPendingCommunityDeepLink( } async function drainPendingCommunityDeepLinks(deps: DeepLinkDeps) { + // The pending-deep-link queue is populated by the Rust backend and only + // exists in the native Tauri shell — invoke() throws without Tauri + // internals in a browser (web) runtime. + if (!isTauri()) return; + while (true) { const pending = await invoke( "take_pending_community_deep_link", @@ -134,6 +139,17 @@ export async function listenForDeepLinks( })(); }; const stopAvailabilityListener = deps.onAddCommunityAvailable(drain); + + // OS-level deep links (`buzz://…`) only exist in the native Tauri shell — + // `listen()` throws without Tauri internals in a browser (web) runtime. + // The in-app "add community available" wiring above still applies. + if (!isTauri()) { + drain(); + return () => { + stopAvailabilityListener(); + }; + } + const connectPromise = listen("deep-link-connect", drain); const joinPromise = listen("deep-link-join", drain); const addCommunityPromise = listen( @@ -160,6 +176,8 @@ export async function listenForDeepLinks( export function listenForMessageDeepLinks( onOpen: (payload: MessageDeepLinkPayload) => void, ): Promise { + // OS-level deep link — no-op outside the native Tauri shell. + if (!isTauri()) return Promise.resolve(() => {}); return listen("deep-link-message", (event) => { onOpen(event.payload); }); @@ -168,6 +186,8 @@ export function listenForMessageDeepLinks( export function listenForNostrBindDeepLinks( onOpen: (payload: NostrBindDeepLinkPayload) => void, ): Promise { + // OS-level deep link — no-op outside the native Tauri shell. + if (!isTauri()) return Promise.resolve(() => {}); return listen("deep-link-nostr-bind", (event) => { onOpen(event.payload); });