// #/settings — API tokens, agents, and a placeholder for Orthos Mode. import { el, mount } from '../dom.js'; import { api } from '../api.js'; function section(title, sub, bodyEl) { return el('div', { class: 'card settings-card' }, el('h3', {}, title), sub ? el('p', { class: 'settings-sub muted' }, sub) : null, bodyEl); } async function renderTokens(c) { c.replaceChildren(el('div', { class: 'muted' }, 'Loading…')); let tokens = [], agents = []; try { tokens = await api.get('/api/agent-tokens'); } catch { /* */ } try { agents = await api.get('/api/agents'); } catch { /* */ } c.replaceChildren(); const sel = el('select', { class: 'pm-input', style: { maxWidth: '210px' } }, agents.map(a => el('option', { value: a.id }, `${a.name} (${a.slug})`))); const label = el('input', { class: 'pm-input', placeholder: 'label (optional)', style: { maxWidth: '170px' } }); const out = el('div', { class: 'token-out' }); const mint = el('button', { class: 'primary', onclick: async () => { if (!sel.value) { out.textContent = 'No agents available'; return; } try { const r = await api.post('/api/agents/' + sel.value + '/tokens', { label: label.value.trim() || undefined }); out.replaceChildren(el('div', { class: 'token-reveal' }, el('span', {}, 'New token — copy it now, it won’t be shown again: '), el('code', {}, r.token))); const list = await api.get('/api/agent-tokens'); tokens = list; paint(); } catch (e) { out.textContent = 'failed: ' + e.message; } } }, 'Mint token'); const listWrap = el('div', {}); function paint() { listWrap.replaceChildren(); if (!tokens.length) { listWrap.appendChild(el('div', { class: 'muted' }, 'No tokens yet.')); return; } for (const t of tokens) { listWrap.appendChild(el('div', { class: 'settings-row' + (t.revoked_at ? ' revoked' : '') }, el('span', { class: 'settings-label' }, t.agent_name || t.agent_slug), el('span', { class: 'settings-value muted' }, `${t.label || '—'} · ${t.last_used ? 'used ' + new Date(t.last_used).toLocaleDateString() : 'never used'}`), t.revoked_at ? el('span', { class: 'muted' }, 'revoked') : el('button', { class: 'proj-btn danger', onclick: async () => { if (!confirm('Revoke this token?')) return; try { await api.del('/api/agent-tokens/' + t.id); renderTokens(c); } catch (e) { alert(e.message); } } }, 'Revoke'))); } } c.appendChild(el('div', { class: 'settings-row settings-create' }, sel, label, mint)); c.appendChild(out); c.appendChild(listWrap); paint(); } function fileBlock(parent, label, content) { parent.appendChild(el('div', { class: 'agent-file-label' }, label)); parent.appendChild(el('div', { class: 'agent-file-content' }, content)); } async function renderAgents(c) { c.replaceChildren(el('div', { class: 'muted' }, 'Loading…')); let agents = []; try { agents = await api.get('/api/agents'); } catch { /* */ } c.replaceChildren(); if (!agents.length) { c.appendChild(el('div', { class: 'muted' }, 'No agents.')); return; } for (const a of agents) { const caps = Object.entries(a.capabilities || {}).filter(([, v]) => v).map(([k]) => k).join(', ') || '—'; const row = el('div', { class: 'agent-row' }); const body = el('div', { class: 'agent-body hidden' }); let loaded = false; const hd = el('div', { class: 'agent-row-hd', onclick: async () => { const open = row.classList.toggle('open'); body.classList.toggle('hidden', !open); if (open && !loaded) { loaded = true; body.replaceChildren(el('div', { class: 'muted' }, 'Loading…')); try { const p = await api.get('/api/agents/' + a.id + '/profile'); body.replaceChildren(); if (p.persona) fileBlock(body, 'Soul · persona', p.persona); else body.appendChild(el('div', { class: 'muted' }, 'No persona defined (config-only agent).')); fileBlock(body, 'Capabilities', JSON.stringify(p.capabilities || {}, null, 2)); if (p.scopes && Object.keys(p.scopes).length) fileBlock(body, 'Scopes', JSON.stringify(p.scopes, null, 2)); body.appendChild(el('div', { class: 'muted', style: { fontSize: '11px', marginTop: '8px' } }, 'Void 2 agents are persona-in-code + DB config — no separate memory files (unlike Void 1).')); } catch (e) { body.replaceChildren(el('div', { class: 'err' }, 'Error: ' + e.message)); } } } }, el('span', { class: 'agent-nm' }, a.name), el('span', { class: 'settings-value muted' }, `${a.slug} · ${a.kind} · ${caps}`), el('span', { class: 'agent-row-chev' }, '›')); row.append(hd, body); c.appendChild(row); } } export async function render(main) { const tokensBody = el('div', { class: 'settings-body' }); const agentsBody = el('div', { class: 'settings-body' }); mount(main, el('h1', { class: 'view-h1' }, '◆ Settings'), section('API Tokens', 'Bearer tokens for agents (e.g. the MCP external-research agent). The secret is shown once at creation.', tokensBody), section('Agents', 'Seeded Cradle agents and what each is allowed to do.', agentsBody), section('Orthos Mode', 'Local-first answering — Orthos answers first, Claude escalates when needed.', el('div', { class: 'muted' }, 'Paused for a future project (arrives with the local-agent layer).')) ); renderTokens(tokensBody); renderAgents(agentsBody); }