diff --git a/packages/app/src/hooks/use-favicon-status.ts b/packages/app/src/hooks/use-favicon-status.ts new file mode 100644 index 000000000..a1dec97d2 --- /dev/null +++ b/packages/app/src/hooks/use-favicon-status.ts @@ -0,0 +1,98 @@ +import { useEffect, useState } from "react"; +import { Platform } from "react-native"; +import { useAggregatedAgents } from "./use-aggregated-agents"; + +type FaviconStatus = "none" | "running" | "attention"; +type ColorScheme = "dark" | "light"; + +/* eslint-disable @typescript-eslint/no-require-imports */ +const FAVICON_IMAGES: Record> = { + dark: { + none: require("../../assets/images/favicon-dark.png"), + running: require("../../assets/images/favicon-dark-running.png"), + attention: require("../../assets/images/favicon-dark-attention.png"), + }, + light: { + none: require("../../assets/images/favicon-light.png"), + running: require("../../assets/images/favicon-light-running.png"), + attention: require("../../assets/images/favicon-light-attention.png"), + }, +}; +/* eslint-enable @typescript-eslint/no-require-imports */ + +function deriveFaviconStatus( + agents: ReturnType["agents"] +): FaviconStatus { + const hasRunning = agents.some((agent) => agent.status === "running"); + if (hasRunning) { + return "running"; + } + const hasAttention = agents.some((agent) => agent.requiresAttention); + if (hasAttention) { + return "attention"; + } + return "none"; +} + +function getFaviconUri(status: FaviconStatus, colorScheme: ColorScheme): string { + const image = FAVICON_IMAGES[colorScheme][status]; + if (typeof image === "object" && "uri" in image) { + return image.uri; + } + const suffix = status === "none" ? "" : `-${status}`; + return `/assets/images/favicon-${colorScheme}${suffix}.png`; +} + +function getOrCreateFaviconLink(): HTMLLinkElement | null { + if (typeof document === "undefined") return null; + + let link = document.querySelector('link[rel="icon"]'); + if (!link) { + link = document.createElement("link"); + link.rel = "icon"; + link.type = "image/png"; + document.head.appendChild(link); + } + return link; +} + +function updateFavicon(status: FaviconStatus, colorScheme: ColorScheme) { + const link = getOrCreateFaviconLink(); + if (!link) return; + + const newHref = getFaviconUri(status, colorScheme); + if (link.href !== newHref) { + link.href = newHref; + } +} + +function getSystemColorScheme(): ColorScheme { + if (typeof window === "undefined") return "dark"; + return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; +} + +export function useFaviconStatus() { + const { agents } = useAggregatedAgents(); + const [colorScheme, setColorScheme] = useState(getSystemColorScheme); + + // Listen for system color scheme changes + useEffect(() => { + if (Platform.OS !== "web" || typeof window === "undefined") return; + + const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); + const handler = (e: MediaQueryListEvent) => { + setColorScheme(e.matches ? "dark" : "light"); + }; + + mediaQuery.addEventListener("change", handler); + return () => mediaQuery.removeEventListener("change", handler); + }, []); + + // Update favicon when agents or color scheme changes + useEffect(() => { + if (Platform.OS !== "web") return; + + const status = deriveFaviconStatus(agents); + updateFavicon(status, colorScheme); + }, [agents, colorScheme]); +}