Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions desktop/src-tauri/src/commands/relay_reconnect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@
// `include!`. See reconnect_hook_config.rs for why this is shared, not a module.
include!("reconnect_hook_config.rs");

/// Returns `true` when an internal build has a reconnect hook configured.
///
/// Used by the frontend to decide whether to show "Waiting to reconnect…"
/// copy and to skip escalation in OSS builds where the hook is a no-op.
#[tauri::command]
pub fn relay_reconnect_hook_configured() -> bool {
option_env!("BUZZ_DESKTOP_BUILD_RELAY_RECONNECT_CMD").is_some()
}

#[tauri::command]
pub async fn relay_reconnect_hook() -> Result<(), String> {
let Some(config_str) = option_env!("BUZZ_DESKTOP_BUILD_RELAY_RECONNECT_CMD") else {
Expand Down
1 change: 1 addition & 0 deletions desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,7 @@ pub fn run() {
set_prevent_sleep_active,
get_agent_memory,
relay_reconnect_hook,
relay_reconnect_hook_configured,
])
.build(tauri::generate_context!())
.expect("error while building tauri application");
Expand Down
32 changes: 31 additions & 1 deletion desktop/src/features/onboarding/ui/ProfileStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from "react";
import { toast } from "sonner";

import { SidebarRelayConnectionCompactCard } from "@/features/sidebar/ui/SidebarRelayConnectionCard";
import { useRelayConnection } from "@/shared/api/useRelayConnection";
import { useReconnectRelay } from "@/shared/api/useReconnectRelay";
import { cn } from "@/shared/lib/cn";
import { isRelayUnreachableError } from "@/shared/lib/relayError";
Expand Down Expand Up @@ -30,7 +31,15 @@ function OnboardingRelayConnectionErrorCard({
isSaving: boolean;
message: string;
}) {
const { isPending: isReconnectPending, reconnect } = useReconnectRelay();
const {
isPending: isReconnectPending,
isWaitingOnReconnectHook,
reconnect,
} = useReconnectRelay();
// Track whether a reconnect attempt was ever initiated from this card so we
// don't call markSuccess() on a "connected" state that pre-dates any click.
const hadActiveReconnectRef = React.useRef(false);
const relayConnectionState = useRelayConnection();
const [dismissedErrorMessage, setDismissedErrorMessage] = React.useState<
string | null
>(null);
Expand Down Expand Up @@ -73,23 +82,43 @@ function OnboardingRelayConnectionErrorCard({
}, ONBOARDING_CONNECTIVITY_SUCCESS_AUTO_DISMISS_MS);
}, [message]);

// Observe real relay connection state. When this card initiated a reconnect
// attempt (phase-3 path: reconnect() returns false) and the relay later
// becomes connected, mark success. Guard: only fire when we actually
// started a reconnect so a pre-existing connected state doesn't trigger it.
React.useEffect(() => {
if (relayConnectionState === "connected" && hadActiveReconnectRef.current) {
hadActiveReconnectRef.current = false;
markSuccess();
}
}, [relayConnectionState, markSuccess]);

