openvibers/openvibe.actor Commits
The agent page: Run now by the name, the agent at a glance, jump links in reach; one type size on a skill's page (#350)
Changes
@@@ -108,3 +108,23 @@ img.ac-face { background: var(--bg-card, #131c2e); }.pa-cmd-where { margin: .4rem 0 .35rem; font-size: 14px; color: var(--text-secondary, #96a7c2); }.pa-cmd { white-space: pre-wrap; overflow-wrap: anywhere; font-weight: 650; }.pa-sub { margin: 1rem 0 .4rem; font-size: 12px; font-weight: 800; letter-spacing: .06em; text-transform: uppercase; color: var(--text-secondary, #96a7c2); }++++/* The agent page, shorter and easier to move around (Opus, 11-08, after HO6-HO9 made it twelve panels long):++ Run now beside the name, the agent at a glance, jump links that stay in reach, explanations one sentence long. */++.pa-runbar { display: flex; gap: .5rem; align-items: flex-end; margin: 0 0 .85rem; max-width: 46rem; }++.pa-runbar-in { flex: 1; min-width: 0; margin: 0; }++.pa-runbar textarea { width: 100%; min-height: 42px; max-height: 9rem; resize: vertical; margin: 0; padding: .55rem .75rem; border-radius: 12px; font: inherit; font-size: .95rem; line-height: 1.4; }++.pa-runbar .pa-btn { flex: none; min-height: 42px; }++.pa-glance { list-style: none; margin: 0 0 .75rem; padding: 0; display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: .5rem; }++.pa-glance a { display: grid; gap: 1px; padding: .55rem .7rem; border-radius: 12px; border: 1px solid var(--border-color, rgba(255,255,255,.12)); background: var(--bg-card, #131c2e); color: inherit; text-decoration: none; min-width: 0; }++.pa-glance a:hover { border-color: var(--accent, #3b82f6); }++.pa-glance b { font-size: 1.15rem; line-height: 1.2; font-variant-numeric: tabular-nums; }++.pa-glance span { font-size: .74rem; color: var(--text-secondary, #96a7c2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }++@media (max-width: 44rem) { .pa-glance { grid-template-columns: repeat(3, minmax(0, 1fr)); } }++.pa-jump { position: sticky; top: calc(var(--ovnav-real-h, 56px) + 4px); z-index: 5; display: flex; gap: .35rem; margin: 0 0 1rem; padding: .4rem; overflow-x: auto; scrollbar-width: none;++ border-radius: 999px; border: 1px solid var(--border-color, rgba(255,255,255,.12)); background: color-mix(in srgb, var(--bg-primary, #0a0f1c) 86%, transparent); -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px); }++.pa-jump::-webkit-scrollbar { display: none; }++.pa-jump a { flex: none; padding: .3rem .8rem; border-radius: 999px; font-size: .85rem; font-weight: 650; color: var(--text-secondary, #96a7c2); text-decoration: none; }++.pa-jump a:hover, .pa-jump a:focus-visible { color: var(--text-primary, #e6edf7); background: color-mix(in srgb, var(--accent, #3b82f6) 14%, transparent); }++.pa-panel[id] { scroll-margin-top: calc(var(--ovnav-real-h, 56px) + 64px); }++.pa-panel .pa-hint { max-width: 62ch; }@@@ -64,3 +64,11 @@.sk-tool-head { align-items: flex-start; }.sk-net, .sk-limits { white-space: normal; }}++++/* One body size on a skill's page (11-08, Opus: the scan's verdict was 12 px, its date 13.6, the next panel 14.4):++ every sentence in a panel .9rem, every date or footnote .8rem, the verdict with its mark. */++.sk-panel .sk-state, .sk-panel .sk-text, .sk-panel .sk-findings li, .sk-instructions { font-size: .9rem; line-height: 1.5; }++.sk-panel .sk-hint { font-size: .8rem; margin-top: .2rem; }++.sk-state--passed::before { content: "\2713\00a0"; color: var(--success, #22c55e); font-weight: 800; }++.sk-state--flagged::before { content: "!\00a0"; color: var(--warning, #f59e0b); font-weight: 800; }++.sk-state { margin: 0; }@@@ -464,28 +464,40 @@ ${signed(req) ? html`<form method="post" action="/voices/${row.id}/borrow"><butt<header class="pa-head ac-sheet-head">${ui.face(contact, 64)}<div><h1>${a.name}</h1><p class="ac-sheet-uses">${ui.usesLine(contact)}</p><p class="pa-lede">${ui.levelChip(contact.level)} <span class="pill ${a.status === 'paused' ? 'pill-failed' : 'pill-succeeded'}">${a.status === 'paused' ? 'Paused' : 'Active'}</span> ${when(a)}${a.next_run_at && a.status !== 'paused' && a.schedule !== 'off' ? html` · next ${time(a.next_run_at)}` : ''}</p></div></header>${a.last_skip_reason ? html`<p class="notice" role="status">The last scheduled run was skipped: ${a.last_skip_reason}</p>` : ''}--<div class="pa-grid">--<section class="pa-panel"><h2>Run now</h2><form method="post" action="/me/agents/${a.id}/run"><label>Input (optional) <span class="pa-hint">Anything this run should also consider.</span><textarea name="input" maxlength="8000" rows="3"></textarea></label><button type="submit" class="pa-btn pa-primary">Run now</button></form></section>++<form method="post" action="/me/agents/${a.id}/run" class="pa-runbar"><label class="pa-runbar-in"><span class="sr-only">Anything this run should also consider (optional)</span><textarea name="input" maxlength="8000" rows="1" placeholder="Anything this run should also consider (optional)"></textarea></label><button type="submit" class="pa-btn pa-primary">Run now</button></form>++<ul class="pa-glance" aria-label="At a glance">++<li><a href="#runs"><b>${runs.length}${runs.length === 20 ? '+' : ''}</b><span>${runs.length === 1 ? 'run' : 'runs'}</span></a></li>++<li><a href="#always"><b>${allowRules.length}</b><span>always allowed</span></a></li>++<li><a href="#decisions"><b>${decisions.filter((d) => d.state === 'pending').length}</b><span>waiting for you</span></a></li>++<li><a href="#spend"><b>${allowance.left}/${allowance.weekly}</b><span>Vibes left</span></a></li>++<li><a href="#skills"><b>${installed.length}</b><span>${installed.length === 1 ? 'skill' : 'skills'}</span></a></li>++<li><a href="#memory"><b>${notes.length}</b><span>${notes.length === 1 ? 'note' : 'notes'}</span></a></li>++</ul>++<nav class="pa-jump" aria-label="On this page"><a href="#level">Trust</a><a href="#wake">Wake</a><a href="#runs">Runs</a><a href="#always">Allowed</a><a href="#spend">Money</a><a href="#mail">Mail</a><a href="#voice">Voice</a><a href="#skills">Skills</a><a href="#memory">Memory</a><a href="#settings">Settings</a></nav><section class="pa-panel" id="level" aria-labelledby="ac-level-h"><h2 id="ac-level-h">How much it may do on its own</h2>${ui.levelPicker(contact)}</section>--<section class="pa-panel"><h2>Wake it when something happens</h2><p>Besides its schedule, this agent can run the moment something happens: a channel goes live, a new blog post, a page changes, a price drops, a webhook arrives. <a href="https://openvibe.watch">OpenVibe.Watch</a> watches for it and runs this agent with what it saw.</p>++<div class="pa-grid">++<section class="pa-panel" id="wake" aria-labelledby="pa-wake-h"><h2 id="pa-wake-h">Wake it when something happens</h2><p class="pa-hint">Beside its schedule, it can run the moment a channel goes live, a post appears, a page or a price changes, or a webhook arrives. <a href="https://openvibe.watch">OpenVibe.Watch</a> does the watching.</p><p class="pa-actions"><a class="pa-btn" href="https://openvibe.watch/watches/new?template=event&agent_id=${a.id}">When something happens on OpenVibe</a> <a class="pa-btn" href="https://openvibe.watch/watches/new?template=page&agent_id=${a.id}">When a page changes</a></p></section>--<section class="pa-panel"><h2>Recent runs</h2>${vibeChat && /^https:\/\/openvibe\.chat\/vibe\/v-[0-9a-f]{12}$/.test(vibeChat.chat_url) ? html`<p class="pa-hint">Each run also lands in <a href="${vibeChat.chat_url}">its Vibe chat</a>, where you can ask Vibe about it.</p>` : ''}${runs.length ? html`<ul class="pa-runs">${runs.map((t) => html`<li><a href="/tasks/${t.id}">${time(t.created_at)}</a> <span class="pill ${pill(t.state)}">${t.state}</span>${usd(t.cost_usd)}${gist(t) ? html`<span class="pa-run-gist">${gist(t)}</span>` : ''}</li>`)}</ul>` : html`<p class="muted">No run yet.</p>`}</section>++<section class="pa-panel" id="runs" aria-labelledby="pa-runs-h"><h2 id="pa-runs-h">Recent runs</h2>${vibeChat && /^https:\/\/openvibe\.chat\/vibe\/v-[0-9a-f]{12}$/.test(vibeChat.chat_url) ? html`<p class="pa-hint">Each run also lands in <a href="${vibeChat.chat_url}">its Vibe chat</a>, where you can ask Vibe about it.</p>` : ''}${runs.length ? html`<ul class="pa-runs">${runs.map((t) => html`<li><a href="/tasks/${t.id}">${time(t.created_at)}</a> <span class="pill ${pill(t.state)}">${t.state}</span>${usd(t.cost_usd)}${gist(t) ? html`<span class="pa-run-gist">${gist(t)}</span>` : ''}</li>`)}</ul>` : html`<p class="muted">No run yet.</p>`}</section></div>++<div class="pa-grid"><section class="pa-panel" id="always" aria-labelledby="pa-always-h"><h2 id="pa-always-h">What it may always do</h2>--<p class="pa-hint">When you said "always allow this for this agent", the permission is here: one kind of step, on one target. Take any of them back and the next one asks again. Spending money, a password, a deletion and a new recipient always ask, whatever is allowed here.</p>++<p class="pa-hint">Each "always allow" you gave: one kind of step on one target. Revoke one and it asks again. Money, passwords, deletions and new recipients always ask.</p>${allowRules.length ? html`<ul class="pa-notes">${allowRules.map((r) => html`<li><p class="pa-note-text">${kinds.phrase(r.kind, r.target)}</p><p class="pa-note-meta">Allowed ${time(r.created_at)}</p><form method="post" action="/me/agents/${a.id}/allows/${r.id}/revoke" class="pa-actions"><button type="submit" class="pa-danger-link">Revoke</button></form></li>`)}</ul>` : html`<p class="muted">Nothing is always allowed for this agent.</p>`}</section>++<section class="pa-panel" id="decisions" aria-labelledby="pa-dec-h"><h2 id="pa-dec-h">What you decided</h2>++<p class="pa-hint">Every approval it asked for, and your answer: once, always, or no.</p>++${decisions.length ? html`<ul class="pa-notes">${decisions.map((d) => html`<li><p class="pa-note-text"><a href="/me/approvals/${d.id}">${d.summary}</a></p><p class="pa-note-meta">${kinds.label(d.kind)} · ${d.state === 'approved' ? (d.decision_scope === 'always' ? 'You approved it and allowed it always' : 'You approved it once') : d.state === 'declined' ? 'You declined it' : d.state === 'expired' ? 'It expired, nobody answered' : 'Waiting for you'} · ${time(d.decided_at || d.created_at)}</p></li>`)}</ul>` : html`<p class="muted">This agent has not asked for anything yet.</p>`}</section>++</div><section class="pa-panel" id="spend" aria-labelledby="ac-spend-h"><h2 id="ac-spend-h">What it may spend</h2>--<p class="pa-hint">An agent may tip a creator on OpenVibe with your Vibes, but only after you approve each tip, and only up to what is left of its weekly allowance. Zero means it may not spend at all: the spending tool is then not offered to it at all.</p>++<p class="pa-hint">It may tip creators with your Vibes: you approve each tip, and it never goes past this weekly allowance. 0 turns spending off.</p><form method="post" action="/me/agents/${a.id}/spend"><label>Weekly allowance (Vibes) <input name="weekly_vibes" type="number" min="0" max="${spend.MAX_ALLOWANCE}" step="1" value="${allowance.weekly}"></label> <button type="submit" class="pa-btn">Save allowance</button></form><p>Left this week: <strong>${allowance.left} of ${allowance.weekly} Vibes</strong> (${allowance.start} to ${allowance.end} UTC). The week starts on Monday.</p><h3 class="pa-sub">What it spent</h3>${payments.length ? html`<ul class="pa-notes">${payments.map((p2) => html`<li><p class="pa-note-text">${p2.state === 'paid' ? html`${p2.amount} Vibes to ${p2.to_label || p2.to_subject}` : html`${p2.amount} Vibes to ${p2.to_label || p2.to_subject} — not paid`}</p><p class="pa-note-meta">${p2.reason ? html`${p2.reason} · ` : ''}${p2.state === 'paid' ? 'Paid' : html`Refused: ${p2.error || 'Tips did not pay it'}`} · ${time(p2.created_at)}</p></li>`)}</ul>` : html`<p class="muted">It has not spent anything yet.</p>`}</section>--<section class="pa-panel" id="decisions" aria-labelledby="pa-dec-h"><h2 id="pa-dec-h">What you decided</h2>--<p class="pa-hint">Every approval this agent asked for, and what you answered: once, always, or no.</p>--${decisions.length ? html`<ul class="pa-notes">${decisions.map((d) => html`<li><p class="pa-note-text"><a href="/me/approvals/${d.id}">${d.summary}</a></p><p class="pa-note-meta">${kinds.label(d.kind)} · ${d.state === 'approved' ? (d.decision_scope === 'always' ? 'You approved it and allowed it always' : 'You approved it once') : d.state === 'declined' ? 'You declined it' : d.state === 'expired' ? 'It expired, nobody answered' : 'Waiting for you'} · ${time(d.decided_at || d.created_at)}</p></li>`)}</ul>` : html`<p class="muted">This agent has not asked for anything yet.</p>`}</section>++${pendingSuggestions.length || applied.length ? html`<section class="pa-panel" aria-labelledby="pa-sugg-h"><h2 id="pa-sugg-h">Suggestions from your agent</h2><p class="pa-hint">After a run that passed its check, the agent may suggest a line for its instructions. Nothing changes until you add it, and you can undo it.</p>${pendingSuggestions.length ? html`<ul class="pa-notes">${pendingSuggestions.map((p) => html`<li><p class="pa-note-text">${p.text}</p><p class="pa-note-meta">Suggested ${time(p.created_at)}${p.from_task ? html` after <a href="/tasks/${p.from_task}">this run</a>` : ''}</p>@@@ -493,11 +505,11 @@ ${pendingSuggestions.length ? html`<ul class="pa-notes">${pendingSuggestions.map${applied.length ? html`<details><summary>Added lately</summary><ul class="pa-notes">${applied.map((p) => html`<li><p class="pa-note-text">${p.text}</p><p class="pa-note-meta">Added ${time(p.decided_at)}</p><form method="post" action="/me/agents/${a.id}/suggestions/${p.id}"><button type="submit" name="decision" value="undo" class="pa-danger-link">Undo</button></form></li>`)}</ul></details>` : ''}</section>` : ''}${mailPanel(a, mailInbox, mailAnswered)}${voicePanel(a, doc, shared)}<section class="pa-panel" id="skills" aria-labelledby="pa-skills-h"><h2 id="pa-skills-h">Its skills</h2>--<p class="pa-hint">A skill is a small document this agent reads to do one kind of job well. It grants nothing: every step the skill helps with still goes through this agent's level and your approvals, exactly as before.</p>++<p class="pa-hint">Small how-to documents it reads for one kind of job. It grants nothing: every step still goes through its level and your approvals.</p>${installed.length ? html`<ul class="pa-notes">${installed.map((k) => html`<li><p class="pa-note-text">${k.name}</p><p class="pa-note-meta">${k.description}</p>${k.withdrawn ? html`<p class="pa-note-meta">${k.withdrawn}</p>` : ''}<form method="post" action="/me/agents/${a.id}/skills/${k.listing_id}/remove" class="pa-actions"><button type="submit" class="pa-danger-link">Remove</button></form></li>`)}</ul>` : html`<p class="muted">No skills on this agent yet.</p>`}--<p class="pa-hint"><a href="/skills">The skills registry</a></p></section><section class="pa-panel"><h2>Memory</h2><p class="pa-hint">What this agent remembers, newest first. Only active notes go into a run. Notes you write are never dropped; when memory is full (50 notes) the oldest note the agent wrote goes first. An agent adds at most one note per run, and only when its answer passed the check; its note is a suggestion until you accept it on <a href="/memory">Memory</a>.</p>++<p class="pa-hint"><a href="/skills">The skills registry</a></p></section><section class="pa-panel" id="memory" aria-labelledby="pa-mem-h"><h2 id="pa-mem-h">Memory</h2><p class="pa-hint">Newest first; only active notes go into a run. It adds at most one note per good run, as a suggestion you accept on <a href="/memory">Memory</a>. Your own notes are never dropped (50 notes at most).</p>${notes.length ? html`<ul class="pa-notes">${notes.map((n) => html`<li><p class="pa-note-text">${n.text}</p><p class="pa-note-meta"><span class="pill ${n.status === 'suggested' ? 'pill-running' : n.status === 'archived' ? 'pill-failed' : 'pill-succeeded'}">${n.status === 'suggested' ? 'Suggested' : n.status === 'archived' ? 'Archived' : 'Active'}</span> ${n.source === 'agent' ? 'Written by the agent' : 'Written by you'} · ${n.kind} · ${time(n.created_at)}${n.last_used_at ? html` · last used ${time(n.last_used_at)}` : ''}${n.pinned ? ' · pinned' : ''}${n.merged_into ? ' · merged away' : ''}${n.retired_at ? ' · retired' : ''}</p>${n.status === 'suggested' ? html`<form method="post" action="/memory/${n.id}/accept" class="pa-actions"><button type="submit" class="pa-btn pa-primary">Accept</button> <button type="submit" class="pa-btn" formaction="/memory/${n.id}/decline">Decline</button></form>` : ''}<form method="post" action="/me/agents/${a.id}/memory/${n.id}/${n.pinned ? 'unpin' : 'pin'}" class="pa-actions"><button type="submit" class="pa-btn">${n.pinned ? 'Unpin' : 'Pin'}</button></form><details><summary>Edit or delete</summary><form method="post" action="/me/agents/${a.id}/memory/${n.id}/edit"><label>Note <input name="text" maxlength="${require('../memory').MAX_TEXT}" value="${n.text}"></label><button type="submit" class="pa-btn">Save</button></form>@@@ -512,7 +524,7 @@ ${diaries.length ? html`<ul class="pa-notes">${diaries.map((d) => html`<li><p cl${tidyChanges.length ? html`<h3 class="pa-sub">What its nights changed</h3><ul class="pa-notes">${tidyChanges.map((c) => html`<li><p class="pa-note-text">${CHANGE_LABEL[c.kind] || 'Changed a note'}</p><p class="pa-note-meta">${time(c.created_at)}${c.undone_at ? ' · undone' : ''}</p>${c.undoable ? html`<form method="post" action="/me/agents/${a.id}/changes/${c.id}/undo"><button type="submit" class="pa-danger-link">Undo</button></form>` : ''}</li>`)}</ul>` : ''}</section>` : ''}--<section class="pa-panel"><h2>Settings</h2><form method="post" action="/me/agents/${a.id}/edit">${form(a)}<button type="submit" class="pa-btn pa-primary">Save settings</button></form>++<section class="pa-panel" id="settings" aria-labelledby="pa-set-h"><h2 id="pa-set-h">Settings</h2><form method="post" action="/me/agents/${a.id}/edit">${form(a)}<button type="submit" class="pa-btn pa-primary">Save settings</button></form><form method="post" action="/me/agents/${a.id}/delete" class="pa-delete"><button type="submit" class="pa-danger-link">Delete this agent</button> <span class="pa-hint">Its memory goes with it; its past runs stay on your task list.</span></form></section>`);});r.post('/me/agents/:id/edit', async (req, res) => { if (!write(req, res)) return; const out = await personal.update(s, config, owner(req), req.params.id, fields(req.body)); if (!out) return page(req, res, 'Agent not found', html`<h1>Agent not found</h1>`, 404); if (!out.row) return page(req, res, 'Invalid agent', html`<h1>Invalid agent</h1><p>${out.error}</p>`, 422); res.redirect(303, `/me/agents/${req.params.id}`); });
@@ -108,3 +108,23 @@ img.ac-face { background: var(--bg-card, #131c2e); }
@@ -108,3 +108,23 @@ img.ac-face { background: var(--bg-card, #131c2e); }
.pa-cmd-where { margin: .4rem 0 .35rem; font-size: 14px; color: var(--text-secondary, #96a7c2); }
.pa-cmd-where { margin: .4rem 0 .35rem; font-size: 14px; color: var(--text-secondary, #96a7c2); }
.pa-cmd { white-space: pre-wrap; overflow-wrap: anywhere; font-weight: 650; }
.pa-cmd { white-space: pre-wrap; overflow-wrap: anywhere; font-weight: 650; }
.pa-sub { margin: 1rem 0 .4rem; font-size: 12px; font-weight: 800; letter-spacing: .06em; text-transform: uppercase; color: var(--text-secondary, #96a7c2); }
.pa-sub { margin: 1rem 0 .4rem; font-size: 12px; font-weight: 800; letter-spacing: .06em; text-transform: uppercase; color: var(--text-secondary, #96a7c2); }
+
+/* The agent page, shorter and easier to move around (Opus, 11-08, after HO6-HO9 made it twelve panels long):
+ Run now beside the name, the agent at a glance, jump links that stay in reach, explanations one sentence long. */
+.pa-runbar { display: flex; gap: .5rem; align-items: flex-end; margin: 0 0 .85rem; max-width: 46rem; }
+.pa-runbar-in { flex: 1; min-width: 0; margin: 0; }
+.pa-runbar textarea { width: 100%; min-height: 42px; max-height: 9rem; resize: vertical; margin: 0; padding: .55rem .75rem; border-radius: 12px; font: inherit; font-size: .95rem; line-height: 1.4; }
+.pa-runbar .pa-btn { flex: none; min-height: 42px; }
+.pa-glance { list-style: none; margin: 0 0 .75rem; padding: 0; display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: .5rem; }
+.pa-glance a { display: grid; gap: 1px; padding: .55rem .7rem; border-radius: 12px; border: 1px solid var(--border-color, rgba(255,255,255,.12)); background: var(--bg-card, #131c2e); color: inherit; text-decoration: none; min-width: 0; }
+.pa-glance a:hover { border-color: var(--accent, #3b82f6); }
+.pa-glance b { font-size: 1.15rem; line-height: 1.2; font-variant-numeric: tabular-nums; }
+.pa-glance span { font-size: .74rem; color: var(--text-secondary, #96a7c2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+@media (max-width: 44rem) { .pa-glance { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
+.pa-jump { position: sticky; top: calc(var(--ovnav-real-h, 56px) + 4px); z-index: 5; display: flex; gap: .35rem; margin: 0 0 1rem; padding: .4rem; overflow-x: auto; scrollbar-width: none;
+ border-radius: 999px; border: 1px solid var(--border-color, rgba(255,255,255,.12)); background: color-mix(in srgb, var(--bg-primary, #0a0f1c) 86%, transparent); -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px); }
+.pa-jump::-webkit-scrollbar { display: none; }
+.pa-jump a { flex: none; padding: .3rem .8rem; border-radius: 999px; font-size: .85rem; font-weight: 650; color: var(--text-secondary, #96a7c2); text-decoration: none; }
+.pa-jump a:hover, .pa-jump a:focus-visible { color: var(--text-primary, #e6edf7); background: color-mix(in srgb, var(--accent, #3b82f6) 14%, transparent); }
+.pa-panel[id] { scroll-margin-top: calc(var(--ovnav-real-h, 56px) + 64px); }
+.pa-panel .pa-hint { max-width: 62ch; }
@@ -64,3 +64,11 @@
@@ -64,3 +64,11 @@
.sk-tool-head { align-items: flex-start; }
.sk-tool-head { align-items: flex-start; }
.sk-net, .sk-limits { white-space: normal; }
.sk-net, .sk-limits { white-space: normal; }
}
}
+
+/* One body size on a skill's page (11-08, Opus: the scan's verdict was 12 px, its date 13.6, the next panel 14.4):
+ every sentence in a panel .9rem, every date or footnote .8rem, the verdict with its mark. */
+.sk-panel .sk-state, .sk-panel .sk-text, .sk-panel .sk-findings li, .sk-instructions { font-size: .9rem; line-height: 1.5; }
+.sk-panel .sk-hint { font-size: .8rem; margin-top: .2rem; }
+.sk-state--passed::before { content: "\2713\00a0"; color: var(--success, #22c55e); font-weight: 800; }
+.sk-state--flagged::before { content: "!\00a0"; color: var(--warning, #f59e0b); font-weight: 800; }
+.sk-state { margin: 0; }
@@ -464,28 +464,40 @@ ${signed(req) ? html`<form method="post" action="/voices/${row.id}/borrow"><butt
@@ -464,28 +464,40 @@ ${signed(req) ? html`<form method="post" action="/voices/${row.id}/borrow"><butt
<header class="pa-head ac-sheet-head">${ui.face(contact, 64)}<div><h1>${a.name}</h1><p class="ac-sheet-uses">${ui.usesLine(contact)}</p>
<header class="pa-head ac-sheet-head">${ui.face(contact, 64)}<div><h1>${a.name}</h1><p class="ac-sheet-uses">${ui.usesLine(contact)}</p>
<p class="pa-lede">${ui.levelChip(contact.level)} <span class="pill ${a.status === 'paused' ? 'pill-failed' : 'pill-succeeded'}">${a.status === 'paused' ? 'Paused' : 'Active'}</span> ${when(a)}${a.next_run_at && a.status !== 'paused' && a.schedule !== 'off' ? html` · next ${time(a.next_run_at)}` : ''}</p></div></header>
<p class="pa-lede">${ui.levelChip(contact.level)} <span class="pill ${a.status === 'paused' ? 'pill-failed' : 'pill-succeeded'}">${a.status === 'paused' ? 'Paused' : 'Active'}</span> ${when(a)}${a.next_run_at && a.status !== 'paused' && a.schedule !== 'off' ? html` · next ${time(a.next_run_at)}` : ''}</p></div></header>
${a.last_skip_reason ? html`<p class="notice" role="status">The last scheduled run was skipped: ${a.last_skip_reason}</p>` : ''}
${a.last_skip_reason ? html`<p class="notice" role="status">The last scheduled run was skipped: ${a.last_skip_reason}</p>` : ''}
-<div class="pa-grid">
-<section class="pa-panel"><h2>Run now</h2><form method="post" action="/me/agents/${a.id}/run"><label>Input (optional) <span class="pa-hint">Anything this run should also consider.</span><textarea name="input" maxlength="8000" rows="3"></textarea></label><button type="submit" class="pa-btn pa-primary">Run now</button></form></section>
+<form method="post" action="/me/agents/${a.id}/run" class="pa-runbar"><label class="pa-runbar-in"><span class="sr-only">Anything this run should also consider (optional)</span><textarea name="input" maxlength="8000" rows="1" placeholder="Anything this run should also consider (optional)"></textarea></label><button type="submit" class="pa-btn pa-primary">Run now</button></form>
+<ul class="pa-glance" aria-label="At a glance">
+<li><a href="#runs"><b>${runs.length}${runs.length === 20 ? '+' : ''}</b><span>${runs.length === 1 ? 'run' : 'runs'}</span></a></li>
+<li><a href="#always"><b>${allowRules.length}</b><span>always allowed</span></a></li>
+<li><a href="#decisions"><b>${decisions.filter((d) => d.state === 'pending').length}</b><span>waiting for you</span></a></li>
+<li><a href="#spend"><b>${allowance.left}/${allowance.weekly}</b><span>Vibes left</span></a></li>
+<li><a href="#skills"><b>${installed.length}</b><span>${installed.length === 1 ? 'skill' : 'skills'}</span></a></li>
+<li><a href="#memory"><b>${notes.length}</b><span>${notes.length === 1 ? 'note' : 'notes'}</span></a></li>
+</ul>
+<nav class="pa-jump" aria-label="On this page"><a href="#level">Trust</a><a href="#wake">Wake</a><a href="#runs">Runs</a><a href="#always">Allowed</a><a href="#spend">Money</a><a href="#mail">Mail</a><a href="#voice">Voice</a><a href="#skills">Skills</a><a href="#memory">Memory</a><a href="#settings">Settings</a></nav>
<section class="pa-panel" id="level" aria-labelledby="ac-level-h"><h2 id="ac-level-h">How much it may do on its own</h2>${ui.levelPicker(contact)}</section>
<section class="pa-panel" id="level" aria-labelledby="ac-level-h"><h2 id="ac-level-h">How much it may do on its own</h2>${ui.levelPicker(contact)}</section>
-<section class="pa-panel"><h2>Wake it when something happens</h2><p>Besides its schedule, this agent can run the moment something happens: a channel goes live, a new blog post, a page changes, a price drops, a webhook arrives. <a href="https://openvibe.watch">OpenVibe.Watch</a> watches for it and runs this agent with what it saw.</p>
+<div class="pa-grid">
+<section class="pa-panel" id="wake" aria-labelledby="pa-wake-h"><h2 id="pa-wake-h">Wake it when something happens</h2><p class="pa-hint">Beside its schedule, it can run the moment a channel goes live, a post appears, a page or a price changes, or a webhook arrives. <a href="https://openvibe.watch">OpenVibe.Watch</a> does the watching.</p>
<p class="pa-actions"><a class="pa-btn" href="https://openvibe.watch/watches/new?template=event&agent_id=${a.id}">When something happens on OpenVibe</a> <a class="pa-btn" href="https://openvibe.watch/watches/new?template=page&agent_id=${a.id}">When a page changes</a></p></section>
<p class="pa-actions"><a class="pa-btn" href="https://openvibe.watch/watches/new?template=event&agent_id=${a.id}">When something happens on OpenVibe</a> <a class="pa-btn" href="https://openvibe.watch/watches/new?template=page&agent_id=${a.id}">When a page changes</a></p></section>
-<section class="pa-panel"><h2>Recent runs</h2>${vibeChat && /^https:\/\/openvibe\.chat\/vibe\/v-[0-9a-f]{12}$/.test(vibeChat.chat_url) ? html`<p class="pa-hint">Each run also lands in <a href="${vibeChat.chat_url}">its Vibe chat</a>, where you can ask Vibe about it.</p>` : ''}${runs.length ? html`<ul class="pa-runs">${runs.map((t) => html`<li><a href="/tasks/${t.id}">${time(t.created_at)}</a> <span class="pill ${pill(t.state)}">${t.state}</span>${usd(t.cost_usd)}${gist(t) ? html`<span class="pa-run-gist">${gist(t)}</span>` : ''}</li>`)}</ul>` : html`<p class="muted">No run yet.</p>`}</section>
+<section class="pa-panel" id="runs" aria-labelledby="pa-runs-h"><h2 id="pa-runs-h">Recent runs</h2>${vibeChat && /^https:\/\/openvibe\.chat\/vibe\/v-[0-9a-f]{12}$/.test(vibeChat.chat_url) ? html`<p class="pa-hint">Each run also lands in <a href="${vibeChat.chat_url}">its Vibe chat</a>, where you can ask Vibe about it.</p>` : ''}${runs.length ? html`<ul class="pa-runs">${runs.map((t) => html`<li><a href="/tasks/${t.id}">${time(t.created_at)}</a> <span class="pill ${pill(t.state)}">${t.state}</span>${usd(t.cost_usd)}${gist(t) ? html`<span class="pa-run-gist">${gist(t)}</span>` : ''}</li>`)}</ul>` : html`<p class="muted">No run yet.</p>`}</section>
</div>
</div>
+<div class="pa-grid">
<section class="pa-panel" id="always" aria-labelledby="pa-always-h"><h2 id="pa-always-h">What it may always do</h2>
<section class="pa-panel" id="always" aria-labelledby="pa-always-h"><h2 id="pa-always-h">What it may always do</h2>
-<p class="pa-hint">When you said "always allow this for this agent", the permission is here: one kind of step, on one target. Take any of them back and the next one asks again. Spending money, a password, a deletion and a new recipient always ask, whatever is allowed here.</p>
+<p class="pa-hint">Each "always allow" you gave: one kind of step on one target. Revoke one and it asks again. Money, passwords, deletions and new recipients always ask.</p>
${allowRules.length ? html`<ul class="pa-notes">${allowRules.map((r) => html`<li><p class="pa-note-text">${kinds.phrase(r.kind, r.target)}</p><p class="pa-note-meta">Allowed ${time(r.created_at)}</p>
${allowRules.length ? html`<ul class="pa-notes">${allowRules.map((r) => html`<li><p class="pa-note-text">${kinds.phrase(r.kind, r.target)}</p><p class="pa-note-meta">Allowed ${time(r.created_at)}</p>
<form method="post" action="/me/agents/${a.id}/allows/${r.id}/revoke" class="pa-actions"><button type="submit" class="pa-danger-link">Revoke</button></form></li>`)}</ul>` : html`<p class="muted">Nothing is always allowed for this agent.</p>`}</section>
<form method="post" action="/me/agents/${a.id}/allows/${r.id}/revoke" class="pa-actions"><button type="submit" class="pa-danger-link">Revoke</button></form></li>`)}</ul>` : html`<p class="muted">Nothing is always allowed for this agent.</p>`}</section>
+<section class="pa-panel" id="decisions" aria-labelledby="pa-dec-h"><h2 id="pa-dec-h">What you decided</h2>
+<p class="pa-hint">Every approval it asked for, and your answer: once, always, or no.</p>
+${decisions.length ? html`<ul class="pa-notes">${decisions.map((d) => html`<li><p class="pa-note-text"><a href="/me/approvals/${d.id}">${d.summary}</a></p><p class="pa-note-meta">${kinds.label(d.kind)} · ${d.state === 'approved' ? (d.decision_scope === 'always' ? 'You approved it and allowed it always' : 'You approved it once') : d.state === 'declined' ? 'You declined it' : d.state === 'expired' ? 'It expired, nobody answered' : 'Waiting for you'} · ${time(d.decided_at || d.created_at)}</p></li>`)}</ul>` : html`<p class="muted">This agent has not asked for anything yet.</p>`}</section>
+</div>
<section class="pa-panel" id="spend" aria-labelledby="ac-spend-h"><h2 id="ac-spend-h">What it may spend</h2>
<section class="pa-panel" id="spend" aria-labelledby="ac-spend-h"><h2 id="ac-spend-h">What it may spend</h2>
-<p class="pa-hint">An agent may tip a creator on OpenVibe with your Vibes, but only after you approve each tip, and only up to what is left of its weekly allowance. Zero means it may not spend at all: the spending tool is then not offered to it at all.</p>
+<p class="pa-hint">It may tip creators with your Vibes: you approve each tip, and it never goes past this weekly allowance. 0 turns spending off.</p>
<form method="post" action="/me/agents/${a.id}/spend"><label>Weekly allowance (Vibes) <input name="weekly_vibes" type="number" min="0" max="${spend.MAX_ALLOWANCE}" step="1" value="${allowance.weekly}"></label> <button type="submit" class="pa-btn">Save allowance</button></form>
<form method="post" action="/me/agents/${a.id}/spend"><label>Weekly allowance (Vibes) <input name="weekly_vibes" type="number" min="0" max="${spend.MAX_ALLOWANCE}" step="1" value="${allowance.weekly}"></label> <button type="submit" class="pa-btn">Save allowance</button></form>
<p>Left this week: <strong>${allowance.left} of ${allowance.weekly} Vibes</strong> (${allowance.start} to ${allowance.end} UTC). The week starts on Monday.</p>
<p>Left this week: <strong>${allowance.left} of ${allowance.weekly} Vibes</strong> (${allowance.start} to ${allowance.end} UTC). The week starts on Monday.</p>
<h3 class="pa-sub">What it spent</h3>
<h3 class="pa-sub">What it spent</h3>
${payments.length ? html`<ul class="pa-notes">${payments.map((p2) => html`<li><p class="pa-note-text">${p2.state === 'paid' ? html`${p2.amount} Vibes to ${p2.to_label || p2.to_subject}` : html`${p2.amount} Vibes to ${p2.to_label || p2.to_subject} — not paid`}</p>
${payments.length ? html`<ul class="pa-notes">${payments.map((p2) => html`<li><p class="pa-note-text">${p2.state === 'paid' ? html`${p2.amount} Vibes to ${p2.to_label || p2.to_subject}` : html`${p2.amount} Vibes to ${p2.to_label || p2.to_subject} — not paid`}</p>
<p class="pa-note-meta">${p2.reason ? html`${p2.reason} · ` : ''}${p2.state === 'paid' ? 'Paid' : html`Refused: ${p2.error || 'Tips did not pay it'}`} · ${time(p2.created_at)}</p></li>`)}</ul>` : html`<p class="muted">It has not spent anything yet.</p>`}
<p class="pa-note-meta">${p2.reason ? html`${p2.reason} · ` : ''}${p2.state === 'paid' ? 'Paid' : html`Refused: ${p2.error || 'Tips did not pay it'}`} · ${time(p2.created_at)}</p></li>`)}</ul>` : html`<p class="muted">It has not spent anything yet.</p>`}
</section>
</section>
-<section class="pa-panel" id="decisions" aria-labelledby="pa-dec-h"><h2 id="pa-dec-h">What you decided</h2>
-<p class="pa-hint">Every approval this agent asked for, and what you answered: once, always, or no.</p>
-${decisions.length ? html`<ul class="pa-notes">${decisions.map((d) => html`<li><p class="pa-note-text"><a href="/me/approvals/${d.id}">${d.summary}</a></p><p class="pa-note-meta">${kinds.label(d.kind)} · ${d.state === 'approved' ? (d.decision_scope === 'always' ? 'You approved it and allowed it always' : 'You approved it once') : d.state === 'declined' ? 'You declined it' : d.state === 'expired' ? 'It expired, nobody answered' : 'Waiting for you'} · ${time(d.decided_at || d.created_at)}</p></li>`)}</ul>` : html`<p class="muted">This agent has not asked for anything yet.</p>`}</section>
+
${pendingSuggestions.length || applied.length ? html`<section class="pa-panel" aria-labelledby="pa-sugg-h"><h2 id="pa-sugg-h">Suggestions from your agent</h2>
${pendingSuggestions.length || applied.length ? html`<section class="pa-panel" aria-labelledby="pa-sugg-h"><h2 id="pa-sugg-h">Suggestions from your agent</h2>
<p class="pa-hint">After a run that passed its check, the agent may suggest a line for its instructions. Nothing changes until you add it, and you can undo it.</p>
<p class="pa-hint">After a run that passed its check, the agent may suggest a line for its instructions. Nothing changes until you add it, and you can undo it.</p>
${pendingSuggestions.length ? html`<ul class="pa-notes">${pendingSuggestions.map((p) => html`<li><p class="pa-note-text">${p.text}</p><p class="pa-note-meta">Suggested ${time(p.created_at)}${p.from_task ? html` after <a href="/tasks/${p.from_task}">this run</a>` : ''}</p>
${pendingSuggestions.length ? html`<ul class="pa-notes">${pendingSuggestions.map((p) => html`<li><p class="pa-note-text">${p.text}</p><p class="pa-note-meta">Suggested ${time(p.created_at)}${p.from_task ? html` after <a href="/tasks/${p.from_task}">this run</a>` : ''}</p>
@@ -493,11 +505,11 @@ ${pendingSuggestions.length ? html`<ul class="pa-notes">${pendingSuggestions.map
@@ -493,11 +505,11 @@ ${pendingSuggestions.length ? html`<ul class="pa-notes">${pendingSuggestions.map
${applied.length ? html`<details><summary>Added lately</summary><ul class="pa-notes">${applied.map((p) => html`<li><p class="pa-note-text">${p.text}</p><p class="pa-note-meta">Added ${time(p.decided_at)}</p>
${applied.length ? html`<details><summary>Added lately</summary><ul class="pa-notes">${applied.map((p) => html`<li><p class="pa-note-text">${p.text}</p><p class="pa-note-meta">Added ${time(p.decided_at)}</p>
<form method="post" action="/me/agents/${a.id}/suggestions/${p.id}"><button type="submit" name="decision" value="undo" class="pa-danger-link">Undo</button></form></li>`)}</ul></details>` : ''}</section>` : ''}
<form method="post" action="/me/agents/${a.id}/suggestions/${p.id}"><button type="submit" name="decision" value="undo" class="pa-danger-link">Undo</button></form></li>`)}</ul></details>` : ''}</section>` : ''}
${mailPanel(a, mailInbox, mailAnswered)}${voicePanel(a, doc, shared)}<section class="pa-panel" id="skills" aria-labelledby="pa-skills-h"><h2 id="pa-skills-h">Its skills</h2>
${mailPanel(a, mailInbox, mailAnswered)}${voicePanel(a, doc, shared)}<section class="pa-panel" id="skills" aria-labelledby="pa-skills-h"><h2 id="pa-skills-h">Its skills</h2>
-<p class="pa-hint">A skill is a small document this agent reads to do one kind of job well. It grants nothing: every step the skill helps with still goes through this agent's level and your approvals, exactly as before.</p>
+<p class="pa-hint">Small how-to documents it reads for one kind of job. It grants nothing: every step still goes through its level and your approvals.</p>
${installed.length ? html`<ul class="pa-notes">${installed.map((k) => html`<li><p class="pa-note-text">${k.name}</p><p class="pa-note-meta">${k.description}</p>
${installed.length ? html`<ul class="pa-notes">${installed.map((k) => html`<li><p class="pa-note-text">${k.name}</p><p class="pa-note-meta">${k.description}</p>
${k.withdrawn ? html`<p class="pa-note-meta">${k.withdrawn}</p>` : ''}
${k.withdrawn ? html`<p class="pa-note-meta">${k.withdrawn}</p>` : ''}
<form method="post" action="/me/agents/${a.id}/skills/${k.listing_id}/remove" class="pa-actions"><button type="submit" class="pa-danger-link">Remove</button></form></li>`)}</ul>` : html`<p class="muted">No skills on this agent yet.</p>`}
<form method="post" action="/me/agents/${a.id}/skills/${k.listing_id}/remove" class="pa-actions"><button type="submit" class="pa-danger-link">Remove</button></form></li>`)}</ul>` : html`<p class="muted">No skills on this agent yet.</p>`}
-<p class="pa-hint"><a href="/skills">The skills registry</a></p></section><section class="pa-panel"><h2>Memory</h2><p class="pa-hint">What this agent remembers, newest first. Only active notes go into a run. Notes you write are never dropped; when memory is full (50 notes) the oldest note the agent wrote goes first. An agent adds at most one note per run, and only when its answer passed the check; its note is a suggestion until you accept it on <a href="/memory">Memory</a>.</p>
+<p class="pa-hint"><a href="/skills">The skills registry</a></p></section><section class="pa-panel" id="memory" aria-labelledby="pa-mem-h"><h2 id="pa-mem-h">Memory</h2><p class="pa-hint">Newest first; only active notes go into a run. It adds at most one note per good run, as a suggestion you accept on <a href="/memory">Memory</a>. Your own notes are never dropped (50 notes at most).</p>
${notes.length ? html`<ul class="pa-notes">${notes.map((n) => html`<li><p class="pa-note-text">${n.text}</p><p class="pa-note-meta"><span class="pill ${n.status === 'suggested' ? 'pill-running' : n.status === 'archived' ? 'pill-failed' : 'pill-succeeded'}">${n.status === 'suggested' ? 'Suggested' : n.status === 'archived' ? 'Archived' : 'Active'}</span> ${n.source === 'agent' ? 'Written by the agent' : 'Written by you'} · ${n.kind} · ${time(n.created_at)}${n.last_used_at ? html` · last used ${time(n.last_used_at)}` : ''}${n.pinned ? ' · pinned' : ''}${n.merged_into ? ' · merged away' : ''}${n.retired_at ? ' · retired' : ''}</p>${n.status === 'suggested' ? html`<form method="post" action="/memory/${n.id}/accept" class="pa-actions"><button type="submit" class="pa-btn pa-primary">Accept</button> <button type="submit" class="pa-btn" formaction="/memory/${n.id}/decline">Decline</button></form>` : ''}
${notes.length ? html`<ul class="pa-notes">${notes.map((n) => html`<li><p class="pa-note-text">${n.text}</p><p class="pa-note-meta"><span class="pill ${n.status === 'suggested' ? 'pill-running' : n.status === 'archived' ? 'pill-failed' : 'pill-succeeded'}">${n.status === 'suggested' ? 'Suggested' : n.status === 'archived' ? 'Archived' : 'Active'}</span> ${n.source === 'agent' ? 'Written by the agent' : 'Written by you'} · ${n.kind} · ${time(n.created_at)}${n.last_used_at ? html` · last used ${time(n.last_used_at)}` : ''}${n.pinned ? ' · pinned' : ''}${n.merged_into ? ' · merged away' : ''}${n.retired_at ? ' · retired' : ''}</p>${n.status === 'suggested' ? html`<form method="post" action="/memory/${n.id}/accept" class="pa-actions"><button type="submit" class="pa-btn pa-primary">Accept</button> <button type="submit" class="pa-btn" formaction="/memory/${n.id}/decline">Decline</button></form>` : ''}
<form method="post" action="/me/agents/${a.id}/memory/${n.id}/${n.pinned ? 'unpin' : 'pin'}" class="pa-actions"><button type="submit" class="pa-btn">${n.pinned ? 'Unpin' : 'Pin'}</button></form>
<form method="post" action="/me/agents/${a.id}/memory/${n.id}/${n.pinned ? 'unpin' : 'pin'}" class="pa-actions"><button type="submit" class="pa-btn">${n.pinned ? 'Unpin' : 'Pin'}</button></form>
<details><summary>Edit or delete</summary><form method="post" action="/me/agents/${a.id}/memory/${n.id}/edit"><label>Note <input name="text" maxlength="${require('../memory').MAX_TEXT}" value="${n.text}"></label><button type="submit" class="pa-btn">Save</button></form>
<details><summary>Edit or delete</summary><form method="post" action="/me/agents/${a.id}/memory/${n.id}/edit"><label>Note <input name="text" maxlength="${require('../memory').MAX_TEXT}" value="${n.text}"></label><button type="submit" class="pa-btn">Save</button></form>
@@ -512,7 +524,7 @@ ${diaries.length ? html`<ul class="pa-notes">${diaries.map((d) => html`<li><p cl
@@ -512,7 +524,7 @@ ${diaries.length ? html`<ul class="pa-notes">${diaries.map((d) => html`<li><p cl
${tidyChanges.length ? html`<h3 class="pa-sub">What its nights changed</h3><ul class="pa-notes">${tidyChanges.map((c) => html`<li><p class="pa-note-text">${CHANGE_LABEL[c.kind] || 'Changed a note'}</p><p class="pa-note-meta">${time(c.created_at)}${c.undone_at ? ' · undone' : ''}</p>
${tidyChanges.length ? html`<h3 class="pa-sub">What its nights changed</h3><ul class="pa-notes">${tidyChanges.map((c) => html`<li><p class="pa-note-text">${CHANGE_LABEL[c.kind] || 'Changed a note'}</p><p class="pa-note-meta">${time(c.created_at)}${c.undone_at ? ' · undone' : ''}</p>
${c.undoable ? html`<form method="post" action="/me/agents/${a.id}/changes/${c.id}/undo"><button type="submit" class="pa-danger-link">Undo</button></form>` : ''}</li>`)}</ul>` : ''}
${c.undoable ? html`<form method="post" action="/me/agents/${a.id}/changes/${c.id}/undo"><button type="submit" class="pa-danger-link">Undo</button></form>` : ''}</li>`)}</ul>` : ''}
</section>` : ''}
</section>` : ''}
-<section class="pa-panel"><h2>Settings</h2><form method="post" action="/me/agents/${a.id}/edit">${form(a)}<button type="submit" class="pa-btn pa-primary">Save settings</button></form>
+<section class="pa-panel" id="settings" aria-labelledby="pa-set-h"><h2 id="pa-set-h">Settings</h2><form method="post" action="/me/agents/${a.id}/edit">${form(a)}<button type="submit" class="pa-btn pa-primary">Save settings</button></form>
<form method="post" action="/me/agents/${a.id}/delete" class="pa-delete"><button type="submit" class="pa-danger-link">Delete this agent</button> <span class="pa-hint">Its memory goes with it; its past runs stay on your task list.</span></form></section>`);
<form method="post" action="/me/agents/${a.id}/delete" class="pa-delete"><button type="submit" class="pa-danger-link">Delete this agent</button> <span class="pa-hint">Its memory goes with it; its past runs stay on your task list.</span></form></section>`);
});
});
r.post('/me/agents/:id/edit', async (req, res) => { if (!write(req, res)) return; const out = await personal.update(s, config, owner(req), req.params.id, fields(req.body)); if (!out) return page(req, res, 'Agent not found', html`<h1>Agent not found</h1>`, 404); if (!out.row) return page(req, res, 'Invalid agent', html`<h1>Invalid agent</h1><p>${out.error}</p>`, 422); res.redirect(303, `/me/agents/${req.params.id}`); });
r.post('/me/agents/:id/edit', async (req, res) => { if (!write(req, res)) return; const out = await personal.update(s, config, owner(req), req.params.id, fields(req.body)); if (!out) return page(req, res, 'Agent not found', html`<h1>Agent not found</h1>`, 404); if (!out.row) return page(req, res, 'Invalid agent', html`<h1>Invalid agent</h1><p>${out.error}</p>`, 422); res.redirect(303, `/me/agents/${req.params.id}`); });