Files
Void-Homelab/public/views/settings.js
root bf58b624a3 feat(ui): hybrid sidebar (sectioned + active pill + agent dots) + agent profile viewer in Settings
Sidebar: Spaces / Agents / Navigate sections, accent pill on active item, status
dots on agents. Settings Agents rows expand to show the agent's persona (soul) +
capabilities/scopes via GET /api/agents/:id/profile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 00:11:14 +10:00

112 lines
5.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// #/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 wont 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);
}