feat(ui): Settings view + per-space project cards (status/research/edit/delete) + theming pass
- Settings (#/settings): API tokens (mint/list/revoke), Agents list, Orthos Mode placeholder - Per-space Projects: Void-1-style expandable cards — inline status, ↻ Research (Eithan stub), Edit/New modal, Delete-with-confirm; migration 019 adds research_status/notes/timestamps; POST /api/projects/:id/research stub; GET /api/agent-tokens list - Global +1 font bump; themed scrollbars; larger/bolder themed topbar Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
81
public/views/settings.js
Normal file
81
public/views/settings.js
Normal file
@@ -0,0 +1,81 @@
|
||||
// #/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();
|
||||
}
|
||||
|
||||
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 scope = a.scopes?.space_id ? 'space-scoped' : '';
|
||||
c.appendChild(el('div', { class: 'settings-row' },
|
||||
el('span', { class: 'settings-label' }, a.name),
|
||||
el('span', { class: 'settings-value muted' }, `${a.slug} · ${a.kind} · caps: ${caps}${scope ? ' · ' + scope : ''}`)));
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user