diff --git a/AGENTS.md b/AGENTS.md
index edda9d5..7eeef24 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -574,6 +574,14 @@ session with **no other memory of this project**. So:
calls are suppressed so partial JSON never renders. Pollinations' `mistral`
slug is dead (404); only `openai` (gpt-oss-20b) is live. `findCoaches` +
`getCommunityStats` cover **all** Lab roles (kinds 35001–35014).
+ **Bubble visibility (2026-10-04):** the floating Sasquatch FAB no longer
+ pulses forever — the `animate-ping` halo shows **only when an unread tip
+ exists**, is `motion-reduce:hidden`, and is suppressed on **full-bleed routes**
+ (`isFullBleed` passed from `Layout`), where the bubble also runs **compact**
+ (smaller, no glow) so it stops competing with the map. A ✕ on the FAB hides
+ it (`useSasquatchHidden`, localStorage `sasquatch-hidden`, same-tab synced via
+ a `sasquatch:hiddenchange` event), and **Settings → Appearance** has a "Show
+ Sasquatch assistant" toggle to bring it back.
- **ContextVM (CVM) server** (`cvm-server/`, Bun): standalone MCP-over-Nostr
endpoint for *external* agents. Runs on this machine as a **system** service
(`/etc/systemd/system/skatespots-cvm.service`, `sudo systemctl restart
diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx
index 938f0b7..4a2fa4d 100644
--- a/src/components/Layout.tsx
+++ b/src/components/Layout.tsx
@@ -1136,6 +1136,7 @@ export function Layout({ children, noPadding }: LayoutProps) {
diff --git a/src/components/SasquatchGuide.tsx b/src/components/SasquatchGuide.tsx
index f8a456a..5185f3b 100644
--- a/src/components/SasquatchGuide.tsx
+++ b/src/components/SasquatchGuide.tsx
@@ -14,6 +14,7 @@ import { Input } from '@/components/ui/input';
import { ScrollArea } from '@/components/ui/scroll-area';
import { X, Minimize2, HelpCircle, Send, Loader2, Wifi, WifiOff } from 'lucide-react';
import { useLocalStorage } from '@/hooks/useLocalStorage';
+import { useSasquatchHidden } from '@/hooks/useSasquatchHidden';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useCVMClient, type SpotResult } from '@/hooks/useCVMClient';
import { usePersonalContext } from '@/hooks/usePersonalContext';
@@ -116,6 +117,8 @@ export interface GuideMessage {
interface SasquatchGuideProps {
message?: GuideMessage;
onDismiss?: () => void;
+ /** True on full-bleed routes (the map) — the bubble runs compact/no-pulse. */
+ isFullBleed?: boolean;
}
interface ChatMessage {
@@ -658,7 +661,7 @@ function ChatText({ text }: { text: string }) {
// ─── Main component ───────────────────────────────────────────────────────────
-export function SasquatchGuide({ message, onDismiss }: SasquatchGuideProps) {
+export function SasquatchGuide({ message, onDismiss, isFullBleed }: SasquatchGuideProps) {
const { user } = useCurrentUser();
const { t, language } = useTranslation();
const cvm = useCVMClient();
@@ -675,6 +678,7 @@ export function SasquatchGuide({ message, onDismiss }: SasquatchGuideProps) {
const [proDialogOpen, setProDialogOpen] = useState(false);
const [isMinimized, setIsMinimized] = useLocalStorage('sasquatch-minimized', false);
+ const [hidden, setHidden] = useSasquatchHidden();
const [isVisible, setIsVisible] = useState(false);
// Fetched only while the panel is open (owner-only).
const aiStatus = useSasquatchAiStatus({ enabled: isOwner && isVisible && !isMinimized });
@@ -1545,6 +1549,11 @@ Instructions:
// opt-in auto-show message (e.g. onboarding) — never automatically on / or /lab.
const shouldShowPanel = isVisible;
+ // A proactive tip is waiting (drives the pulse/dot), and the map route runs
+ // the bubble in a quieter, compact form.
+ const hasUnread = !!message;
+ const compact = !!isFullBleed;
+
// Notify the rest of the app (e.g. MapPage FAB) when the panel opens or closes
useEffect(() => {
const panelOpen = shouldShowPanel && !isMinimized;
@@ -1574,10 +1583,12 @@ Instructions:
enableChat: true,
};
+ if (hidden) return null;
+
return (
<>
- {/* ── Minimized bubble — always visible, draggable ── */}
- {(!shouldShowPanel || isMinimized) && (
+ {/* ── Minimized bubble — visible unless the user hid the assistant ── */}
+ {!hidden && (!shouldShowPanel || isMinimized) && (
-
-
+ {/* Pulse only when there's an unread tip, never on the map, and never
+ under prefers-reduced-motion (the mount animation is the nuisance). */}
+ {hasUnread && !compact && (
+
+ )}
+ {hasUnread && (
+
+ )}
+
)}
diff --git a/src/hooks/useSasquatchHidden.ts b/src/hooks/useSasquatchHidden.ts
new file mode 100644
index 0000000..a6b7d09
--- /dev/null
+++ b/src/hooks/useSasquatchHidden.ts
@@ -0,0 +1,31 @@
+import { useCallback, useEffect } from 'react';
+import { useLocalStorage } from '@/hooks/useLocalStorage';
+
+const KEY = 'sasquatch-hidden';
+const EVENT = 'sasquatch:hiddenchange';
+
+/**
+ * Whether the floating Sasquatch assistant is hidden. Device-local (like the
+ * other visibility prefs) and synced **within the same tab** via a custom event
+ * — `useLocalStorage` only syncs across tabs, so the Settings toggle and the
+ * guide itself need this bridge to stay in step.
+ */
+export function useSasquatchHidden(): [boolean, (value: boolean) => void] {
+ const [hidden, setHiddenRaw] = useLocalStorage(KEY, false);
+
+ useEffect(() => {
+ const handler = (e: Event) => setHiddenRaw(!!(e as CustomEvent).detail);
+ window.addEventListener(EVENT, handler);
+ return () => window.removeEventListener(EVENT, handler);
+ }, [setHiddenRaw]);
+
+ const setHidden = useCallback(
+ (value: boolean) => {
+ setHiddenRaw(value);
+ window.dispatchEvent(new CustomEvent(EVENT, { detail: value }));
+ },
+ [setHiddenRaw],
+ );
+
+ return [hidden, setHidden];
+}
diff --git a/src/i18n/batch3/sasquatchUi.ts b/src/i18n/batch3/sasquatchUi.ts
index 51ef174..ad6b1be 100644
--- a/src/i18n/batch3/sasquatchUi.ts
+++ b/src/i18n/batch3/sasquatchUi.ts
@@ -29,6 +29,7 @@ const sasquatchUi = {
// ── Bubble + header ──
bubbleTitle: 'Press and drag to move · Tap to open',
openGuideAria: 'Open Sasquatch guide',
+ hideAria: 'Hide the Sasquatch assistant',
activeUntil: 'Active until {{date}}',
platformOwner: 'Platform owner',
upgradeTitle: 'Upgrade to Sasquatch Pro',
@@ -160,6 +161,7 @@ const sasquatchUi = {
// ── Bubble + header ──
bubbleTitle: 'Pulsa y arrastra para mover · Toca para abrir',
openGuideAria: 'Abrir la guía del Sasquatch',
+ hideAria: 'Ocultar el asistente Sasquatch',
activeUntil: 'Activo hasta el {{date}}',
platformOwner: 'Propietario de la plataforma',
upgradeTitle: 'Mejora a Sasquatch Pro',
diff --git a/src/i18n/en.ts b/src/i18n/en.ts
index 0da425e..dca6179 100644
--- a/src/i18n/en.ts
+++ b/src/i18n/en.ts
@@ -5072,6 +5072,9 @@ settingsUi: {
myStashSub: 'Private gear inventory',
relaysSub: 'Nostr relay connections',
appearanceSub: 'Language and theme',
+ assistant: 'Assistant',
+ showSasquatch: 'Show Sasquatch assistant',
+ showSasquatchDesc: 'Show the floating Sasquatch button on the map and other screens.',
notifications: 'Notifications',
notificationsSub: 'Alerts for DMs, crews and zaps',
coordinator: 'Coordinator',
diff --git a/src/i18n/es.ts b/src/i18n/es.ts
index bb69d30..23bc589 100644
--- a/src/i18n/es.ts
+++ b/src/i18n/es.ts
@@ -5072,6 +5072,9 @@ settingsUi: {
myStashSub: 'Inventario privado de material',
relaysSub: 'Conexiones a relays de Nostr',
appearanceSub: 'Idioma y tema',
+ assistant: 'Asistente',
+ showSasquatch: 'Mostrar el asistente Sasquatch',
+ showSasquatchDesc: 'Muestra el botón flotante del Sasquatch en el mapa y otras pantallas.',
notifications: 'Notificaciones',
notificationsSub: 'Avisos de MD, crews y zaps',
coordinator: 'Coordinador',
diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx
index b772eff..64a51c7 100644
--- a/src/pages/SettingsPage.tsx
+++ b/src/pages/SettingsPage.tsx
@@ -1,6 +1,8 @@
import { useSeoMeta } from '@unhead/react';
import { Link, useSearchParams } from 'react-router-dom';
import { EditProfileForm } from '@/components/EditProfileForm';
+import { Switch } from '@/components/ui/switch';
+import { useSasquatchHidden } from '@/hooks/useSasquatchHidden';
import { SkaterDirectoryCard } from '@/components/SkaterDirectoryCard';
import { ProfilePreviewCard } from '@/components/ProfilePreviewCard';
import { RelayListManager } from '@/components/RelayListManager';
@@ -609,6 +611,7 @@ function RelaysScreen({ onBack }: { onBack: () => void }) {
function AppearanceScreen({ onBack }: { onBack: () => void }) {
const { t } = useTranslation();
+ const [sasquatchHidden, setSasquatchHidden] = useSasquatchHidden();
return (