import { useEffect, useState } from 'react'; import type { Cli } from '../types'; import * as api from '../api'; import SkillsEditor from './SkillsEditor'; import UsagePanel from './UsagePanel'; import { SunGlyph, MoonGlyph, RefreshGlyph, InfoGlyph } from './icons'; import Logo from './Logo'; type Page = 'general' | 'usage' | 'skills'; const PAGES: { id: Page; label: string }[] = [ { id: 'general', label: 'General' }, { id: 'usage', label: 'Usage' }, { id: 'skills', label: 'Skills' }, ]; interface Info { dataDir?: string; home?: string; spaceId?: string | null; spaceHost?: string | null; tmux?: boolean; canRelaunch?: boolean; secrets?: string[]; bucketUnverified?: boolean; } function urlBase64ToUint8Array(base64: string) { const padding = '='.repeat((4 - (base64.length % 4)) % 4); const raw = atob((base64 + padding).replace(/-/g, '+').replace(/_/g, '/')); return Uint8Array.from([...raw].map((c) => c.charCodeAt(0))); } type PushState = 'checking' | 'unsupported' | 'framed' | 'denied' | 'off' | 'on' | 'busy'; // "Enable notifications" for THIS device: agents can then message the operator // (only when explicitly asked to — see the environment skill). function PushRow() { const [state, setState] = useState('checking'); const [note, setNote] = useState(''); useEffect(() => { (async () => { // Inside the huggingface.co iframe the browser blocks notification // permission + push — only the Space's direct URL works. if (window.top !== window.self) return setState('framed'); if (!('serviceWorker' in navigator) || !('PushManager' in window)) return setState('unsupported'); if (Notification.permission === 'denied') return setState('denied'); try { // `ready` can hang in odd contexts — don't leave the row stuck forever. const reg = await Promise.race([ navigator.serviceWorker.ready, new Promise((r) => setTimeout(() => r(null), 4000)), ]); if (!reg) return setState('unsupported'); setState((await reg.pushManager.getSubscription()) ? 'on' : 'off'); } catch { setState('unsupported'); } })(); }, []); const enable = async () => { setState('busy'); setNote(''); try { const reg = await navigator.serviceWorker.ready; const perm = await Notification.requestPermission(); if (perm !== 'granted') { setState(perm === 'denied' ? 'denied' : 'off'); return; } const { publicKey } = await api.getPushKey(); const sub = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(publicKey) }); await api.subscribePush(sub); setState('on'); setNote('This device will receive agent notifications.'); } catch { setState('off'); setNote('Could not subscribe — on iPhone, add the app to your Home Screen first.'); } setTimeout(() => setNote(''), 6000); }; const disable = async () => { setState('busy'); try { const reg = await navigator.serviceWorker.ready; const sub = await reg.pushManager.getSubscription(); if (sub) { await api.unsubscribePush(sub.endpoint); await sub.unsubscribe(); } } catch { /* ignore */ } setState('off'); }; const test = async () => { setNote(''); try { const r = await api.sendTestNotification(); setNote(r.sent > 0 ? 'Sent — check your notifications.' : 'No delivery — is this device subscribed?'); } catch { setNote('Failed to send.'); } setTimeout(() => setNote(''), 6000); }; return ( <>
Notifications
Agents can message this device when you explicitly ask them to ("notify me when the tests pass"). On iPhone: add the app to your Home Screen first, then enable here.
{note &&
{note}
}
{state === 'framed' ? ( Open directly to enable ↗ ) : state === 'unsupported' ? ( not supported here ) : state === 'denied' ? ( blocked in browser settings ) : state === 'on' ? ( ) : ( )}
); } export default function SettingsView({ page, onPage, onClose, theme, onToggleTheme, clis, info, onShowWelcome, }: { page: Page; onPage: (p: Page) => void; onClose: () => void; theme: 'light' | 'dark'; onToggleTheme: () => void; clis: Cli[]; info: Info | null; onShowWelcome?: () => void; }) { const [relaunch, setRelaunch] = useState<{ busy?: boolean; msg?: string; confirm?: boolean }>({}); const [secretKeys, setSecretKeys] = useState([]); const [notes, setNotes] = useState>({}); const [savedNotes, setSavedNotes] = useState>({}); const [secretsSaved, setSecretsSaved] = useState<'idle' | 'saving' | 'saved'>('idle'); useEffect(() => { api.getSecrets().then((d) => { setSecretKeys(d.detected); setNotes(d.notes || {}); setSavedNotes(d.notes || {}); }).catch(() => {}); }, []); const notesDirty = secretKeys.some((k) => (notes[k] || '') !== (savedNotes[k] || '')); // One-line textareas that grow with their content. const grow = (el: HTMLTextAreaElement) => { el.style.height = 'auto'; el.style.height = `${el.scrollHeight}px`; }; const growRef = (el: HTMLTextAreaElement | null) => { if (el) grow(el); }; useEffect(() => { document.querySelectorAll('textarea.secret-desc').forEach(grow); }, [secretKeys]); // Autosave: settle for a moment after the last keystroke, then persist. useEffect(() => { if (!notesDirty) return; const t = setTimeout(async () => { setSecretsSaved('saving'); try { await api.saveSecrets(notes); setSavedNotes({ ...notes }); setSecretsSaved('saved'); setTimeout(() => setSecretsSaved('idle'), 1800); } catch { setSecretsSaved('idle'); } }, 900); return () => clearTimeout(t); }, [notes, notesDirty]); const doRelaunch = async () => { setRelaunch({ busy: true }); try { const r = await api.relaunchSpace(); if (r.ok) setRelaunch({ msg: 'Rebuilding — the Space will restart in ~1–2 min. Reload once it\'s back.' }); else if (r.reason === 'no-token') setRelaunch({ msg: 'Add an HF_TOKEN secret (write access) to the Space to enable one-click relaunch, or use ⋮ → Factory reboot.' }); else setRelaunch({ msg: `Couldn't relaunch (${r.reason}).` }); } catch { setRelaunch({ msg: 'Request failed.' }); } }; return (
{page === 'general' && (

General

Theme
Defaults to your system setting.
{onShowWelcome && (
Welcome guide
A quick tour of how the tool works.
)}

Agents

A coloured dot means the agent is configured and ready. Log in once inside a session (or set a key as a Space secret) — credentials persist on the bucket across restarts and all sessions.
{clis.filter((c) => c.id !== 'shell' && c.id !== 'files').map((c) => { const ready = c.available && c.ready; return (
{c.label} {c.available && c.version && v{c.version}} {!c.available ? 'unavailable' : ready ? 'ready' : 'needs setup'} {/* fixed slot so version/state columns align across rows */} {c.available && !ready && c.setup && ( )}
); })}

Secrets & variables{secretsSaved !== 'idle' && {secretsSaved === 'saving' ? 'saving…' : 'saved ✓'}}

Detected by diffing the runtime environment against a build-time snapshot — names only, never values. Describe what each is for (saved automatically); this publishes an environment skill so every agent knows what's available.
{secretKeys.length === 0 ? (
None detected.
) : (
{secretKeys.map((k) => (
{k}