// #/little-blue — the caretaker page: Little Blue's chat (reuses agent_chat) + // a manual Actions panel (run whitelisted actions; approve/reject risky ones). import { el, mount, clear } from '../dom.js'; import { api } from '../api.js'; import { wireAgentChat } from '../components/agent_chat.js'; const BLUE_LABELS = { search: '🔍 looking', list_actions: '📋 checking what she can do', propose_action: '🔧 taking action' }; function toast(host, msg, kind) { const t = el('div', { class: 'lb-toast' + (kind === 'err' ? ' err' : '') }, msg); host.appendChild(t); setTimeout(() => t.remove(), 4000); } async function renderActions(panel, toastHost) { clear(panel); let actions = [], pending = []; try { ({ actions } = await api.get('/api/actions')); } catch (e) { actions = []; } try { ({ pending } = await api.get('/api/actions/pending')); } catch (e) { pending = []; } panel.appendChild(el('div', { class: 'lb-sec-t' }, 'Actions')); if (!actions.length) { panel.appendChild(el('div', { class: 'muted' }, 'No actions configured yet (populate config/actions.json).')); } for (const a of actions) { panel.appendChild(el('div', { class: 'lb-action' }, el('span', { class: 'lb-a-label' }, a.label || a.id), el('span', { class: 'lb-a-tier ' + a.tier }, a.tier), el('button', { class: 'lb-run', onclick: async (ev) => { ev.target.disabled = true; try { const r = await api.post(`/api/actions/${a.id}/run`); toast(toastHost, r.executed ? `Ran "${a.label || a.id}"` : `Queued "${a.label || a.id}" for approval`); renderActions(panel, toastHost); } catch (e) { toast(toastHost, 'Failed: ' + e.message, 'err'); ev.target.disabled = false; } } }, a.tier === 'risky' ? 'Request' : 'Run'))); } if (pending.length) { panel.appendChild(el('div', { class: 'lb-sec-t' }, 'Awaiting approval')); for (const p of pending) { const row = el('div', { class: 'lb-pending' }, el('span', { class: 'lb-a-label' }, p.action_id), el('button', { class: 'ok', onclick: () => resolve(p.id, 'approve') }, 'Approve'), el('button', { class: 'no', onclick: () => resolve(p.id, 'reject') }, 'Reject')); panel.appendChild(row); async function resolve(id, verb) { try { await api.post(`/api/actions/pending/${id}/${verb}`); toast(toastHost, verb === 'approve' ? 'Approved' : 'Rejected'); renderActions(panel, toastHost); } catch (e) { toast(toastHost, 'Failed: ' + e.message, 'err'); } } } } } export async function render(main) { const log = el('div', { class: 'rail-log sentinel-log' }); const input = el('textarea', { class: 'rail-input', rows: 1, placeholder: 'Tell Little Blue what’s wrong…' }); const actionsPanel = el('div', { class: 'lb-actions' }); const toastHost = el('div', { class: 'lb-toasts' }); mount(main, el('h1', { class: 'view-h1' }, '◆ Little Blue — Caretaker'), el('p', { class: 'view-sub' }, 'She keeps the lab alive. Safe fixes run on her word; risky ones wait for yours.'), toastHost, el('div', { class: 'lb-grid' }, el('div', { class: 'lb-chat' }, log, el('div', { class: 'rail-inputwrap' }, input)), actionsPanel)); const chat = wireAgentChat({ logEl: log, inputEl: input, historyUrl: '/api/little-blue', turnUrl: '/api/little-blue/turn', agentName: 'Little Blue', showDrafts: false, toolLabels: BLUE_LABELS }); await chat.load(); await renderActions(actionsPanel, toastHost); }