const runConnectivityAction = React.useCallback(
(runAction: () => Promise<boolean | undefined>) => {
if (reconnectActionPendingRef.current) {
return;
}

hadActiveReconnectRef.current = true;
reconnectActionPendingRef.current = true;
setIsReconnectActionPending(true);
setHasSuccess(false);
void Promise.resolve()
.then(runAction)
.then((didReconnect) => {
if (didReconnect !== false) {
// Synchronous success (phase 1) — clear the ref and mark success
// immediately. The connection-state effect won't fire because the
// ref was just cleared.
hadActiveReconnectRef.current = false;
markSuccess();
}
// didReconnect === false means phase 3 is active; the
// connection-state effect will call markSuccess() when the relay
// becomes connected.
})
.catch((error) => {
hadActiveReconnectRef.current = false;
const detail = error instanceof Error ? error.message : String(error);
toast.error(`Could not reconnect to the relay. ${detail}`);
})
Expand All @@ -116,6 +145,7 @@ function OnboardingRelayConnectionErrorCard({
isActionDisabled={isActionPending}
isConnected={hasSuccess}
isReconnectPending={isActionPending}
isWaitingOnReconnectHook={isWaitingOnReconnectHook}
onDismiss={() => setDismissedErrorMessage(message)}
onReconnect={handleReconnectRelay}
surface="secondary"
Expand Down
3 changes: 3 additions & 0 deletions desktop/src/features/sidebar/ui/AppSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -772,6 +772,9 @@ export function AppSidebar({
isReconnectPending={
sidebarRelayConnectionCard.isRelayReconnectPending
}
isWaitingOnReconnectHook={
sidebarRelayConnectionCard.isWaitingOnReconnectHook
}
onDismiss={
sidebarRelayConnectionCard.onDismissRelayConnectionCard
}
Expand Down
15 changes: 13 additions & 2 deletions desktop/src/features/sidebar/ui/SidebarRelayConnectionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type SidebarRelayConnectionCardProps = {
className?: string;
isConnected?: boolean;
isReconnectPending: boolean;
isWaitingOnReconnectHook?: boolean;
onDismiss?: () => void;
onReconnect: () => void;
surface?: SidebarActionCardSurface;
Expand All @@ -24,6 +25,7 @@ export function SidebarRelayConnectionCard({
isActionDisabled = false,
isConnected = false,
isReconnectPending,
isWaitingOnReconnectHook = false,
onDismiss,
onReconnect,
surface,
Expand All @@ -35,6 +37,7 @@ export function SidebarRelayConnectionCard({
isActionDisabled={isActionDisabled}
isConnected={isConnected}
isReconnectPending={isReconnectPending}
isWaitingOnReconnectHook={isWaitingOnReconnectHook}
onDismiss={onDismiss}
onReconnect={onReconnect}
surface={surface}
Expand All @@ -49,11 +52,19 @@ export function SidebarRelayConnectionCompactCard({
isActionDisabled = false,
isConnected = false,
isReconnectPending,
isWaitingOnReconnectHook = false,
onDismiss,
onReconnect,
surface,
testId = "sidebar-relay-unreachable-compact",
}: SidebarRelayConnectionCardProps) {
const reconnectTitle = isWaitingOnReconnectHook
? "Waiting to reconnect"
: "Connecting";
const reconnectDescription = isWaitingOnReconnectHook
? "Complete any prompts opened by the reconnect helper to continue."
: "Reconnecting";

return (
<SidebarCompactActionCard
actionAriaLabel={isConnected ? "Connected" : "Connect to relay"}
Expand All @@ -63,7 +74,7 @@ export function SidebarRelayConnectionCompactCard({
isConnected
? undefined
: isReconnectPending
? "Reconnecting"
? reconnectDescription
: "Click to connect"
}
dismissLabel="Dismiss relay notification"
Expand All @@ -89,7 +100,7 @@ export function SidebarRelayConnectionCompactCard({
isConnected
? "Connected"
: isReconnectPending
? "Connecting"
? reconnectTitle
: "Can't reach the relay"
}
tone={isConnected ? "success" : "neutral"}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,18 @@ export function useSidebarRelayConnectionCard(
const [isWindowVisible, setIsWindowVisible] =
React.useState(isDocumentVisible);
const hasActiveRelayUnreachableError =
hasRelayUnreachableError && !hasSuccess;
hasRelayUnreachableError && !hasSuccess && !isRelayConnectionConnected;
const isRelayConnectionActuallyDegraded =
hasActiveRelayUnreachableError || isRelayConnectionStateDegraded;
const isRelayConnectionSuccess = hasSuccess && isRelayConnectionConnected;
const canShow = isRelayConnectionActuallyDegraded || isRelayConnectionSuccess;
const show = canShow && !isDismissed;
const wasProblemCardVisibleRef = React.useRef(false);
const { isPending: isReconnectPending, reconnect } = useReconnectRelay();
const {
isPending: isReconnectPending,
isWaitingOnReconnectHook,
reconnect,
} = useReconnectRelay();
const [connectivityAction, setConnectivityAction] = React.useState<
"relay-connection" | null
>(null);
Expand Down Expand Up @@ -212,6 +216,7 @@ export function useSidebarRelayConnectionCard(
hasRelayUnreachableError,
isRelayConnectionSuccess,
isRelayReconnectPending,
isWaitingOnReconnectHook,
onDismissRelayConnectionCard: () => {
setRelayConnectivitySuccess(relayUrl, false);
setIsDismissed(true);
Expand Down
Loading