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, demoMode, onToggleDemo, }: { page: Page; onPage: (p: Page) => void; onClose: () => void; theme: 'light' | 'dark'; onToggleTheme: () => void; clis: Cli[]; info: Info | null; onShowWelcome?: () => void; demoMode?: boolean; onToggleDemo?: () => 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]); // Operator config (artifacts hub, jobs policy): load once, autosave on edit. const [cfg, setCfg] = useState(null); const [savedCfg, setSavedCfg] = useState(''); const [cfgSaved, setCfgSaved] = useState<'idle' | 'saving' | 'saved'>('idle'); useEffect(() => { api.getConfig().then((c) => { setCfg(c); setSavedCfg(JSON.stringify(c)); }).catch(() => {}); }, []); useEffect(() => { if (!cfg || JSON.stringify(cfg) === savedCfg) return; const t = setTimeout(async () => { setCfgSaved('saving'); try { await api.saveConfig(cfg); setSavedCfg(JSON.stringify(cfg)); setCfgSaved('saved'); setTimeout(() => setCfgSaved('idle'), 1800); } catch { setCfgSaved('idle'); } }, 900); return () => clearTimeout(t); }, [cfg, savedCfg]); 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.
)} {onToggleDemo && (
Demo mode{demoMode && on}
Hides your current sessions from the sidebar so the Space looks fresh. Nothing is deleted, logins and secrets stay valid, and anything you start while it's on stays visible. Turn it off to bring everything back.
)}

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}