/** * ov-run.js (openvibe-shared 4.224.0): the one run thread. Everything a person asks an AI to do on OpenVibe is a run * with a thread of steps (a chat with Vibe, a task for an agent, a coding task); this draws every one of them the same * way, on every site, from the Frame's Run API (Network /api/frame/runs, Contracts `agents.run@1`, `agents.step@1`, * `agents.event@1`). The owner, 11-12, crossing out the menu's Chat · Code · Agents switch: "one new unified agent … * a shared UI … a full conversation with rich embedding … agent mode". * * const view = OpenVibeRun.mount(host, { * base: 'https://openvibe.network/api/frame', // the Frame (default) * token: () => token, // the person's Network token (or a string) * me: { name, avatar_url }, // the person, for their own bubbles * sibling: (file) => url, // where ov-composer.js is (the navbar's sibling()) * examples: ['A goal bar for my stream', …], // what people make, shown in turn in the empty box * start: { kind, target, context }, // what a new run starts with (the box's defaults) * }); * view.list('code'); view.open('chat:c_8f3k2m'); view.start('A goal bar for my stream'); view.prefill('Words, unsent'); * view.destroy(); * (o.filter: the list's first filter, all | chat | task | code; o.open: a run's ref to open first) * * 4.225.0, what the menu's Vibe needs to be the one place (the Dock's Vibe and Agents panels did all of it): * o.starters: [[glyph, words], …] things to ask, as chips above the box (All and Chats); a press starts that run * o.actions: { base } the Action Center's waiting and running work from every product (Network * GET /api/v1/me/actions), merged into the list (All and Tasks) when a run does not * already show it; approved or declined in place, and opened on its own page * o.top(el) → { destroy } something of the host's above the list (All and Tasks): the menu puts the Dock's * Now, your Actor and the live agent view there * o.panes: { chat, task, code } a filter the host still draws itself: pane(el) → { destroy }. The menu's Vibe * lends the Dock's Vibe, Agents and Code panels here until this view does all * they do; the pane replaces the rows and the box under the filters * o.onPlace({ filter } | { ref }) where the person is, for the host to remember * view.prefill(text) the words in the list's box, unsent (Ask your agent) * * OpenVibeRun.card(type, (data, step, ctx) => Node) a rich card for a step's `ui.type` (a site or a mod adds its * own); an unknown type shows the step's words. * * The list: needs-you first, then newest, filtered All · Chats · Tasks · Code; each run is its agent's face (glyph and * hue, or its picture, with what it is doing), its title, its last line and when. The thread: your words on the right, * the agent's on the left (paragraphs, lists, links, code, numbered sources), thinking folded, each tool one quiet row, * an approval as a card with its buttons, child runs as rows, files as chips. It follows the run live (the stream, * resumed after its last step) and the composer below says the next thing, or starts a new run from the list. */ (function (root) { 'use strict'; if (root.OpenVibeRun || typeof document === 'undefined') return; // ── glyphs (the registry's drawings, openvibe-shared/glyphs.js; 24-unit strokes) ── const G = { back: '', open: '', send: '', plus: '', spark: '', chat: '', code: '', robot: '', tool: '', think: '', file: '', check: '', x: '', pr: '', link: '', child: '', clock: '', search: '', book: '', pencil: '', live: '', store: '', game: '', compass: '', page: '', timer: '', bell: '', calendar: '', phone: '', }; const KIND_GLYPH = { chat: 'chat', ask: 'search', task: 'robot', code: 'code', machine: 'tool', goal: 'spark' }; const KIND_WORD = { chat: 'Chat', ask: 'Answer', task: 'Task', code: 'Code', machine: 'Machine', goal: 'Goal' }; const SERVICE_NAME = { actor: 'Actor', codes: 'Codes', ai: 'AI', network: 'OpenVibe', run: 'Run', domains: 'Domains' }; const svg = (name, cls = '') => { const s = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); s.setAttribute('viewBox', '0 0 24 24'); s.setAttribute('aria-hidden', 'true'); if (cls) s.setAttribute('class', cls); s.innerHTML = G[name] || G.spark; // the registry's constants above, never page text return s; }; const el = (tag, cls, text) => { const n = document.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = String(text); return n; }; const httpsUrl = (v) => { try { const u = new URL(String(v)); return u.protocol === 'https:' ? u.href : ''; } catch { return ''; } }; const hueOf = (id) => { let x = 7; for (const c of String(id || 'ov')) x = (x * 31 + c.codePointAt(0)) % 360; return x; }; const ago = (iso) => { const t = Date.parse(iso || ''); if (!t) return ''; const s = Math.max(1, Math.round((Date.now() - t) / 1000)); if (s < 60) return 'now'; if (s < 3600) return `${Math.round(s / 60)} min ago`; if (s < 86400) return `${Math.round(s / 3600)} h ago`; const d = Math.round(s / 86400); return d === 1 ? 'yesterday' : `${d} days ago`; }; const needsPerson = (r) => !!r && r.status === 'waiting' && !!r.waiting && r.waiting.for !== 'message'; const isEnd = (r) => !!r && ['done', 'failed', 'stopped'].includes(r.status); // ── rich cards ── const CARDS = new Map(); function card(type, render) { if (typeof type === 'string' && /^[a-z][a-z0-9-]{0,40}$/.test(type) && typeof render === 'function') CARDS.set(type, render); } card('results', (data) => { const row = el('div', 'ovr-chips'); for (const r of (Array.isArray(data.results) ? data.results : []).slice(0, 12)) { const c = el('span', 'ovr-chip'); c.append(svg(r.kind === 'timer' ? 'timer' : r.kind === 'reminder' ? 'clock' : 'check'), el('span', '', r.label || r.text || r.kind || 'Done')); if (r.undone) c.classList.add('is-undone'); row.append(c); } return row; }); card('table', (data) => { const wrap = el('div', 'ovr-table-wrap'); const t = el('table', 'ovr-table'); const cols = Array.isArray(data.columns) ? data.columns.slice(0, 8) : []; if (cols.length) { const tr = el('tr'); cols.forEach((c) => tr.append(el('th', '', c))); const th = el('thead'); th.append(tr); t.append(th); } const tb = el('tbody'); for (const row of (Array.isArray(data.rows) ? data.rows : []).slice(0, 50)) { const tr = el('tr'); (Array.isArray(row) ? row : []).slice(0, 8).forEach((v) => tr.append(el('td', '', v))); tb.append(tr); } t.append(tb); wrap.append(t); return wrap; }); card('link', (data) => { const url = httpsUrl(data.url); if (!url) return null; const a = el('a', 'ovr-linkcard'); a.href = url; a.target = '_blank'; a.rel = 'noopener noreferrer'; const img = httpsUrl(data.image); if (img) { const i = el('img'); i.src = img; i.alt = ''; i.loading = 'lazy'; a.append(i); } const t = el('span', 'ovr-linkcard-t'); t.append(el('b', '', data.title || url), el('small', '', data.site || new URL(url).hostname)); a.append(t); return a; }); card('code', (data) => { const pre = el('pre', 'ovr-code'); pre.append(el('code', '', String(data.code || data.text || '').slice(0, 20000))); return pre; }); // ── words: paragraphs, lists, links, inline code, numbered sources ── function words(text, sources) { const box = el('div', 'ovr-words'); const raw = String(text || '').replace(/\r\n/g, '\n').slice(0, 65536); const blocks = raw.split(/\n{2,}/); for (const b of blocks) { const lines = b.split('\n'); if (lines.every((l) => /^\s*[-*•]\s+/.test(l) || !l.trim())) { const ul = el('ul'); lines.filter((l) => l.trim()).forEach((l) => { const li = el('li'); inline(li, l.replace(/^\s*[-*•]\s+/, ''), sources); ul.append(li); }); box.append(ul); } else if (lines.every((l) => /^\s*\d+[.)]\s+/.test(l) || !l.trim())) { const ol = el('ol'); lines.filter((l) => l.trim()).forEach((l) => { const li = el('li'); inline(li, l.replace(/^\s*\d+[.)]\s+/, ''), sources); ol.append(li); }); box.append(ol); } else if (/^```/.test(b.trim())) { const pre = el('pre', 'ovr-code'); pre.append(el('code', '', b.trim().replace(/^```[a-z0-9]*\n?/i, '').replace(/```$/, ''))); box.append(pre); } else { const p = el('p'); lines.forEach((l, i) => { if (i) p.append(el('br')); inline(p, l, sources); }); box.append(p); } } return box; } function inline(node, text, sources) { const re = /(https?:\/\/[^\s<>"')\]]+)|`([^`]{1,200})`|\*\*([^*]{1,200})\*\*|\[(\d{1,2})\]/g; let last = 0, m; while ((m = re.exec(text))) { if (m.index > last) node.append(document.createTextNode(text.slice(last, m.index))); if (m[1]) { const a = el('a', 'ovr-a', m[1]); a.href = m[1]; a.target = '_blank'; a.rel = 'nofollow noopener noreferrer'; node.append(a); } else if (m[2]) node.append(el('code', 'ovr-ic', m[2])); else if (m[3]) node.append(el('strong', '', m[3])); else if (m[4]) { const src = (sources || []).find((s) => Number(s.n) === Number(m[4])); const url = src && httpsUrl(src.url); if (url) { const a = el('a', 'ovr-cite', m[4]); a.href = url; a.target = '_blank'; a.rel = 'nofollow noopener noreferrer'; a.title = src.title || src.site || url; node.append(a); } else node.append(document.createTextNode(m[0])); } last = m.index + m[0].length; } if (last < text.length) node.append(document.createTextNode(text.slice(last))); } // ── an agent's face: its picture, or its glyph on its hue; what it is doing as a small badge ── function face(run, size = 'm') { const a = (run && run.agent) || {}; const f = el('span', `ovr-face is-${size}`); f.style.setProperty('--ovr-h', String(Number.isInteger(a.hue) ? a.hue : hueOf(a.id || (run && run.kind)))); const pic = httpsUrl(a.avatar_url); if (pic) { const i = el('img'); i.src = pic; i.alt = ''; i.loading = 'lazy'; f.append(i); } else f.append(svg(G[a.glyph] ? a.glyph : KIND_GLYPH[run && run.kind] || 'spark')); if (run && run.doing && run.status === 'running') { const d = el('i', 'ovr-doing'); d.append(svg(G[run.doing.glyph] ? run.doing.glyph : 'spark')); f.append(d); f.classList.add('is-working'); } if (needsPerson(run)) f.classList.add('is-needs'); return f; } function statusLine(run) { if (!run) return ''; if (needsPerson(run)) return (run.waiting && run.waiting.what) || 'Needs you'; if (run.status === 'running') return (run.doing && run.doing.text) || 'Working'; if (run.status === 'planning') return 'Starting'; if (run.status === 'waiting') return 'Your turn'; return { done: 'Done', failed: 'Did not finish', stopped: 'Stopped' }[run.status] || ''; } // ── the stream: a fetch reader over server-sent events (EventSource cannot send a bearer) ── function follow({ url, headers, after, onEvent, signal }) { let last = String(after || 0); let stopped = false; const once = async () => { const r = await fetch(url, { headers: { ...headers, Accept: 'text/event-stream', 'Last-Event-ID': last }, signal, credentials: 'omit' }); if (!r.ok || !r.body) throw new Error(`stream ${r.status}`); const reader = r.body.getReader(); const dec = new TextDecoder(); let buf = ''; for (;;) { const { value, done } = await reader.read(); if (done) return 'closed'; buf += dec.decode(value, { stream: true }); let i; while ((i = buf.indexOf('\n\n')) >= 0) { const raw = buf.slice(0, i); buf = buf.slice(i + 2); let id = null, event = 'message', data = ''; for (const line of raw.split('\n')) { if (line.startsWith('id:')) id = line.slice(3).trim(); else if (line.startsWith('event:')) event = line.slice(6).trim(); else if (line.startsWith('data:')) data += line.slice(5).trim(); } let d = null; try { d = JSON.parse(data); } catch { d = null; } if (event === 'step' && id && /^\d+$/.test(id) && Number(id) > Number(last)) last = id; if (onEvent(event, d) === 'end') return 'end'; } } }; (async () => { let failures = 0; while (!stopped && !(signal && signal.aborted)) { try { const how = await once(); failures = 0; if (how === 'end') return; } catch { if (signal && signal.aborted) return; failures++; } await new Promise((r) => setTimeout(r, Math.min(30000, 800 * 2 ** Math.min(failures, 5)))); } })(); return () => { stopped = true; }; } const FILTERS = [['all', 'All', 'spark'], ['chat', 'Chats', 'chat'], ['task', 'Tasks', 'robot'], ['code', 'Code', 'code']]; const FILTER_IDS = FILTERS.map(([id]) => id); function mount(host, o = {}) { styles(); const base = String(o.base || 'https://openvibe.network/api/frame').replace(/\/+$/, ''); const tokenOf = () => (typeof o.token === 'function' ? o.token() : o.token) || ''; const headers = () => { const t = tokenOf(); return t ? { Authorization: `Bearer ${t}` } : {}; }; const api = async (path, init = {}) => { const r = await fetch(`${base}/runs${path}`, { ...init, credentials: 'omit', headers: { Accept: 'application/json', ...(init.body ? { 'Content-Type': 'application/json' } : {}), ...headers(), ...(init.headers || {}) } }); const j = await r.json().catch(() => ({})); if (!r.ok) { const e = new Error(j.error || `The runs did not answer (${r.status})`); e.status = r.status; e.code = j.code; throw e; } return j; }; const box = el('div', 'ovr'); host.textContent = ''; host.append(box); const actionsBase = o.actions ? String(o.actions.base || 'https://openvibe.network').replace(/\/+$/, '') : ''; const place = (p) => { try { if (typeof o.onPlace === 'function') o.onPlace(p); } catch { /* the host's */ } }; let topHandle = null, paneHandle = null, paneId = null; const dropTop = () => { if (topHandle && typeof topHandle.destroy === 'function') { try { topHandle.destroy(); } catch { /* */ } } topHandle = null; }; const dropPane = () => { if (paneHandle && typeof paneHandle.destroy === 'function') { try { paneHandle.destroy(); } catch { /* */ } } paneHandle = null; paneId = null; }; const paneFor = (f) => (o.panes && typeof o.panes[f] === 'function' ? o.panes[f] : null); /** The Action Center's items (common.action-item@1) that need you or are working, as rows beside the runs. */ async function actionRows(runs) { if (!actionsBase || !['all', 'task'].includes(state.filter)) return []; try { const r = await fetch(`${actionsBase}/api/v1/me/actions`, { headers: { Accept: 'application/json', ...headers() }, credentials: 'omit' }); if (!r.ok) return []; const doc = await r.json(); const shown = new Set(runs.map((x) => x.ref)); runs.forEach((x) => { if (x.waiting && x.waiting.approval) shown.add(`approval:${x.waiting.approval}`); }); const out = []; for (const bucket of ['waiting', 'running']) { for (const it of (doc && doc.buckets && Array.isArray(doc.buckets[bucket]) ? doc.buckets[bucket] : []).slice(0, 30)) { if (!it || typeof it.id !== 'string' || shown.has(`${it.service}:${it.id}`) || (it.kind === 'approval' && shown.has(`approval:${it.id}`))) continue; out.push({ action: it, id: it.id, kind: it.kind === 'code_task' ? 'code' : 'task', title: it.title, url: it.href, status: bucket === 'waiting' ? 'waiting' : 'running', updated_at: it.updated_at, waiting: bucket === 'waiting' ? { for: 'approval', what: it.summary || it.title } : null, doing: bucket === 'running' ? { text: it.step || it.summary || 'Working', glyph: it.kind === 'code_task' ? 'code' : 'robot' } : null, preview: it.summary ? { text: it.summary } : null, agent: { id: `${it.service}`, name: it.agent || SERVICE_NAME[it.service] || 'OpenVibe', glyph: it.kind === 'code_task' ? 'code' : it.kind === 'ai_run' ? 'spark' : 'robot' }, }); } } return out; } catch { return []; } } const state = { view: 'list', filter: FILTER_IDS.includes(o.filter) ? o.filter : 'all', runs: [], run: null, steps: new Map(), stopStream: null, abort: null, gen: 0 }; function clearStream() { if (state.stopStream) state.stopStream(); state.stopStream = null; if (state.abort) state.abort.abort(); state.abort = null; } // ── the composer: the shared one when it is there, else a plain box ── function composer(where, { placeholder, onSend }) { const slot = el('div', 'ovr-compose'); where.append(slot); const plain = () => { const f = el('form', 'ovr-plain'); const ta = el('textarea'); ta.rows = 2; ta.placeholder = placeholder; ta.setAttribute('aria-label', placeholder); const go = el('button', 'ovr-send'); go.type = 'submit'; go.setAttribute('aria-label', 'Send'); go.append(svg('send')); f.append(ta, go); f.addEventListener('submit', async (e) => { e.preventDefault(); const t = ta.value.trim(); if (!t) return; go.disabled = true; const old = slot.querySelector('.ovr-note'); if (old) old.remove(); try { if (await onSend({ text: t }) !== false) ta.value = ''; } catch (err) { slot.append(el('p', 'ovr-note is-err', (err && err.message) || 'That did not send')); } finally { go.disabled = false; } }); ta.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); f.requestSubmit ? f.requestSubmit() : f.dispatchEvent(new Event('submit', { cancelable: true })); } }); slot.append(f); }; const C = root.OpenVibeComposer; const real = () => { try { // The composer notes a failed send itself and keeps the words. root.OpenVibeComposer.mount(slot, { preset: 'agent', placeholder, label: placeholder, examples: o.examples, user: o.me, noDraft: true, onSubmit: (v) => onSend({ text: v.text, files: v.files, mode: v.mode }) }); } catch { slot.textContent = ''; plain(); } }; if (C && C.mount) real(); else if (typeof o.sibling === 'function') { let sc = document.getElementById('ov-composer-script'); if (!sc) { sc = document.createElement('script'); sc.id = 'ov-composer-script'; sc.async = true; sc.src = o.sibling('ov-composer.js'); document.head.appendChild(sc); } plain(); sc.addEventListener('load', () => { if (slot.isConnected && root.OpenVibeComposer) { slot.textContent = ''; real(); } }, { once: true }); } else plain(); } /** A new run with these words (Vibe answers, or hands the work on), then its thread. */ async function start(text) { const run = await api('', { method: 'POST', headers: { 'Idempotency-Key': `ovr-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}` }, body: JSON.stringify({ ...(o.start || {}), text: String(text || '').slice(0, 16000) }) }); open(run.ref || run.id, run); return run; } // ── the list ── async function list(filter) { if (filter && FILTER_IDS.includes(filter)) state.filter = filter; clearStream(); const gen = ++state.gen; // The list's frame (filters, the box) is drawn once; a filter only reloads the rows, so the words being typed stay. if (state.view !== 'list' || !box.querySelector('.ovr-rows')) { state.view = 'list'; state.run = null; box.textContent = ''; box.className = 'ovr is-list'; const head = el('div', 'ovr-head'); const tabs = el('div', 'ovr-filters'); tabs.setAttribute('role', 'tablist'); tabs.setAttribute('aria-label', 'Show'); for (const [id, label, ic] of FILTERS) { const b = el('button', 'ovr-filter'); b.type = 'button'; b.setAttribute('role', 'tab'); b.dataset.filter = id; b.append(svg(ic), el('span', '', label)); b.addEventListener('click', () => { if (state.filter !== id || state.view !== 'list') list(id); }); tabs.append(b); } tabs.addEventListener('keydown', (e) => { if (e.key !== 'ArrowRight' && e.key !== 'ArrowLeft') return; const all = [...tabs.children]; const k = all.indexOf(document.activeElement); if (k < 0) return; const next = all[(k + (e.key === 'ArrowRight' ? 1 : all.length - 1)) % all.length]; e.preventDefault(); next.focus(); next.click(); }); head.append(tabs); const rows = el('ul', 'ovr-rows'); rows.setAttribute('aria-label', 'Your runs'); const foot = el('div', 'ovr-list-note'); const top = el('div', 'ovr-top'); const pane = el('div', 'ovr-pane'); pane.hidden = true; box.append(head, top, rows, foot, pane); dropTop(); dropPane(); if (Array.isArray(o.starters) && o.starters.length) { const st = el('div', 'ovr-starters'); st.setAttribute('role', 'group'); st.setAttribute('aria-label', 'Things to ask'); o.starters.slice(0, 8).forEach((x) => { const [ic, words] = Array.isArray(x) ? x : ['spark', String(x)]; const b = el('button', 'ovr-starter'); b.type = 'button'; b.append(svg(G[ic] ? ic : 'spark'), el('span', '', words)); b.addEventListener('click', async () => { b.disabled = true; try { await start(words); } catch (err) { b.disabled = false; foot.textContent = ''; foot.append(el('p', 'ovr-note is-err', err.message)); } }); st.append(b); }); box.append(st); } composer(box, { placeholder: o.placeholder || 'Ask, or say what to make', onSend: (v) => start(v.text) }); } place({ filter: state.filter }); box.querySelectorAll('.ovr-filter').forEach((b) => { const on = b.dataset.filter === state.filter; b.setAttribute('aria-selected', String(on)); b.tabIndex = on ? 0 : -1; }); // A filter the host draws itself (a pane): it takes the place of the rows, the starters and the box. The host's // pieces may share one owner (the Dock lends one panel at a time), so the old one goes before the new one comes. const pf = paneFor(state.filter); const topEl = box.querySelector('.ovr-top'), paneEl = box.querySelector('.ovr-pane'); const wantTop = !pf && ['all', 'task'].includes(state.filter) && typeof o.top === 'function'; if (!wantTop) { dropTop(); topEl.textContent = ''; } if (paneId !== (pf ? state.filter : null)) { dropPane(); paneEl.textContent = ''; } box.classList.toggle('has-pane', !!pf); paneEl.hidden = !pf; topEl.hidden = !wantTop; { const st = box.querySelector('.ovr-starters'); if (st) st.hidden = !!pf || !['all', 'chat'].includes(state.filter); } if (pf) { if (!paneHandle) { paneId = state.filter; try { paneHandle = pf(paneEl) || {}; } catch { paneHandle = {}; } } const rows0 = box.querySelector('.ovr-rows'); rows0.textContent = ''; rows0.removeAttribute('aria-busy'); box.querySelector('.ovr-list-note').textContent = ''; return; } if (wantTop && !topHandle) { try { topHandle = o.top(topEl) || null; } catch { topHandle = null; } } const rows = box.querySelector('.ovr-rows'); const foot = box.querySelector('.ovr-list-note'); rows.textContent = ''; foot.textContent = ''; rows.setAttribute('aria-busy', 'true'); for (let i = 0; i < 3; i++) rows.append(el('li', 'ovr-row is-ghost')); let out; try { out = await api(`?limit=40${state.filter !== 'all' ? `&kind=${state.filter}` : ''}`); } catch (err) { if (gen !== state.gen) return; rows.textContent = ''; rows.removeAttribute('aria-busy'); foot.append(empty(err.status === 401 ? 'Sign in to see your runs' : 'Your runs did not load', err.status === 401 ? 'Your chats, tasks and code are here once you sign in.' : 'Check your connection; they come back on their own.')); return; } if (gen !== state.gen) return; state.runs = Array.isArray(out.runs) ? out.runs : []; const extra = await actionRows(state.runs); if (gen !== state.gen) return; if (extra.length) { // What needs you first, then newest: the Action Center's rows join in that order. const needs = (r) => (needsPerson(r) ? 1 : 0); state.runs = state.runs.concat(extra).sort((x, y) => needs(y) - needs(x) || String(y.updated_at || '').localeCompare(String(x.updated_at || ''))); } rows.textContent = ''; rows.removeAttribute('aria-busy'); if (!state.runs.length) foot.append(empty('Nothing here yet', state.filter === 'code' ? 'Say what to build below: a page, a fix, controls for your stream.' : 'Ask anything below. Chats, tasks and code you start show up here.')); state.runs.forEach((r, i) => { const li = el('li'); li.append(row(r, i)); const acts = answerRow(r); if (acts) { li.classList.add('has-acts'); li.append(acts); } rows.append(li); }); if (Array.isArray(out.partial) && out.partial.length) foot.append(el('p', 'ovr-note', `Some of your runs did not load (${out.partial.join(', ')}).`)); } function row(r, i) { const away = r.action ? httpsUrl(r.url) : ''; const b = away ? el('a', `ovr-row${needsPerson(r) ? ' is-needs' : ''}`) : el('button', `ovr-row${needsPerson(r) ? ' is-needs' : ''}`); if (away) { b.href = away; b.target = '_blank'; b.rel = 'noopener'; } else b.type = 'button'; b.style.setProperty('--i', String(Math.min(i, 12))); const t = el('span', 'ovr-row-t'); const top = el('span', 'ovr-row-top'); top.append(el('b', '', r.title || (r.agent && r.agent.name) || KIND_WORD[r.kind] || 'Run'), el('time', '', ago(r.updated_at))); const line = el('span', 'ovr-row-line', needsPerson(r) || r.status === 'running' ? statusLine(r) : (r.preview && r.preview.text) || statusLine(r)); const meta = el('span', 'ovr-row-meta'); meta.append(svg(KIND_GLYPH[r.kind] || 'spark'), el('span', '', [KIND_WORD[r.kind] || 'Task', r.agent && r.agent.name && r.agent.name !== r.title ? r.agent.name : ''].filter(Boolean).join(' · '))); if (needsPerson(r)) meta.append(el('i', 'ovr-pill is-needs', 'Needs you')); else if (r.status === 'running') meta.append(el('i', 'ovr-pill is-working', 'Working')); else if (r.status === 'failed') meta.append(el('i', 'ovr-pill is-failed', 'Did not finish')); t.append(top, line, meta); b.append(face(r), t); if (!away) b.addEventListener('click', () => open(r.ref || r.id, r)); return b; } /** A run that asks before it acts is answered from the list (the Action Center's approve in place). */ function answerRow(r) { const w = r && r.waiting; const it = r && r.action; if (it && !(needsPerson(r) && Array.isArray(it.actions) && it.actions.includes('approve'))) return null; if (!it && (!needsPerson(r) || w.for !== 'approval' || !w.approval || !/^[A-Za-z0-9._~-]{1,160}$/.test(String(w.approval)))) return null; const box2 = el('div', 'ovr-row-acts'); const say = (text, err) => { box2.textContent = ''; box2.append(el('p', `ovr-note${err ? ' is-err' : ''}`, text)); }; [['approve', 'Approve', true], ['decline', 'Not now', false]].forEach(([id, label, primary]) => { const b = el('button', primary ? 'ovr-btn is-primary' : 'ovr-btn', label); b.type = 'button'; b.setAttribute('aria-label', `${label}: ${r.title || 'this run'}`); b.addEventListener('click', async () => { [...box2.querySelectorAll('button')].forEach((x) => { x.disabled = true; }); try { if (it) { const res = await fetch(`${actionsBase}/api/v1/me/actions/${encodeURIComponent(it.service)}/${encodeURIComponent(it.id)}/${id}`, { method: 'POST', credentials: 'omit', headers: { Accept: 'application/json', 'Content-Type': 'application/json', ...headers() }, body: '{}' }); if (!res.ok) { const j = await res.json().catch(() => ({})); throw new Error(j.detail || j.error || j.title || `That did not go through (${res.status})`); } } else await api(`/${encodeURIComponent(r.ref || r.id)}/approvals/${encodeURIComponent(w.approval)}`, { method: 'POST', body: JSON.stringify({ answer: id }) }); say(id === 'approve' ? 'Approved' : 'Declined'); } catch (err) { [...box2.querySelectorAll('button')].forEach((x) => { x.disabled = false; }); box2.append(el('p', 'ovr-note is-err', err.message)); } }); box2.append(b); }); return box2; } function empty(title, sub) { const e = el('div', 'ovr-empty'); e.append(svg('spark')); const w = el('div'); w.append(el('b', '', title), el('span', '', sub)); e.append(w); return e; } // ── the thread ── async function open(ref, known) { clearStream(); dropTop(); dropPane(); const gen = ++state.gen; state.view = 'thread'; state.run = known || null; state.steps = new Map(); place({ ref }); box.textContent = ''; box.className = 'ovr is-thread'; const head = el('div', 'ovr-thead'); const back = el('button', 'ovr-ib'); back.type = 'button'; back.setAttribute('aria-label', 'Back to your runs'); back.title = 'Your runs'; back.append(svg('back')); back.addEventListener('click', () => list()); const who = el('div', 'ovr-who'); const title = el('b'); const sub = el('small'); who.append(title, sub); const away = el('a', 'ovr-ib'); away.setAttribute('aria-label', 'Open on its own page'); away.title = 'Open on its own page'; away.target = '_blank'; away.rel = 'noopener'; away.append(svg('open')); const faceSlot = el('span', 'ovr-face-slot'); head.append(back, faceSlot, who, away); const thread = el('div', 'ovr-thread'); thread.setAttribute('role', 'log'); thread.setAttribute('aria-live', 'polite'); box.append(head, thread); const paintHead = (r) => { state.run = r; faceSlot.textContent = ''; faceSlot.append(face(r, 's')); title.textContent = r.title || (r.agent && r.agent.name) || 'Run'; sub.textContent = [KIND_WORD[r.kind] || 'Task', statusLine(r)].filter(Boolean).join(' · '); const url = httpsUrl(r.url); away.hidden = !url; if (url) away.href = url; box.classList.toggle('is-needs', needsPerson(r)); }; if (known) paintHead(known); composer(box, { placeholder: 'Say the next thing', onSend: async (v) => { const step = await api(`/${encodeURIComponent(ref)}/messages`, { method: 'POST', body: JSON.stringify({ text: v.text }) }); add(step); } }); const add = (s) => { if (!s || !Number.isInteger(s.seq)) return; if (state.steps.has(s.seq)) { const old = state.steps.get(s.seq); const n = stepNode(s); old.replaceWith(n); state.steps.set(s.seq, n); return; } const wasNear = thread.scrollHeight - thread.scrollTop - thread.clientHeight < 160; const none = thread.querySelector(':scope > .ovr-none'); if (none) none.remove(); const n = stepNode(s); state.steps.set(s.seq, n); const after = [...state.steps.keys()].filter((k) => k > s.seq).sort((a, b) => a - b)[0]; if (after !== undefined && state.steps.get(after).parentNode === thread) thread.insertBefore(n, state.steps.get(after)); else thread.append(n); if (wasNear) requestAnimationFrame(() => { thread.scrollTop = thread.scrollHeight; }); }; try { const [r, page] = await Promise.all([api(`/${encodeURIComponent(ref)}`), api(`/${encodeURIComponent(ref)}/steps?limit=200`)]); if (gen !== state.gen) return; paintHead(r); (page.steps || []).forEach(add); requestAnimationFrame(() => { thread.scrollTop = thread.scrollHeight; }); if (!(page.steps || []).length) thread.append(el('p', 'ovr-note ovr-none', 'Nothing here yet.')); } catch (err) { if (gen !== state.gen) return; thread.append(empty('This run did not load', err.message)); return; } if (isEnd(state.run)) return; const last = Math.max(0, ...state.steps.keys()); state.abort = new AbortController(); state.stopStream = follow({ url: `${base}/runs/${encodeURIComponent(ref)}/events`, headers: headers(), after: last, signal: state.abort.signal, onEvent(event, d) { if (gen !== state.gen) return 'end'; if (event === 'step' && d && d.step) add(d.step); else if (event === 'delta' && d && state.steps.has(d.seq)) { const w = state.steps.get(d.seq).querySelector('.ovr-words'); if (w) w.append(document.createTextNode(String(d.text || ''))); } else if (event === 'run' && d && d.run) paintHead(d.run); else if (event === 'end') { if (state.run) paintHead({ ...state.run, status: (d && d.status) || 'done' }); return 'end'; } return null; } }); } function stepNode(s) { const from = s.from || (s.kind === 'message' ? 'agent' : 'agent'); if (s.kind === 'message' && from === 'person') { const n = el('div', 'ovr-step is-you'); const b = el('div', 'ovr-bubble'); b.append(words(s.text)); n.append(b); attachments(n, s); return n; } if (s.kind === 'message' || (s.kind !== 'tool' && s.kind !== 'think' && s.kind !== 'approval' && s.kind !== 'child' && s.kind !== 'file' && s.text)) { const n = el('div', `ovr-step is-agent${s.status === 'running' ? ' is-writing' : ''}`); if (s.author && s.author.name) n.append(el('small', 'ovr-author', s.author.name)); n.append(words(s.text, s.sources)); cardFor(n, s); if (Array.isArray(s.sources) && s.sources.length) { const src = el('div', 'ovr-sources'); s.sources.slice(0, 12).forEach((x) => { const url = httpsUrl(x.url); if (!url) return; const a = el('a', 'ovr-source'); a.href = url; a.target = '_blank'; a.rel = 'nofollow noopener noreferrer'; a.append(el('i', '', String(x.n)), el('span', '', x.title || x.site || new URL(url).hostname)); src.append(a); }); n.append(src); } attachments(n, s); return n; } if (s.kind === 'think') { const d = el('details', 'ovr-step is-think'); const sm = el('summary'); sm.append(svg('think'), el('span', '', s.title || 'Thinking')); d.append(sm); if (s.text) d.append(words(s.text)); return d; } if (s.kind === 'approval') { const n = el('div', 'ovr-step is-approval'); const head = el('div', 'ovr-approval-h'); head.append(svg('check'), el('b', '', s.title || 'It asks before it goes on')); n.append(head); if (s.text) n.append(words(s.text)); const r = s.result && typeof s.result === 'object' ? s.result : {}; const opts = Array.isArray(r.options) ? r.options : [{ id: 'approve', label: 'Approve' }, { id: 'decline', label: 'Not now' }]; const row = el('div', 'ovr-approval-acts'); opts.slice(0, 4).forEach((op, i) => { const b = el('button', i === 0 ? 'ovr-btn is-primary' : 'ovr-btn', op.label || op.id); b.type = 'button'; b.addEventListener('click', async () => { [...row.children].forEach((x) => { x.disabled = true; }); try { await api(`/${encodeURIComponent(state.run.ref || state.run.id)}/approvals/${encodeURIComponent(r.approval || r.id || String(s.seq))}`, { method: 'POST', body: JSON.stringify({ answer: op.id }) }); row.replaceWith(el('p', 'ovr-note', `You answered: ${op.label || op.id}`)); } catch (err) { [...row.children].forEach((x) => { x.disabled = false; }); n.append(el('p', 'ovr-note is-err', err.message)); } }); row.append(b); }); if (!r.answer) n.append(row); else n.append(el('p', 'ovr-note', `Answered: ${r.answer}`)); return n; } // tool, child, file: one quiet row const n = el(s.text && s.kind === 'tool' ? 'details' : 'div', `ovr-step is-row is-${s.kind}${s.status === 'running' ? ' is-running' : s.status === 'failed' ? ' is-failed' : ''}`); const line = el(s.text && s.kind === 'tool' ? 'summary' : 'div', 'ovr-row-step'); const glyph = G[s.glyph] ? s.glyph : s.kind === 'child' ? 'child' : s.kind === 'file' ? 'file' : 'tool'; line.append(svg(glyph), el('span', '', s.title || (s.tool_call && s.tool_call.name) || (s.kind === 'file' ? s.text : s.kind === 'child' ? 'A helper took part of the work' : 'A step'))); if (s.status === 'running') line.append(el('i', 'ovr-pulse')); if (s.status === 'failed') line.append(svg('x', 'ovr-fail')); n.append(line); if (s.text && s.kind === 'tool') n.append(words(s.text)); if (s.kind === 'child' && s.child_run) { n.classList.add('is-link'); n.addEventListener('click', () => open(s.child_run)); } return n; } function cardFor(n, s) { if (!s.ui || !s.ui.type) return; const render = CARDS.get(s.ui.type); if (!render) return; try { const c = render(s.ui.data || {}, s, { open }); if (c) { c.classList && c.classList.add('ovr-card'); n.append(c); } } catch { /* the words above say it */ } } function attachments(n, s) { const list = Array.isArray(s.attachments) ? s.attachments : []; if (!list.length) return; const row = el('div', 'ovr-files'); list.slice(0, 8).forEach((a) => { const url = httpsUrl(a.url); if (a.kind === 'image' && url) { const i = el('img', 'ovr-thumb'); i.src = url; i.alt = a.alt || ''; i.loading = 'lazy'; row.append(i); } else { const c = el('span', 'ovr-chip'); c.append(svg('file'), el('span', '', a.name || a.kind)); row.append(c); } }); n.append(row); } /** Words in the list's box, unsent, with the focus there (a page's "Ask your agent"). */ function prefill(text) { if (state.view !== 'list' || paneFor(state.filter)) list(paneFor(state.filter) ? 'all' : undefined); const put = (n = 0) => { const ta = box.querySelector('.ovr-compose textarea'); if (!ta) { if (n < 40) setTimeout(() => put(n + 1), 50); return; } ta.value = String(text || '').slice(0, 16000); ta.dispatchEvent(new Event('input', { bubbles: true })); try { ta.focus({ preventScroll: true }); ta.setSelectionRange(ta.value.length, ta.value.length); } catch { /* */ } }; put(); } const view = { list, open, start, prefill, destroy() { clearStream(); dropTop(); dropPane(); state.gen++; host.textContent = ''; }, get state() { return { view: state.view, run: state.run, filter: state.filter }; } }; if (o.open) open(o.open); else list(); return view; } let styled = false; function styles() { if (styled || document.getElementById('ovr-styles')) { styled = true; return; } styled = true; const st = document.createElement('style'); st.id = 'ovr-styles'; st.textContent = ` .ovr { --ovr-gap: 12px; display: flex; flex-direction: column; min-height: 0; height: 100%; font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: var(--text-primary, #e6edf7); } .ovr svg { width: 18px; height: 18px; flex: none; fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; } .ovr-head { padding: 4px 4px 8px; } .ovr-filters { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 4px; padding: 4px; border-radius: 16px; background: color-mix(in srgb, var(--text-primary, #e6edf7) 5%, transparent); } .ovr-filter { all: unset; box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; gap: 7px; min-height: 42px; border-radius: 12px; cursor: pointer; font-family: 'OV Brand', Nunito, Inter, sans-serif; font-weight: 800; font-size: 14px; color: var(--text-secondary, #a8b3c4); transition: background-color .18s, color .18s; } .ovr-filter[aria-selected="true"] { color: var(--text-primary, #e6edf7); background: var(--bg-secondary, #121826); box-shadow: 0 1px 2px rgba(0,0,0,.3), inset 0 0 0 1px color-mix(in srgb, var(--accent, #3b82f6) 35%, transparent); } .ovr-filter[aria-selected="true"] svg { color: var(--accent-light, var(--accent, #60a5fa)); } .ovr-filter:focus-visible, .ovr-row:focus-visible, .ovr-ib:focus-visible, .ovr-btn:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 2px; } @media (max-width: 380px) { .ovr-filter { font-size: 13px; gap: 5px; } .ovr-filter svg { width: 16px; height: 16px; } } .ovr-rows { flex: 0 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; display: grid; align-content: start; gap: 6px; margin: 0; padding: 2px 4px 10px; list-style: none; } .ovr-rows > li { display: grid; min-width: 0; } .ovr-rows > li.has-acts { border-radius: 16px; background: color-mix(in srgb, #f59e0b 7%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, #f59e0b 50%, transparent); } .ovr-rows > li.has-acts > .ovr-row { background: transparent; box-shadow: none; } .ovr-row-acts { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 0 12px 12px 72px; } .ovr-row-acts .ovr-btn { min-height: 38px; font-size: 13.5px; } .ovr-row-acts .ovr-note { margin: 0; } @media (max-width: 380px) { .ovr-row-acts { padding-left: 12px; } } .ovr-rows:empty { display: none; } .ovr-list-note:empty { display: none; } .ovr-list-note { padding: 0 4px; } .ovr.is-list > .ovr-compose, .ovr.is-list > .ovr-starters:not([hidden]) { margin-top: auto; } .ovr.is-list > .ovr-starters:not([hidden]) + .ovr-compose { margin-top: 0; } /* On a wide screen the list, the thread and the box keep a reading column. */ .ovr-head, .ovr-top, .ovr-rows, .ovr-list-note, .ovr-starters, .ovr-compose, .ovr-thead { width: 100%; max-width: 780px; margin-inline: auto; box-sizing: border-box; } .ovr-thread { padding-inline: max(4px, calc((100% - 772px) / 2)); } .ovr-row { all: unset; box-sizing: border-box; display: flex; align-items: center; gap: 12px; min-height: 72px; padding: 10px 12px; border-radius: 16px; cursor: pointer; background: color-mix(in srgb, var(--text-primary, #e6edf7) 4%, transparent); box-shadow: inset 0 0 0 1px var(--border, rgba(255,255,255,.07)); transition: background-color .15s, transform .15s; animation: ovr-rise .3s cubic-bezier(.2,.8,.2,1) both; animation-delay: calc(var(--i, 0) * 18ms); } .ovr-row.is-needs { box-shadow: inset 0 0 0 1px color-mix(in srgb, #f59e0b 50%, transparent); background: color-mix(in srgb, #f59e0b 7%, transparent); } .ovr-row:active { transform: scale(.985); } @media (hover: hover) and (pointer: fine) { .ovr-row:hover { background: color-mix(in srgb, var(--text-primary, #e6edf7) 8%, transparent); } } .ovr-row.is-ghost { min-height: 72px; animation: ovr-shimmer 1.4s ease-in-out infinite; cursor: default; } .ovr-row-t { flex: 1; min-width: 0; display: grid; gap: 3px; } .ovr-row-top { display: flex; align-items: baseline; gap: 8px; min-width: 0; } .ovr-row-top b { flex: 1; min-width: 0; font-family: 'OV Brand', Nunito, Inter, sans-serif; font-size: 15px; font-weight: 800; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow-wrap: anywhere; } .ovr-row-top time { flex: none; font-size: 12px; color: var(--text-muted, #93a0b4); } .ovr-row-line { font-size: 13.5px; color: var(--text-secondary, #a8b3c4); overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow-wrap: anywhere; line-height: 1.35; } .ovr-row-meta { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-muted, #93a0b4); } .ovr-row-meta svg { width: 14px; height: 14px; } .ovr-pill { font-style: normal; font-size: 11.5px; font-weight: 750; padding: 1px 8px; border-radius: 999px; margin-left: auto; } .ovr-pill.is-needs { color: #fbbf24; background: color-mix(in srgb, #f59e0b 16%, transparent); } .ovr-pill.is-working { color: var(--accent-light, #60a5fa); background: color-mix(in srgb, var(--accent, #3b82f6) 16%, transparent); } .ovr-pill.is-failed { color: #f87171; background: color-mix(in srgb, #ef4444 14%, transparent); } .ovr-face { position: relative; flex: none; display: grid; place-items: center; border-radius: 30%; color: hsl(var(--ovr-h, 215) 92% 84%); background: linear-gradient(150deg, hsl(var(--ovr-h, 215) 72% 58% / .4), hsl(var(--ovr-h, 215) 70% 38% / .22)); box-shadow: inset 0 0 0 1px hsl(var(--ovr-h, 215) 70% 64% / .3); } :root[data-theme="light"] .ovr-face { color: hsl(var(--ovr-h, 215) 70% 30%); } @media (prefers-color-scheme: light) { :root:not([data-theme="dark"]) .ovr-face { color: hsl(var(--ovr-h, 215) 70% 30%); } } .ovr-face.is-m { width: 48px; height: 48px; } .ovr-face.is-m > svg { width: 26px; height: 26px; } /* 4.238.0 (the owner, 11-13: "waste of space … only a tiny bit of space between"): the list is tight, its rows one line of title and time, one of what came back, their face a size down. */ .ovr-rows { gap: 4px; padding: 0 2px 8px; } .ovr-row { min-height: 60px; gap: 10px; padding: 8px 10px; border-radius: 14px; } .ovr-row-t { gap: 1px; } .ovr-row-line { -webkit-line-clamp: 1; } .ovr-rows .ovr-face.is-m { width: 40px; height: 40px; border-radius: 12px; } .ovr-rows .ovr-face.is-m > svg { width: 22px; height: 22px; } .ovr-row-acts { padding-left: 60px; } .ovr-face.is-s { width: 38px; height: 38px; } .ovr-face.is-s > svg { width: 21px; height: 21px; } .ovr-face img { width: 100%; height: 100%; border-radius: inherit; object-fit: cover; } .ovr-face.is-needs { box-shadow: 0 0 0 2px #f59e0b; } .ovr-face.is-working { animation: ovr-breathe 2.4s ease-in-out infinite; } .ovr-doing { position: absolute; right: -4px; bottom: -4px; display: grid; place-items: center; width: 20px; height: 20px; border-radius: 50%; color: #fff; background: linear-gradient(140deg, var(--accent, #3b82f6), color-mix(in srgb, var(--accent, #3b82f6) 45%, #a855f7)); box-shadow: 0 0 0 2px var(--bg-secondary, #121826); } .ovr-doing svg { width: 12px; height: 12px; stroke-width: 2.3; } .ovr-empty { display: flex; align-items: center; gap: 14px; margin: 6px 0; padding: 16px; border-radius: 16px; border: 1px dashed color-mix(in srgb, var(--text-muted, #93a0b4) 45%, transparent); } .ovr-empty > svg { width: 24px; height: 24px; color: var(--accent-light, #60a5fa); } .ovr-empty > div { display: grid; gap: 2px; } .ovr-empty b { font-size: 14.5px; } .ovr-empty span { font-size: 13px; color: var(--text-muted, #93a0b4); } .ovr-note { margin: 6px 4px; font-size: 13px; color: var(--text-muted, #93a0b4); } .ovr-note.is-err { color: #f87171; } .ovr-compose { flex: none; padding: 8px 4px 4px; } .ovr-top { flex: none; } .ovr-pane { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; overflow: hidden; } .ovr-pane[hidden] { display: none; } .ovr.has-pane > .ovr-rows, .ovr.has-pane > .ovr-list-note, .ovr.has-pane > .ovr-compose { display: none; } .ovr-top[hidden], .ovr-starters[hidden] { display: none; } .ovr-top:empty { display: none; } .ovr-starters { flex: none; display: flex; gap: 6px; overflow-x: auto; padding: 8px 4px 0; scrollbar-width: none; -webkit-mask-image: linear-gradient(90deg, #000 92%, transparent); mask-image: linear-gradient(90deg, #000 92%, transparent); } .ovr-starters::-webkit-scrollbar { display: none; } .ovr-starter { all: unset; box-sizing: border-box; flex: none; display: inline-flex; align-items: center; gap: 7px; min-height: 38px; padding: 0 13px 0 10px; border-radius: 999px; cursor: pointer; font-size: 13.5px; white-space: nowrap; color: var(--text-secondary, #a8b3c4); background: color-mix(in srgb, var(--text-primary, #e6edf7) 6%, transparent); box-shadow: inset 0 0 0 1px var(--border, rgba(255,255,255,.07)); transition: background-color .15s, color .15s; } .ovr-starter svg { width: 16px; height: 16px; color: var(--accent-light, #60a5fa); } .ovr-starter:disabled { opacity: .5; cursor: default; } .ovr-starter:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 2px; } @media (hover: hover) and (pointer: fine) { .ovr-starter:hover { color: var(--text-primary, #e6edf7); background: color-mix(in srgb, var(--accent, #3b82f6) 12%, transparent); } } a.ovr-row { color: inherit; text-decoration: none; } .ovr-plain { display: flex; align-items: flex-end; gap: 8px; padding: 8px; border-radius: 18px; border: 1px solid var(--border, rgba(148,163,184,.2)); background: var(--bg-primary, rgba(0,0,0,.2)); } .ovr-plain textarea { flex: 1; min-height: 40px; max-height: 160px; resize: none; border: 0; outline: 0; background: transparent; color: inherit; font: inherit; font-size: 15px; } .ovr-send { all: unset; display: grid; place-items: center; width: 40px; height: 40px; border-radius: 50%; cursor: pointer; color: #fff; background: var(--accent-strong, var(--accent, #2563eb)); } .ovr-thead { flex: none; display: flex; align-items: center; gap: 10px; padding: 4px 4px 10px; border-bottom: 1px solid var(--border, rgba(255,255,255,.07)); } .ovr-ib { all: unset; box-sizing: border-box; flex: none; display: grid; place-items: center; width: 40px; height: 40px; border-radius: 50%; cursor: pointer; color: var(--text-secondary, #a8b3c4); background: color-mix(in srgb, var(--text-primary, #e6edf7) 7%, transparent); transition: background-color .15s, transform .12s; } .ovr-ib[hidden] { display: none; } .ovr-ib:active { transform: scale(.92); } @media (hover: hover) { .ovr-ib:hover { color: var(--text-primary, #e6edf7); background: color-mix(in srgb, var(--text-primary, #e6edf7) 13%, transparent); } } .ovr-who { flex: 1; min-width: 0; display: grid; gap: 1px; } .ovr-who b { font-family: 'OV Brand', Nunito, Inter, sans-serif; font-size: 15.5px; font-weight: 800; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .ovr-who small { font-size: 12.5px; color: var(--text-secondary, #a8b3c4); overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .ovr.is-needs .ovr-who small { color: #fbbf24; } .ovr-thread { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; display: flex; flex-direction: column; gap: 10px; padding: 12px 4px; } .ovr-step { max-width: min(640px, 92%); animation: ovr-rise .26s cubic-bezier(.2,.8,.2,1) both; } .ovr-step.is-you { align-self: flex-end; } .ovr-bubble { padding: 9px 13px; border-radius: 18px 18px 6px 18px; color: var(--on-accent-strong, #fff); background: var(--accent-strong, var(--accent, #2563eb)); } .ovr-bubble .ovr-a { color: inherit; text-decoration-color: color-mix(in srgb, currentColor 55%, transparent); text-underline-offset: 2px; } .ovr-step.is-agent { align-self: flex-start; padding: 10px 14px; border-radius: 18px 18px 18px 6px; background: color-mix(in srgb, var(--text-primary, #e6edf7) 6%, transparent); } .ovr-step.is-writing .ovr-words > :last-child::after { content: ''; display: inline-block; width: .5em; height: 1em; margin-left: 2px; vertical-align: -2px; border-radius: 2px; background: currentColor; opacity: .5; animation: ovr-caret 1s steps(2) infinite; } .ovr-author { display: block; margin-bottom: 3px; font-size: 12px; font-weight: 750; color: var(--text-muted, #93a0b4); } .ovr-words { font-size: 15px; line-height: 1.5; overflow-wrap: anywhere; } .ovr-words p { margin: 0; } .ovr-words p + p, .ovr-words p + ul, .ovr-words ul + p, .ovr-words ol + p, .ovr-words p + ol { margin-top: 8px; } .ovr-words ul, .ovr-words ol { margin: 0; padding-left: 20px; } .ovr-words li + li { margin-top: 3px; } .ovr-a { color: var(--accent-light, #60a5fa); text-decoration: underline; text-underline-offset: 2px; } .ovr-ic { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .9em; padding: 1px 5px; border-radius: 6px; background: color-mix(in srgb, var(--text-primary, #e6edf7) 9%, transparent); } .ovr-cite { display: inline-grid; place-items: center; min-width: 18px; height: 18px; margin: 0 2px; padding: 0 4px; border-radius: 6px; font-size: 11px; font-weight: 800; text-decoration: none; vertical-align: 2px; color: var(--accent-light, #60a5fa); background: color-mix(in srgb, var(--accent, #3b82f6) 16%, transparent); } .ovr-code { margin: 8px 0 0; padding: 10px 12px; border-radius: 12px; overflow-x: auto; font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; background: color-mix(in srgb, #000 30%, transparent); } .ovr-sources { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; } .ovr-source { display: inline-flex; align-items: center; gap: 6px; max-width: 100%; padding: 4px 10px 4px 4px; border-radius: 999px; font-size: 12.5px; color: var(--text-secondary, #a8b3c4); text-decoration: none; background: color-mix(in srgb, var(--text-primary, #e6edf7) 6%, transparent); } .ovr-source i { font-style: normal; display: grid; place-items: center; width: 20px; height: 20px; border-radius: 50%; font-size: 11px; font-weight: 800; color: var(--accent-light, #60a5fa); background: color-mix(in srgb, var(--accent, #3b82f6) 16%, transparent); } .ovr-source span { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; max-width: 220px; } .ovr-card { margin-top: 10px; } .ovr-chips { display: flex; flex-wrap: wrap; gap: 6px; } .ovr-chip { display: inline-flex; align-items: center; gap: 6px; padding: 5px 11px 5px 8px; border-radius: 999px; font-size: 13px; background: color-mix(in srgb, var(--accent, #3b82f6) 13%, transparent); } .ovr-chip svg { width: 15px; height: 15px; color: var(--accent-light, #60a5fa); } .ovr-chip.is-undone { opacity: .5; text-decoration: line-through; } .ovr-table-wrap { overflow-x: auto; } .ovr-table { border-collapse: collapse; font-size: 13.5px; min-width: 100%; } .ovr-table th, .ovr-table td { padding: 6px 10px; text-align: left; border-bottom: 1px solid var(--border, rgba(255,255,255,.08)); } .ovr-table th { font-weight: 750; color: var(--text-secondary, #a8b3c4); } .ovr-linkcard { display: flex; gap: 10px; align-items: center; padding: 8px; border-radius: 14px; color: inherit; text-decoration: none; background: color-mix(in srgb, var(--text-primary, #e6edf7) 5%, transparent); } .ovr-linkcard img { width: 64px; height: 48px; object-fit: cover; border-radius: 10px; } .ovr-linkcard-t { display: grid; min-width: 0; } .ovr-linkcard-t b { font-size: 14px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .ovr-linkcard-t small { font-size: 12px; color: var(--text-muted, #93a0b4); } .ovr-step.is-row, .ovr-step.is-think { align-self: flex-start; max-width: 100%; } .ovr-row-step, .ovr-step.is-think > summary { display: flex; align-items: center; gap: 8px; min-height: 30px; padding: 2px 8px; border-radius: 10px; font-size: 13.5px; color: var(--text-secondary, #a8b3c4); cursor: default; list-style: none; } .ovr-step.is-think > summary, details.ovr-step > .ovr-row-step { cursor: pointer; } .ovr-step.is-think > summary::-webkit-details-marker, details.ovr-step > summary::-webkit-details-marker { display: none; } .ovr-row-step svg, .ovr-step.is-think > summary svg { width: 16px; height: 16px; color: var(--text-muted, #93a0b4); } .ovr-step.is-row.is-running .ovr-row-step { color: var(--text-primary, #e6edf7); } .ovr-step.is-row.is-failed .ovr-row-step { color: #f87171; } .ovr-step.is-row.is-link { cursor: pointer; } .ovr-step.is-think .ovr-words, details.ovr-step.is-row .ovr-words { margin: 4px 0 2px 32px; font-size: 13.5px; color: var(--text-secondary, #a8b3c4); } .ovr-pulse { width: 8px; height: 8px; border-radius: 50%; background: var(--accent-light, #60a5fa); animation: ovr-pulse 1.2s ease-in-out infinite; } .ovr-fail { width: 15px; height: 15px; color: #f87171; } .ovr-step.is-approval { align-self: stretch; max-width: 100%; padding: 12px 14px; border-radius: 16px; background: color-mix(in srgb, #f59e0b 8%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, #f59e0b 40%, transparent); } .ovr-approval-h { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; } .ovr-approval-h svg { color: #fbbf24; } .ovr-approval-acts { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; } .ovr-btn { all: unset; box-sizing: border-box; display: inline-flex; align-items: center; min-height: 40px; padding: 0 16px; border-radius: 999px; cursor: pointer; font-weight: 750; font-size: 14px; background: color-mix(in srgb, var(--text-primary, #e6edf7) 9%, transparent); } .ovr-btn.is-primary { color: var(--on-accent-strong, #fff); background: var(--accent-strong, var(--accent, #2563eb)); } .ovr-btn:disabled { opacity: .5; cursor: default; } .ovr-files { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; } .ovr-step.is-you .ovr-files { justify-content: flex-end; } .ovr-thumb { width: 120px; height: 90px; object-fit: cover; border-radius: 12px; } @keyframes ovr-rise { from { opacity: 0; transform: translateY(6px); } } @keyframes ovr-shimmer { 50% { opacity: .55; } } @keyframes ovr-breathe { 50% { box-shadow: 0 0 0 5px color-mix(in srgb, var(--accent, #3b82f6) 22%, transparent); } } @keyframes ovr-pulse { 50% { opacity: .35; transform: scale(.7); } } @keyframes ovr-caret { 50% { opacity: 0; } } html[data-ov-motion=reduced] .ovr-row, html[data-ov-motion=reduced] .ovr-step { animation-name: ovr-fade; animation-delay: 0s; } @keyframes ovr-fade { from { opacity: 0; } } html[data-ov-motion=reduced] .ovr-face.is-working { animation-duration: 5s; } html[data-ov-motion=off] .ovr-row, html[data-ov-motion=off] .ovr-step, html[data-ov-motion=off] .ovr-face.is-working, html[data-ov-motion=off] .ovr-pulse, html[data-ov-motion=off] .ovr-row.is-ghost { animation: none; } `; document.head.appendChild(st); } root.OpenVibeRun = { mount, card, version: 1 }; if (typeof module !== 'undefined' && module.exports) module.exports = root.OpenVibeRun; })(typeof window !== 'undefined' ? window : globalThis);