import { useTranslation } from "@browser-skill/i18n/react"; import { Badge, Button, Input, Label } from "@browser-skill/ui"; import { RiArrowLeftLine, RiArrowRightSLine, RiCheckLine, RiFileCopyLine, RiInformationLine, } from "@remixicon/react"; import { type ChangeEvent, useEffect, useState } from "react"; import { PROTOCOL_VERSION } from "@/transport/handshake"; import functionIconUrl from "../../../assets/function.svg"; import { ConnectionStatusIndicator } from "./connection-status-indicator"; import { POPUP_FEATURES, type PopupView } from "./features"; import { Switch } from "./switch"; import { type PopupStatusState, useConnectionState } from "./use-connection-state"; import { useControlHintsHidden } from "./use-control-hints-hidden"; const STATE_LABEL_KEYS = { disconnected: "popup.stateLabel.disconnected", connected: "popup.stateLabel.connected", version_skew: "popup.stateLabel.version_skew", disabled: "popup.stateLabel.disabled", } as const satisfies Record; const STATE_BADGE_KEYS = { disconnected: "popup.stateBadge.disconnected", connected: "popup.stateBadge.connected", version_skew: "popup.stateBadge.version_skew", disabled: "popup.stateBadge.disabled", } as const satisfies Record; function getLogoSrc() { if (typeof chrome !== "undefined" && chrome.runtime?.getURL) { return chrome.runtime.getURL("icon/logo.png"); } return "/icon/logo.png"; } export function App() { const { t } = useTranslation("extension"); const { snapshot, statusState, setConnectionEnabled } = useConnectionState(); const [controlHintsHidden, setControlHintsHidden] = useControlHintsHidden(); const [view, setView] = useState("main"); const [copiedInstanceId, setCopiedInstanceId] = useState(false); const [purposeDraft, setPurposeDraft] = useState(""); const [startUrlDraft, setStartUrlDraft] = useState(""); // Bumped on every successful copy so the "copied" toast re-shows (and its // auto-hide timer restarts) even when the copied content is unchanged. const [copiedTick, setCopiedTick] = useState(0); const showCopiedToast = copiedTick > 0; useEffect(() => { const query = window.matchMedia("(prefers-color-scheme: dark)"); const applyTheme = (isDark: boolean) => { document.documentElement.classList.toggle("dark", isDark); }; applyTheme(query.matches); const onChange = (event: MediaQueryListEvent) => applyTheme(event.matches); query.addEventListener("change", onChange); return () => query.removeEventListener("change", onChange); }, []); useEffect(() => { setCopiedInstanceId(false); setCopiedTick(0); }, [snapshot.instanceId]); // Hide the copied toast when the command changes (topic / start URL edits). useEffect(() => { setCopiedTick(0); }, [purposeDraft, startUrlDraft]); // Auto-hide the copied toast shortly after it appears. useEffect(() => { if (copiedTick === 0) return; const timer = window.setTimeout(() => setCopiedTick(0), 1500); return () => window.clearTimeout(timer); }, [copiedTick]); const isSkewed = statusState === "version_skew"; const connectionLive = statusState === "connected" || isSkewed; const daemonVersion = snapshot.handshake?.version ?? "—"; const daemonProtocol = snapshot.handshake?.protocol_version ?? "—"; const extensionVersion = snapshot.extensionVersion || "—"; const instanceId = snapshot.instanceId || "—"; const copyInstanceId = async () => { if (!snapshot.instanceId || !navigator.clipboard?.writeText) return; await navigator.clipboard.writeText(snapshot.instanceId); setCopiedInstanceId(true); }; const recordReady = connectionLive && Boolean(snapshot.instanceId); const recordPurpose = purposeDraft.trim(); const recordStartUrl = startUrlDraft.trim(); const recordCommand = snapshot.instanceId ? [ `bsk record start --browser ${snapshot.instanceId}`, recordStartUrl ? `--url ${recordStartUrl}` : "", recordPurpose ? `--purpose "${recordPurpose}"` : "", ] .filter(Boolean) .join(" ") : ""; const recordPrompt = snapshot.instanceId ? t("popup.record.promptTemplate", { command: recordCommand, }) : ""; const copyRecordPrompt = async () => { if (!recordReady || !navigator.clipboard?.writeText) return; await navigator.clipboard.writeText(recordPrompt); setCopiedTick((tick) => tick + 1); }; const headerTitle = view === "features" ? t("popup.launcher.title") : view === "record" ? t("popup.record.sectionTitle") : t("popup.brandName"); return (
{view !== "main" ? ( ) : ( )}

{headerTitle}

{view === "main" && ( )}
{view === "main" && ( <>
{t(STATE_LABEL_KEYS[statusState])}
{t(STATE_BADGE_KEYS[statusState])}
{isSkewed && (

{t("popup.versionSkewWarning", { extensionProtocol: PROTOCOL_VERSION, cliProtocol: daemonProtocol, })}

)}
{t("popup.controlHintsToggleTitle")} {t("popup.controlHintsToggleHint")} setControlHintsHidden(!shown)} aria-label={t("popup.controlHintsToggleTitle")} data-slot="popup-control-hints-toggle" />
{snapshot.lastError && (
{snapshot.lastError}
)}
CLI {daemonVersion} / Ext {extensionVersion}
{t("popup.instanceTitle")} {instanceId}
)} {view === "features" && (
{POPUP_FEATURES.map((feature) => { const Icon = feature.icon; return ( ); })}
)} {view === "record" && (

{t("popup.record.sectionHint")}

) => setPurposeDraft(event.target.value) } placeholder={t("popup.record.topicPlaceholder")} className="mt-0 h-8 text-sm" data-slot="popup-record-purpose-input" />
) => setStartUrlDraft(event.target.value) } placeholder={t("popup.record.startUrlPlaceholder")} className="mt-0 h-8 text-sm" data-slot="popup-record-start-url-input" />