/** * SettingsSurface — root settings surface * * Reads globals: * __SECTION__ — active section name (string) * __BASE__ — base path * __CONFIG_SECTIONS__ — v0.38.3: extension config sections (array|null) * * Policy flags (BYOK, personas) come from sw.auth.policies, * populated at SDK boot via GET /api/v1/profile/permissions. * * Layout: topbar + left nav + content area (same CSS classes as before). * All sections are native Preact components loaded lazily. */ const { html } = window; const { useState, useEffect, useCallback, useMemo } = hooks; const { render } = preact; import { ToastContainer } from '../../primitives/toast.js'; import { DialogStack } from '../../shell/dialog-stack.js'; // ── Lazy section imports ──────────────────── // Each returns a Promise<{ default: Component }> or { SectionName: Component } const sectionModules = { general: () => import('./general.js'), appearance: () => import('./appearance.js'), profile: () => import('./profile.js'), teams: () => import('./teams.js'), models: () => import('./models.js'), providers: () => import('./providers.js'), connections: () => import('./connections.js'), personas: () => import('./personas.js'), roles: () => import('./roles.js'), usage: () => import('./usage.js'), workflows: () => import('./workflows.js'), tasks: () => import('./tasks.js'), gitkeys: () => import('./gitkeys.js'), data: () => import('./data.js'), knowledge: () => import('./knowledge.js'), memory: () => import('./memory.js'), notifications: () => import('./notifications.js'), }; // ── Nav structure ─────────────────────────── const NAV_ITEMS = [ { key: 'general', label: 'General' }, { key: 'appearance', label: 'Appearance' }, { key: 'models', label: 'Models' }, { key: 'personas', label: 'Personas', gate: 'personas' }, { key: 'profile', label: 'Profile' }, { key: 'teams', label: 'Teams' }, { key: 'workflows', label: 'Assignments' }, { key: 'tasks', label: 'Tasks' }, { key: 'connections', label: 'Connections' }, { key: 'gitkeys', label: 'Git Keys' }, { key: 'knowledge', label: 'Knowledge Bases' }, { key: 'memory', label: 'Memory' }, { key: 'notifications', label: 'Notifications' }, { key: 'data', label: 'Data & Privacy' }, ]; const BYOK_ITEMS = [ { key: 'providers', label: 'My Providers' }, { key: 'roles', label: 'Model Roles' }, { key: 'usage', label: 'My Usage' }, ]; // ── Section title map ─────────────────────── const SECTION_TITLES = { general: 'General', appearance: 'Appearance', models: 'Models', personas: 'Personas', profile: 'Profile', teams: 'Teams', workflows: 'Assignments', tasks: 'Tasks', gitkeys: 'Git Keys', connections: 'Connections', data: 'Data & Privacy', providers: 'My Providers', roles: 'Model Roles', usage: 'My Usage', knowledge: 'Knowledge Bases', memory: 'Memory', notifications: 'Notifications', }; // ── v0.38.3: Extension config sections ────── // Packages declare config_section in their manifest. The backend passes // matching entries via __CONFIG_SECTIONS__. We merge them into the nav // and section module map for lazy loading. const _configSections = window.__CONFIG_SECTIONS__ || []; const EXT_NAV_ITEMS = _configSections.map(cs => ({ key: cs.package_id, label: cs.label })); const _base = window.__BASE__ || ''; for (const cs of _configSections) { const pkgId = cs.package_id; const component = cs.component || 'js/config.js'; sectionModules[pkgId] = () => import(`${_base}/surfaces/${pkgId}/${component}`); SECTION_TITLES[pkgId] = cs.label; } function SettingsSurface() { const BASE = window.__BASE__ || ''; const section = window.__SECTION__ || 'general'; const policies = sw.auth?.policies || {}; const [byokEnabled, setByokEnabled] = useState(policies.allow_user_byok === 'true'); const [personasEnabled, setPersonasEnabled] = useState(policies.allow_user_personas === 'true'); const [SectionComponent, setSectionComponent] = useState(null); // Update when permissions refresh (e.g. after boot or policy change) useEffect(() => { function _onPermsChanged() { const p = sw.auth?.policies || {}; setByokEnabled(p.allow_user_byok === 'true'); setPersonasEnabled(p.allow_user_personas === 'true'); } sw.on('auth.permissions.changed', _onPermsChanged); return () => sw.off('auth.permissions.changed', _onPermsChanged); }, []); // Load the section component useEffect(() => { setSectionComponent(null); if (sectionModules[section]) { sectionModules[section]().then(mod => { // Module exports the component as the first named export const Comp = mod.default || Object.values(mod)[0]; setSectionComponent(() => Comp); }).catch(e => { console.error('[settings] Failed to load section:', section, e); }); } // Bridge sections are handled inline }, [section]); const onNavClick = useCallback((e) => { e.preventDefault(); const href = e.currentTarget.getAttribute('href'); if (href) location.replace(href); }, []); const backClick = useCallback((e) => { e.preventDefault(); const RETURN_KEY = 'sb_settings_return'; const returnURL = sessionStorage.getItem(RETURN_KEY); sessionStorage.removeItem(RETURN_KEY); location.href = returnURL || BASE + '/'; }, []); // Stash return URL on first load useEffect(() => { const RETURN_KEY = 'sb_settings_return'; if (!sessionStorage.getItem(RETURN_KEY)) { const ref = document.referrer; if (ref && ref.startsWith(location.origin) && !ref.includes('/settings')) { sessionStorage.setItem(RETURN_KEY, ref); } else { sessionStorage.setItem(RETURN_KEY, location.origin + BASE + '/'); } } }, []); const title = SECTION_TITLES[section] || 'Settings'; return html`
${/* Top Bar */''}
Back
Settings
${/* Left Nav */''}
${NAV_ITEMS .filter(item => !item.gate || (item.gate === 'personas' && personasEnabled)) .map(item => html` ${item.label} `)} ${byokEnabled && html`
${BYOK_ITEMS.map(item => html` ${item.label} `)}
`} ${EXT_NAV_ITEMS.length > 0 && html`
Extensions
${EXT_NAV_ITEMS.map(item => html` ${item.label} `)}
`}
${/* Content */''}

${title}

${SectionComponent ? html`<${SectionComponent} />` : html`
Loading\u2026
` }
<${ToastContainer} /> <${DialogStack} /> `; } // ── Mount ──────────────────────────────────── const mount = document.getElementById('settings-mount'); if (mount) { render(html`<${SettingsSurface} />`, mount); console.log('[settings] Surface mounted'); } export { SettingsSurface };