// ═══════════════════════════════════════════════════════════════
// OpenVibe — the Dock's Actor block (Actor v3 plan, stage A3; redrawn 2026-11-10)
//
// Your Actor at the top of the Dock's Agents tab, on every OpenVibe site. dock.js loads this file the first time that
// tab opens:
// OpenVibeDockActor.mount(container, { token, base, active }) → { refresh(), destroy() }
//
// It reads OpenVibe.Actor's GET /api/v1/me/actor with the person's Network token (scope actor.me.read); the answers,
// pause, resume and tell are POSTs with actor.me.write. On a site every call goes through Network's same-origin relay
// (/api/frame/actor…) because every site's connect-src allows openvibe.network and refuses openvibe.actor; `base`
// reaches Actor itself in a test, and `frameBase` names the relay. Nothing is asked without a token.
//
// The look (the owner, 2026-11-10: "more intuitive, minimalist, elegant"): one row and one line to type in.
// - Who: the face with a ring in the colour of its state, the name, and one quiet line (the state, today's spend
// against the cap). The row is the way to the Actor's page; there is no separate "Open".
// - One action beside it, the one that fits: "Resume" in the accent when it is paused, a quiet pause when it runs.
// - What it does, only when there is something to say: the step it is on (a soft pulse), what it looks at, the
// steps before it, and what comes next, as a short line of dots. A paused Actor shows none of this: the row says it.
// - What it asks you, with its own buttons, in the warning tone (a lean and a glow, never a bar).
// - One field to tell it something, with send inside the field.
// A person with no Actor sees nothing here. Everything people or the Actor wrote is set as text, never markup. The
// block is built once: a refresh repaints what changed and never touches the line being typed. Every control is 44 px.
// ═══════════════════════════════════════════════════════════════
(function (root) {
'use strict';
if (typeof document === 'undefined' || root.OpenVibeDockActor) return;
const POLL_MS = 15000;
const ACTOR_REV = { 'ov-window.js': '01be12417e83', 'agent-sheet.js': '8d9980041b99' };
const SCRIPT_BASE = document.currentScript && document.currentScript.src ? new URL('.', document.currentScript.src).href : '/shared/';
const STATE = { working: 'Working', resting: 'Resting', paused: 'Paused', waiting: 'Waiting for you' };
// The lines Actor sends as "the current step" when there is none: the state already says them.
const NOT_A_STEP = /^(paused|resting\b.*)\.?$/i;
const ICON = {
play: '',
pause: '',
go: '',
send: '',
ask: '',
past: '',
};
function styles(doc) {
const d = doc || document;
if (!d || d.getElementById('ovdac-styles')) return;
const st = d.createElement('style');
st.id = 'ovdac-styles';
st.textContent = `
.ovdac { display: none; flex: none; padding: 12px 12px 8px; border-bottom: 1px solid var(--border, rgba(255,255,255,.08));
--ovdac-ok: var(--success, #22c55e); --ovdac-warn: var(--warning, #f5a524); --ovdac-ink: var(--text-primary, #ececf1); }
.ovdac.on { display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; }
.ovdac [hidden] { display: none !important; }
.ovdac svg { width: 18px; height: 18px; flex: none; }
.ovdac .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
.ovdac-head { display: flex; align-items: center; gap: 8px; min-width: 0; }
.ovdac-past { flex: none; display: grid; place-items: center; width: 44px; height: 44px; padding: 0; border: 0; border-radius: 12px; cursor: pointer;
background: color-mix(in srgb, var(--ovdac-ink, #e6edf7) 6%, transparent); color: var(--text-secondary, #a8b3c4); }
.ovdac-past:hover, .ovdac-past[aria-expanded="true"] { color: var(--ovdac-ink, #e6edf7); background: color-mix(in srgb, var(--accent, #3b82f6) 18%, transparent); }
.ovdac-past svg { width: 19px; height: 19px; }
.ovdac-past.is-busy svg { animation: ovdac-spin-past 1s ease-in-out infinite; }
@keyframes ovdac-spin-past { 50% { opacity: .4; } }
html[data-ov-motion=off] .ovdac-past.is-busy svg { animation: none; }
.ovdac-foot:has(.ovdac-note:empty) { display: none; }
.ovdac-id { flex: 1; min-width: 0; display: flex; align-items: center; gap: 12px; min-height: 48px; padding: 2px 8px 2px 4px; margin-left: -4px; border-radius: 14px;
color: inherit; text-decoration: none; transition: background .15s ease; }
.ovdac-id:hover { background: color-mix(in srgb, var(--ovdac-ink) 5%, transparent); }
.ovdac-id:focus-visible, .ovdac-go:focus-visible, .ovdac-btn:focus-visible, .ovdac-link:focus-visible, .ovdac-send:focus-visible { outline: 2px solid var(--accent-light, var(--accent, #3b82f6)); outline-offset: 2px; }
.ovdac-face { position: relative; flex: none; width: 40px; height: 40px; border-radius: 50%; display: grid; place-items: center;
background: var(--accent, #3b82f6); color: var(--on-accent, #fff); font-weight: 800; font-size: 16px; transition: filter .2s ease, opacity .2s ease; }
.ovdac-face img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; }
.ovdac-face::after { content: ''; position: absolute; inset: -4px; border-radius: 50%; border: 2px solid transparent; transition: border-color .2s ease; }
.ovdac[data-state="working"] .ovdac-face::after { border-color: var(--ovdac-ok); animation: ovdac-ring 2.4s ease-in-out infinite; }
.ovdac[data-state="waiting"] .ovdac-face::after { border-color: var(--ovdac-warn); }
.ovdac[data-state="paused"] .ovdac-face { filter: saturate(.5); opacity: .78; }
.ovdac-who { min-width: 0; display: grid; gap: 1px; }
.ovdac-name { display: flex; align-items: center; gap: 1px; min-width: 0; font-weight: 750; font-size: 15px; line-height: 1.25; color: var(--ovdac-ink); }
.ovdac-name span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ovdac-name svg { width: 16px; height: 16px; color: var(--text-muted, #8b8b96); transition: transform .18s ease, color .18s ease; }
.ovdac-id:hover .ovdac-name svg { transform: translateX(2px); color: var(--text-secondary, #b6b6c0); }
.ovdac-line { display: flex; gap: 6px; min-width: 0; font-size: 12.5px; line-height: 1.35; color: var(--text-muted, #8b8b96); white-space: nowrap; }
.ovdac-state { flex: none; font-weight: 650; color: var(--text-secondary, #b6b6c0); }
.ovdac[data-state="working"] .ovdac-state { color: color-mix(in srgb, var(--ovdac-ok) 72%, var(--ovdac-ink)); }
.ovdac[data-state="waiting"] .ovdac-state { color: color-mix(in srgb, var(--ovdac-warn) 76%, var(--ovdac-ink)); }
.ovdac-spend { min-width: 0; overflow: hidden; text-overflow: ellipsis; font-variant-numeric: tabular-nums; }
.ovdac-spend::before { content: '·'; margin-right: 6px; }
.ovdac-go { flex: none; display: inline-flex; align-items: center; justify-content: center; gap: 6px; min-width: 44px; height: 44px; padding: 0; border-radius: 999px;
border: 1px solid var(--border, rgba(255,255,255,.14)); background: transparent; color: var(--text-secondary, #b6b6c0);
font: inherit; font-size: 13.5px; font-weight: 700; cursor: pointer; white-space: nowrap; transition: background .15s ease, color .15s ease, transform .12s ease, box-shadow .2s ease; }
.ovdac-go:hover { color: var(--ovdac-ink); background: color-mix(in srgb, var(--ovdac-ink) 7%, transparent); }
.ovdac-go.is-main { padding: 0 18px 0 13px; border-color: transparent; background: var(--accent-strong, var(--accent, #2563eb)); color: var(--on-accent-strong, #fff);
box-shadow: 0 8px 20px -10px var(--accent, #3b82f6); }
.ovdac-go.is-main:hover { background: var(--accent-strong, var(--accent, #2563eb)); color: var(--on-accent-strong, #fff); filter: brightness(1.08); }
.ovdac-go:active, .ovdac-btn:active, .ovdac-send:active { transform: scale(.96); }
.ovdac .is-busy { opacity: .6; pointer-events: none; animation: ovdac-pulse 1.1s ease-in-out infinite; }
.ovdac-rail { list-style: none; margin: 0; padding: 2px 4px 2px 6px; display: grid; gap: 9px; }
.ovdac-rail li { position: relative; padding-left: 22px; font-size: 12.5px; line-height: 1.4; color: var(--text-secondary, #b6b6c0); overflow-wrap: anywhere; }
.ovdac-rail li::before { content: ''; position: absolute; left: 1px; top: .36em; width: 6px; height: 6px; border-radius: 50%; border: 1.5px solid var(--text-muted, #8b8b96); }
.ovdac-rail li:not(:last-child)::after { content: ''; position: absolute; left: 5px; top: calc(.36em + 13px); bottom: -8px; width: 1px;
background: color-mix(in srgb, var(--ovdac-ink) 14%, transparent); }
.ovdac-rail .is-live { font-size: 13.5px; font-weight: 650; color: var(--ovdac-ink); }
.ovdac-rail .is-live::before { border-color: var(--ovdac-ok); background: var(--ovdac-ok); box-shadow: 0 0 0 4px color-mix(in srgb, var(--ovdac-ok) 20%, transparent); animation: ovdac-pulse 1.8s ease-in-out infinite; }
.ovdac-rail .is-next { color: var(--text-muted, #8b8b96); }
.ovdac-rail .is-next::before { border-style: dashed; }
.ovdac-rail b { font-weight: 650; }
.ovdac-look { display: block; margin-top: 2px; font-size: 12px; font-weight: 400; color: var(--text-muted, #8b8b96); }
.ovdac-ask { display: grid; gap: 10px; padding: 12px 14px 10px; border-radius: 16px;
background: linear-gradient(135deg, color-mix(in srgb, var(--ovdac-warn) 15%, transparent), color-mix(in srgb, var(--ovdac-warn) 5%, transparent) 62%);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ovdac-warn) 22%, transparent), 0 12px 28px -20px var(--ovdac-warn); }
.ovdac-ask-k { display: flex; align-items: center; gap: 8px; font-size: 12px; font-weight: 700; color: color-mix(in srgb, var(--ovdac-warn) 74%, var(--ovdac-ink)); }
.ovdac-ask-k i { display: grid; place-items: center; width: 24px; height: 24px; border-radius: 50%; background: color-mix(in srgb, var(--ovdac-warn) 24%, transparent); }
.ovdac-ask-k svg { width: 16px; height: 16px; }
.ovdac-ask p { margin: 0; font-size: 14px; line-height: 1.45; color: var(--ovdac-ink); overflow-wrap: anywhere; }
.ovdac-ask .row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.ovdac-btn { display: inline-flex; align-items: center; justify-content: center; min-height: 44px; padding: 0 16px; border-radius: 999px;
border: 1px solid color-mix(in srgb, var(--ovdac-ink) 18%, transparent); background: transparent; color: var(--ovdac-ink);
font: inherit; font-size: 13.5px; font-weight: 700; cursor: pointer; white-space: nowrap; transition: background .15s ease, transform .12s ease; }
.ovdac-btn:hover { background: color-mix(in srgb, var(--ovdac-ink) 8%, transparent); }
.ovdac-btn.is-yes { background: var(--accent-strong, var(--accent, #2563eb)); border-color: transparent; color: var(--on-accent-strong, #fff); }
.ovdac-btn.is-yes:hover { background: var(--accent-strong, var(--accent, #2563eb)); filter: brightness(1.08); }
.ovdac-btn.is-quiet { padding: 0 10px; border-color: transparent; color: var(--text-secondary, #b6b6c0); font-weight: 650; }
.ovdac-more { display: inline-flex; align-items: center; min-height: 44px; margin: -8px 0 -8px; font-size: 12.5px; color: var(--text-secondary, #b6b6c0); text-decoration: underline; text-underline-offset: 3px; }
.ovdac-tell { position: relative; display: flex; }
.ovdac-tell input { flex: 1; min-width: 0; height: 52px; box-sizing: border-box; padding: 0 58px 0 18px; border-radius: 26px;
border: 1px solid var(--border, rgba(255,255,255,.14)); background: var(--bg-input, color-mix(in srgb, var(--ovdac-ink) 4%, transparent));
color: var(--ovdac-ink); font: inherit; font-size: 14.5px; outline: none; transition: border-color .15s ease, box-shadow .15s ease; }
.ovdac-tell input::placeholder { color: var(--text-muted, #8b8b96); }
.ovdac-tell input:focus { border-color: color-mix(in srgb, var(--accent, #3b82f6) 62%, transparent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent, #3b82f6) 18%, transparent); }
.ovdac-send { position: absolute; right: 4px; top: 4px; display: grid; place-items: center; width: 44px; height: 44px; padding: 0; border: 0; border-radius: 50%;
background: color-mix(in srgb, var(--ovdac-ink) 8%, transparent); color: var(--text-muted, #8b8b96); cursor: pointer;
transition: background .18s ease, color .18s ease, transform .12s ease, box-shadow .2s ease; }
.ovdac-tell.has-text .ovdac-send { background: var(--accent-strong, var(--accent, #2563eb)); color: var(--on-accent-strong, #fff); box-shadow: 0 6px 16px -8px var(--accent, #3b82f6); }
.ovdac-foot { display: flex; align-items: center; gap: 10px; min-width: 0; margin-top: -6px; }
.ovdac-link { flex: none; display: inline-flex; align-items: center; gap: 6px; min-height: 44px; padding: 0 8px; margin-left: -8px; border: 0; border-radius: 10px;
background: none; color: var(--text-muted, #8b8b96); font: inherit; font-size: 12.5px; font-weight: 650; cursor: pointer; transition: color .15s ease; }
.ovdac-link:hover, .ovdac-link[aria-expanded="true"] { color: var(--ovdac-ink); }
.ovdac-link svg { width: 15px; height: 15px; }
.ovdac-note { flex: 1; min-width: 0; text-align: right; font-size: 12.5px; line-height: 1.35; color: var(--text-muted, #8b8b96); overflow-wrap: anywhere; }
.ovdac-note.is-ok { color: color-mix(in srgb, var(--ovdac-ok) 70%, var(--ovdac-ink)); }
.ovdac-note.is-bad { color: color-mix(in srgb, var(--danger, #ef4444) 74%, var(--ovdac-ink)); }
.ovdac-hist { list-style: none; margin: -4px 0 4px; padding: 0; display: grid; gap: 6px; font-size: 12.5px; line-height: 1.4; color: var(--text-secondary, #b6b6c0); overflow-wrap: anywhere; }
.ovdac-hist b { margin-right: 6px; font-weight: 650; color: var(--text-muted, #8b8b96); }
@keyframes ovdac-pulse { 50% { opacity: .45; } }
@keyframes ovdac-ring { 50% { opacity: .35; transform: scale(1.06); } }
@media (pointer: coarse) { .ovdac-tell input { font-size: 16px; } }
html[data-ov-motion=off] .ovdac *, html[data-ov-motion=off] .ovdac *::before, html[data-ov-motion=off] .ovdac *::after { animation: none !important; transition: none !important; }
`;
(d.head || d.documentElement).appendChild(st);
}
const money = (n) => { const v = Number(n) || 0; return v < 0.01 && v > 0 ? `$${v.toFixed(3)}` : `$${v.toFixed(2)}`; };
/** "in 40 min", "in 3 h", "tomorrow": when the next goal is due; '' when unknown or already due. */
function due(at, now = Date.now()) {
const t = Date.parse(at || '');
if (!Number.isFinite(t)) return '';
const min = Math.round((t - now) / 60000);
if (min < 1) return '';
if (min < 60) return `in ${min} min`;
if (min < 24 * 60) return `in ${Math.round(min / 60)} h`;
return min < 48 * 60 ? 'tomorrow' : `in ${Math.round(min / 1440)} days`;
}
function mount(container, opts = {}) {
const d = container.ownerDocument || document;
styles(d);
const o = { ...opts };
const h = (tag, cls, text) => { const n = d.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = text; return n; };
const icon = (name) => { const t = d.createElement('template'); t.innerHTML = ICON[name]; return t.content.firstChild; };
const tip = (n, text) => { if (root.OVTip) n.setAttribute('data-ov-tip', text); else n.title = text; };
let gone = false, timer = 0, busy = false, shown = false, last = '', actor = null, noteTimer = 0;
container.replaceChildren();
container.classList.add('ovdac');
// Network's frame relay on a site; an explicit base reaches Actor itself (a test).
const direct = o.base ? String(o.base).replace(/\/+$/, '') : '';
const frameBase = String(o.frameBase || 'https://openvibe.network/api/frame').replace(/\/+$/, '');
const endpoint = (path) => (direct ? direct + '/api/v1/me/actor' + path : frameBase + '/actor' + path);
// The way to the Actor's page is a navigation, never a fetch: Actor's own site, wherever the API is.
const openBase = direct || 'https://openvibe.actor';
async function api(path, init = {}) {
const t = typeof o.token === 'function' ? await o.token() : o.token;
if (!t) throw Object.assign(new Error('Sign in to use your Actor'), { status: 401 });
const headers = { accept: 'application/json', ...(init.json ? { 'content-type': 'application/json' } : {}), authorization: `Bearer ${t}` };
const r = await fetch(endpoint(path), { method: init.method || 'GET', headers, credentials: 'omit', body: init.json ? JSON.stringify(init.body || {}) : undefined });
const b = await r.json().catch(() => ({}));
if (!r.ok) throw Object.assign(new Error(b.detail || `HTTP ${r.status}`), { status: r.status });
return b;
}
function stateOf(a) {
if (Array.isArray(a.asks) && a.asks.length) return 'waiting';
return STATE[a.status] ? a.status : 'paused';
}
// ── The parts, built once ────────────────────────────────────────────────────────────────────────────────
const head = h('div', 'ovdac-head');
const id = h('a', 'ovdac-id'); id.href = openBase + '/you'; id.target = '_blank'; id.rel = 'noopener';
function loadScript(file, ready) {
if (ready()) return Promise.resolve(true);
return new Promise((resolve) => {
const sid = `ovdac-${file}`;
let script = d.getElementById(sid);
if (!script) { script = d.createElement('script'); script.id = sid; script.src = SCRIPT_BASE + file + (ACTOR_REV[file] ? `?v=${ACTOR_REV[file]}` : ''); script.async = true; d.head.appendChild(script); }
script.addEventListener('load', () => resolve(!!ready()), { once: true });
script.addEventListener('error', () => resolve(false), { once: true });
});
}
// The row opens its sheet (agent-sheet.js in the one window: now, asks, work, schedule, memory, computer); the
// Actor's page is the sheet's "Open". A modified click, or a sheet that cannot load, goes to the page as before.
id.addEventListener('click', async (e) => {
if (!actor || !(actor.agent_id || actor.id) || e.metaKey || e.ctrlKey || e.shiftKey || e.button) return;
e.preventDefault();
const ok = (await Promise.all([loadScript('ov-window.js', () => !!root.OVWindow), loadScript('agent-sheet.js', () => !!root.OVAgentSheet)])).every(Boolean);
if (!ok) { d.defaultView.open(id.href, '_blank', 'noopener'); return; }
root.OVAgentSheet.open({ kind: 'actor', id: actor.agent_id || actor.id, name: actor.name || 'Your Actor', token: o.token,
frameBase: o.frameBase, base: direct || null, from: id, openHref: openBase + '/you' });
});
const face = h('span', 'ovdac-face');
const who = h('span', 'ovdac-who');
const name = h('span', 'ovdac-name'); const nameText = h('span'); name.append(nameText, icon('go'));
const line = h('span', 'ovdac-line'); const stateText = h('span', 'ovdac-state'); const spend = h('span', 'ovdac-spend'); line.append(stateText, spend);
who.append(name, line); id.append(face, who);
const go = h('button', 'ovdac-go'); go.type = 'button';
go.addEventListener('click', () => {
if (!actor) return;
const paused = actor.status === 'paused';
act(paused ? '/resume' : '/pause', {}, go, paused ? 'Resumed.' : 'Paused.');
});
head.append(id, go);
const rail = h('ul', 'ovdac-rail'); rail.hidden = true;
const askBox = h('div', 'ovdac-ask'); askBox.hidden = true;
const form = h('form', 'ovdac-tell');
const input = h('input'); input.type = 'text'; input.maxLength = 2000; input.autocomplete = 'off'; input.enterKeyHint = 'send';
input.placeholder = 'Tell your Actor something'; input.setAttribute('aria-label', 'Tell your Actor something');
const send = h('button', 'ovdac-send'); send.type = 'submit'; send.setAttribute('aria-label', 'Send'); send.append(icon('send'));
input.addEventListener('input', () => form.classList.toggle('has-text', !!input.value.trim()));
form.append(input, send);
form.addEventListener('submit', (e) => {
e.preventDefault();
const text = input.value.trim();
if (!text) { input.focus(); return; }
const paused = actor && actor.status === 'paused';
act('/tell', { text }, send, paused ? 'Sent. Your Actor reads it once you resume it.' : 'Sent. Your Actor reads it at its next step.')
.then((ok) => { if (ok) { input.value = ''; form.classList.remove('has-text'); } });
});
const foot = h('div', 'ovdac-foot');
// 4.238.0 (the owner, 11-13: a "Recent history" line was a waste of space): a clock in the Actor's own row.
const past = h('button', 'ovdac-past'); past.type = 'button'; past.setAttribute('aria-expanded', 'false'); past.append(icon('past'));
past.setAttribute('aria-label', 'Your Actor\'s recent steps'); past.setAttribute('data-ov-tip', 'Recent steps');
head.insertBefore(past, go);
const note = h('div', 'ovdac-note'); note.setAttribute('role', 'status'); note.setAttribute('aria-live', 'polite');
const hist = h('ul', 'ovdac-hist'); hist.hidden = true;
foot.append(note);
// The button that opens the list closes it.
past.addEventListener('click', async () => {
if (!hist.hidden) { hist.hidden = true; past.setAttribute('aria-expanded', 'false'); return; }
past.disabled = true; past.classList.add('is-busy');
const one = (text) => { hist.replaceChildren(h('li', '', text)); };
try {
const t = typeof o.token === 'function' ? await o.token() : o.token;
if (!t) throw new Error('Sign in to read history.');
const url = String(o.networkBase || 'https://openvibe.network').replace(/\/+$/, '') + '/api/v1/history/agents/recent?limit=10';
const response = await fetch(url, { headers: { accept: 'application/json', authorization: `Bearer ${t}` }, credentials: 'omit' });
const result = await response.json().catch(() => ({}));
if (response.status === 403) one('Your agent history setting is off. Turn it on in My settings to allow this read.');
else if (!response.ok) one('Recent history is unavailable.');
else {
const items = (result.items || []).slice(0, 10);
if (!items.length) one('No recent history.');
else hist.replaceChildren(...items.map((x) => { const li = h('li'); li.append(h('b', '', String(x.type || '')), d.createTextNode(String(x.title || ''))); return li; }));
}
} catch { one('Recent history is unavailable.'); }
finally { past.disabled = false; past.classList.remove('is-busy'); }
hist.hidden = false; past.setAttribute('aria-expanded', 'true');
});
/** One line under the field: what just happened. A good word goes by itself after a moment; a problem stays. */
function say(text, tone) {
clearTimeout(noteTimer);
note.textContent = text || '';
note.className = 'ovdac-note' + (tone ? ` is-${tone}` : '');
if (text && tone === 'ok') noteTimer = setTimeout(() => { note.textContent = ''; note.className = 'ovdac-note'; }, 6000);
}
// ── A refresh repaints the row, the steps and the ask; the field and the history stay as they are ────────
function paint(a) {
if (!a || (a.enabled === false && !a.id)) {
actor = null; shown = false; last = '';
container.replaceChildren(); container.classList.remove('on'); container.removeAttribute('data-state');
return;
}
actor = a;
if (!shown) { container.replaceChildren(head, rail, askBox, form, foot, hist); container.classList.add('on'); shown = true; }
const sig = JSON.stringify([a.name, a.status, a.avatar, a.brain, a.caps, a.now, a.asks]);
if (sig === last) return;
last = sig;
const state = stateOf(a), now = a.now || {};
container.setAttribute('data-state', state);
// Who, and the one line about it.
const label = String(a.name || 'Your Actor');
const pic = a.avatar && typeof a.avatar.url === 'string' && /^https:\/\//.test(a.avatar.url) ? a.avatar.url : '';
if (pic) { const img = h('img'); img.alt = ''; img.src = pic; face.replaceChildren(img); } else face.replaceChildren(d.createTextNode(label.trim().charAt(0).toUpperCase() || 'A'));
nameText.textContent = label;
id.setAttribute('aria-label', `${label}, ${STATE[state]}. Open your Actor's page`);
stateText.textContent = STATE[state];
const cap = a.caps && Number(a.caps.usd) > 0 ? ` of ${money(a.caps.usd)}` : '';
spend.textContent = `${money(now.cost_today)}${cap} today`;
tip(spend, `Model: ${a.brain && a.brain !== 'auto' ? a.brain : 'Auto'}`);
// The one action that fits.
const paused = a.status === 'paused';
go.className = 'ovdac-go' + (paused ? ' is-main' : '');
go.replaceChildren(icon(paused ? 'play' : 'pause'), h('span', paused ? '' : 'sr', paused ? 'Resume' : 'Pause'));
tip(go, paused ? 'Your Actor does nothing until you resume it' : 'Pause your Actor');
// What it does: the step it is on, the steps before it, what comes next. Nothing when there is nothing to say.
const asks = Array.isArray(a.asks) ? a.asks : [];
const ask = asks[0];
const step = String(now.current_step || '').trim();
const live = a.status === 'working' && step && !NOT_A_STEP.test(step) && !(ask && step === String(ask.text || '').trim()) ? step : '';
const items = [];
if (live) {
const li = h('li', 'is-live'); li.append(d.createTextNode(live));
const looking = now.looking_at && (now.looking_at.title || now.looking_at);
if (looking) li.append(h('span', 'ovdac-look', `Looking at ${String(looking)}`));
items.push(li);
}
const before = (Array.isArray(now.last_three_steps) ? now.last_three_steps : []).map((x) => String((x && x.text) || x || '').trim()).filter((t) => t && t !== live).slice(0, 3);
if (!paused) before.forEach((t) => items.push(h('li', '', t)));
if (!paused && now.next && (now.next.text || typeof now.next === 'string')) {
const li = h('li', 'is-next'); const when = due(now.next.at);
li.append(h('b', '', 'Next: '), d.createTextNode(String(now.next.text || now.next) + (when ? `, ${when}` : '')));
items.push(li);
}
rail.replaceChildren(...items); rail.hidden = !items.length;
// The first thing it asks, with its own buttons.
if (ask) {
const k = h('div', 'ovdac-ask-k'); const chip = h('i'); chip.append(icon('ask')); k.append(chip, d.createTextNode(`${label} asks`));
const row = h('div', 'row');
const options = Array.isArray(ask.options) && ask.options.length ? ask.options : [{ id: 'yes', label: 'Yes' }];
options.slice(0, 3).forEach((op, i) => {
const b = h('button', `ovdac-btn${i === 0 ? ' is-yes' : ''}`, String(op.label || op)); b.type = 'button';
b.addEventListener('click', () => act(`/asks/${encodeURIComponent(ask.id)}/answer`, { option: op.id || op }, b, 'Answered.'));
row.append(b);
});
const no = h('button', 'ovdac-btn is-quiet', 'Not now'); no.type = 'button';
no.addEventListener('click', () => act(`/asks/${encodeURIComponent(ask.id)}/decline`, {}, no));
row.append(no);
askBox.replaceChildren(k, h('p', '', String(ask.text || '')), row);
if (asks.length > 1) { const more = h('a', 'ovdac-more', `${asks.length - 1} more on your Actor's page`); more.href = openBase + '/you'; more.target = '_blank'; more.rel = 'noopener'; askBox.append(more); }
} else askBox.replaceChildren();
askBox.hidden = !ask;
}
async function act(path, body, btn, done) {
if (busy) return false;
busy = true; if (btn) { btn.disabled = true; btn.classList.add('is-busy'); }
say('');
try {
await api(path, { method: 'POST', json: true, body });
last = '';
await refresh();
if (done) say(done, 'ok');
return true;
} catch (err) { say(`That did not go through: ${err.message}`, 'bad'); return false; }
finally { busy = false; if (btn) { btn.disabled = false; btn.classList.remove('is-busy'); } }
}
async function refresh() {
if (gone) return;
if (o.active && !o.active()) return;
try { const data = await api(''); if (!gone) paint(data); }
catch (err) { if (!gone && (err.status === 401 || err.status === 403 || err.status === 404)) paint(null); }
}
refresh();
timer = setInterval(refresh, POLL_MS);
return { refresh, destroy() { gone = true; clearInterval(timer); clearTimeout(noteTimer); container.replaceChildren(); container.classList.remove('on'); container.removeAttribute('data-state'); } };
}
root.OpenVibeDockActor = { mount };
})(typeof window !== 'undefined' ? window : globalThis);