openvibers/openvibe.shared ov-run.js

ov-run.js

main 62.2 KB · 734 lines · javascript

1/**
2 * 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
3 * 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
4 * way, on every site, from the Frame's Run API (Network /api/frame/runs, Contracts `agents.run@1`, `agents.step@1`,
5 * `agents.event@1`). The owner, 11-12, crossing out the menu's Chat · Code · Agents switch: "one new unified agent …
6 * a shared UI … a full conversation with rich embedding … agent mode".
7 *
8 * const view = OpenVibeRun.mount(host, {
9 * base: 'https://openvibe.network/api/frame', // the Frame (default)
10 * token: () => token, // the person's Network token (or a string)
11 * me: { name, avatar_url }, // the person, for their own bubbles
12 * sibling: (file) => url, // where ov-composer.js is (the navbar's sibling())
13 * examples: ['A goal bar for my stream', …], // what people make, shown in turn in the empty box
14 * start: { kind, target, context }, // what a new run starts with (the box's defaults)
15 * });
16 * view.list('code'); view.open('chat:c_8f3k2m'); view.start('A goal bar for my stream'); view.prefill('Words, unsent');
17 * view.destroy();
18 * (o.filter: the list's first filter, all | chat | task | code; o.open: a run's ref to open first)
19 *
20 * 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):
21 * o.starters: [[glyph, words], …] things to ask, as chips above the box (All and Chats); a press starts that run
22 * o.actions: { base } the Action Center's waiting and running work from every product (Network
23 * GET /api/v1/me/actions), merged into the list (All and Tasks) when a run does not
24 * already show it; approved or declined in place, and opened on its own page
25 * o.top(el) → { destroy } something of the host's above the list (All and Tasks): the menu puts the Dock's
26 * Now, your Actor and the live agent view there
27 * o.panes: { chat, task, code } a filter the host still draws itself: pane(el) → { destroy }. The menu's Vibe
28 * lends the Dock's Vibe, Agents and Code panels here until this view does all
29 * they do; the pane replaces the rows and the box under the filters
30 * o.onPlace({ filter } | { ref }) where the person is, for the host to remember
31 * view.prefill(text) the words in the list's box, unsent (Ask your agent)
32 *
33 * OpenVibeRun.card(type, (data, step, ctx) => Node) a rich card for a step's `ui.type` (a site or a mod adds its
34 * own); an unknown type shows the step's words.
35 *
36 * The list: needs-you first, then newest, filtered All · Chats · Tasks · Code; each run is its agent's face (glyph and
37 * hue, or its picture, with what it is doing), its title, its last line and when. The thread: your words on the right,
38 * the agent's on the left (paragraphs, lists, links, code, numbered sources), thinking folded, each tool one quiet row,
39 * an approval as a card with its buttons, child runs as rows, files as chips. It follows the run live (the stream,
40 * resumed after its last step) and the composer below says the next thing, or starts a new run from the list.
41 */
42(function (root) {
43 'use strict';
44 if (root.OpenVibeRun || typeof document === 'undefined') return;
45
46 // ── glyphs (the registry's drawings, openvibe-shared/glyphs.js; 24-unit strokes) ──
47 const G = {
48 back: '<path d="M20.8 12H4M10.3 5.7l-6.3 6.3l6.3 6.3"/>',
49 open: '<path d="M7 17L17 7M9 7h8v8"/>',
50 send: '<path d="M12 19.5V5M5.8 11.2L12 5l6.2 6.2"/>',
51 plus: '<path d="M12 5v14M5 12h14"/>',
52 spark: '<path d="M10.4 3.6l1.7 4.8L17 10.1l-4.8 1.7L10.4 16.7l-1.7-4.8L3.9 10.1l4.8-1.7ZM17 14.8l0.8 2.1L19.8 17.6l-2.1 0.8L17 20.3l-0.8-2.1L14.3 17.6l2.1-0.8Z"/>',
53 chat: '<path d="M3.8 4.3h16.8v11.7H8l-4.2 4.2Z"/>',
54 code: '<path d="M7.8 7.8l-4.2 4.2l4.2 4.2M16.2 7.8l4.2 4.2l-4.2 4.2M14.2 4.6l-4.2 14.8"/>',
55 robot: '<rect x="5.4" y="8.3" width="13.2" height="10.4" rx="2.8"/><path d="M12 4.6v3.8M9.2 13.1h0M14.8 13.1h0M9.6 16.4h4.7"/>',
56 tool: '<path d="M14.6 3.6a5.2 5.2 0 0 0-4.6 7.3L3.7 17.3V20.3h3.1l6.4-6.4a5.2 5.2 0 0 0 7.3-4.6l-3.2 1.2l-2.5-2.5l1.2-3.2Z"/>',
57 think: '<path d="M9 18h6M10 21h4M12 3a6 6 0 0 0-3.5 10.9V16h7v-2.1A6 6 0 0 0 12 3Z"/>',
58 file: '<path d="M7 3h7l5 5v12a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1zM14 3v5h5"/>',
59 check: '<path d="M5 12.5l4.5 4.5L19 7.5"/>',
60 x: '<path d="M7 7l10 10M17 7L7 17"/>',
61 pr: '<circle cx="6" cy="6" r="2.2"/><circle cx="6" cy="18" r="2.2"/><circle cx="18" cy="18" r="2.2"/><path d="M6 8.2v7.6M18 15.8V9a3 3 0 0 0-3-3h-4M13 4l-2 2 2 2"/>',
62 link: '<path d="M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1"/>',
63 child: '<path d="M6 4v8a4 4 0 0 0 4 4h8M14 12l4 4-4 4"/>',
64 clock: '<circle cx="12" cy="12" r="8.5"/><path d="M12 7.5V12l3 2"/>',
65 search: '<circle cx="11" cy="11" r="6.5"/><path d="M16 16l4.5 4.5"/>',
66 book: '<path d="M12 6.3C10 4.8 7 4.3 3.5 4.9v13.2c3.5-0.6 6.5-0.1 8.5 1.4c2-1.5 5-2 8.5-1.4V4.9C17 4.3 14 4.8 12 6.3Z"/><path d="M12 6.3v13.2"/>',
67 pencil: '<path d="M4 20h4L19 9l-4-4L4 16v4zM13.5 6.5l4 4"/>',
68 live: '<circle cx="12" cy="11.8" r="2"/><path d="M8.6 8.4a4.9 4.9 0 0 0 0 7.1M15.4 8.4a4.9 4.9 0 0 1 0 7.1M6.3 6.1a8.1 8.1 0 0 0 0 11.9M17.7 6.1a8.1 8.1 0 0 1 0 11.9"/>',
69 store: '<path d="M3.7 8.8L5.3 3.7h13.4L20.3 8.8M3.7 8.8h16.5v1.6a2.8 2.8 0 0 1-5.4 0a2.8 2.8 0 0 1-5.6 0A2.8 2.8 0 0 1 3.7 10.4ZM4.8 12.9v7.2h14.5v-7.2"/>',
70 game: '<rect x="3.7" y="7.2" width="16.7" height="9.7" rx="4.4"/><path d="M7.6 10.7v2.7M6.4 12h2.7M15.1 11.6h0M17.4 12.9h0"/>',
71 compass: '<circle cx="12" cy="12" r="8.5"/><path d="m15.6 8.4-2.1 5.1-5.1 2.1 2.1-5.1Z"/>',
72 page: '<path d="M6.5 3h7.5l4 4v14h-11.5Z"/><path d="M14 3v4h4M9.5 12h5M9.5 15.5h5"/>',
73 timer: '<path d="M12 4.5a8.2 8.2 0 1 1 0 16.4 8.2 8.2 0 0 1 0-16.4zM12 8.6v4.2l2.7 1.7M9.6 2.6h4.8"/>',
74 bell: '<path d="M6 16.5V11a6 6 0 0 1 12 0v5.5l1.5 1.5h-15ZM10 20.5a2.2 2.2 0 0 0 4 0"/>',
75 calendar: '<rect x="4" y="5.5" width="16" height="14.5" rx="2.5"/><path d="M4 10h16M8.5 3.5v4M15.5 3.5v4"/>',
76 phone: '<path d="M6.5 3.8h3l1.5 4-2 1.3a10.5 10.5 0 0 0 5.9 5.9l1.3-2 4 1.5v3a1.8 1.8 0 0 1-2 1.8A15.5 15.5 0 0 1 4.7 5.8a1.8 1.8 0 0 1 1.8-2Z"/>',
77 };
78 const KIND_GLYPH = { chat: 'chat', ask: 'search', task: 'robot', code: 'code', machine: 'tool', goal: 'spark' };
79 const KIND_WORD = { chat: 'Chat', ask: 'Answer', task: 'Task', code: 'Code', machine: 'Machine', goal: 'Goal' };
80 const SERVICE_NAME = { actor: 'Actor', codes: 'Codes', ai: 'AI', network: 'OpenVibe', run: 'Run', domains: 'Domains' };
81 const svg = (name, cls = '') => {
82 const s = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
83 s.setAttribute('viewBox', '0 0 24 24'); s.setAttribute('aria-hidden', 'true'); if (cls) s.setAttribute('class', cls);
84 s.innerHTML = G[name] || G.spark; // the registry's constants above, never page text
85 return s;
86 };
87 const el = (tag, cls, text) => { const n = document.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = String(text); return n; };
88 const httpsUrl = (v) => { try { const u = new URL(String(v)); return u.protocol === 'https:' ? u.href : ''; } catch { return ''; } };
89 const hueOf = (id) => { let x = 7; for (const c of String(id || 'ov')) x = (x * 31 + c.codePointAt(0)) % 360; return x; };
90 const ago = (iso) => {
91 const t = Date.parse(iso || ''); if (!t) return '';
92 const s = Math.max(1, Math.round((Date.now() - t) / 1000));
93 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`;
94 const d = Math.round(s / 86400); return d === 1 ? 'yesterday' : `${d} days ago`;
95 };
96 const needsPerson = (r) => !!r && r.status === 'waiting' && !!r.waiting && r.waiting.for !== 'message';
97 const isEnd = (r) => !!r && ['done', 'failed', 'stopped'].includes(r.status);
98
99 // ── rich cards ──
100 const CARDS = new Map();
101 function card(type, render) { if (typeof type === 'string' && /^[a-z][a-z0-9-]{0,40}$/.test(type) && typeof render === 'function') CARDS.set(type, render); }
102 card('results', (data) => {
103 const row = el('div', 'ovr-chips');
104 for (const r of (Array.isArray(data.results) ? data.results : []).slice(0, 12)) {
105 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'));
106 if (r.undone) c.classList.add('is-undone');
107 row.append(c);
108 }
109 return row;
110 });
111 card('table', (data) => {
112 const wrap = el('div', 'ovr-table-wrap'); const t = el('table', 'ovr-table');
113 const cols = Array.isArray(data.columns) ? data.columns.slice(0, 8) : [];
114 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); }
115 const tb = el('tbody');
116 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); }
117 t.append(tb); wrap.append(t); return wrap;
118 });
119 card('link', (data) => {
120 const url = httpsUrl(data.url); if (!url) return null;
121 const a = el('a', 'ovr-linkcard'); a.href = url; a.target = '_blank'; a.rel = 'noopener noreferrer';
122 const img = httpsUrl(data.image); if (img) { const i = el('img'); i.src = img; i.alt = ''; i.loading = 'lazy'; a.append(i); }
123 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);
124 return a;
125 });
126 card('code', (data) => { const pre = el('pre', 'ovr-code'); pre.append(el('code', '', String(data.code || data.text || '').slice(0, 20000))); return pre; });
127
128 // ── words: paragraphs, lists, links, inline code, numbered sources ──
129 function words(text, sources) {
130 const box = el('div', 'ovr-words');
131 const raw = String(text || '').replace(/\r\n/g, '\n').slice(0, 65536);
132 const blocks = raw.split(/\n{2,}/);
133 for (const b of blocks) {
134 const lines = b.split('\n');
135 if (lines.every((l) => /^\s*[-*•]\s+/.test(l) || !l.trim())) {
136 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);
137 } else if (lines.every((l) => /^\s*\d+[.)]\s+/.test(l) || !l.trim())) {
138 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);
139 } else if (/^```/.test(b.trim())) {
140 const pre = el('pre', 'ovr-code'); pre.append(el('code', '', b.trim().replace(/^```[a-z0-9]*\n?/i, '').replace(/```$/, ''))); box.append(pre);
141 } else { const p = el('p'); lines.forEach((l, i) => { if (i) p.append(el('br')); inline(p, l, sources); }); box.append(p); }
142 }
143 return box;
144 }
145 function inline(node, text, sources) {
146 const re = /(https?:\/\/[^\s<>"')\]]+)|`([^`]{1,200})`|\*\*([^*]{1,200})\*\*|\[(\d{1,2})\]/g;
147 let last = 0, m;
148 while ((m = re.exec(text))) {
149 if (m.index > last) node.append(document.createTextNode(text.slice(last, m.index)));
150 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); }
151 else if (m[2]) node.append(el('code', 'ovr-ic', m[2]));
152 else if (m[3]) node.append(el('strong', '', m[3]));
153 else if (m[4]) {
154 const src = (sources || []).find((s) => Number(s.n) === Number(m[4]));
155 const url = src && httpsUrl(src.url);
156 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); }
157 else node.append(document.createTextNode(m[0]));
158 }
159 last = m.index + m[0].length;
160 }
161 if (last < text.length) node.append(document.createTextNode(text.slice(last)));
162 }
163
164 // ── an agent's face: its picture, or its glyph on its hue; what it is doing as a small badge ──
165 function face(run, size = 'm') {
166 const a = (run && run.agent) || {};
167 const f = el('span', `ovr-face is-${size}`);
168 f.style.setProperty('--ovr-h', String(Number.isInteger(a.hue) ? a.hue : hueOf(a.id || (run && run.kind))));
169 const pic = httpsUrl(a.avatar_url);
170 if (pic) { const i = el('img'); i.src = pic; i.alt = ''; i.loading = 'lazy'; f.append(i); }
171 else f.append(svg(G[a.glyph] ? a.glyph : KIND_GLYPH[run && run.kind] || 'spark'));
172 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'); }
173 if (needsPerson(run)) f.classList.add('is-needs');
174 return f;
175 }
176 function statusLine(run) {
177 if (!run) return '';
178 if (needsPerson(run)) return (run.waiting && run.waiting.what) || 'Needs you';
179 if (run.status === 'running') return (run.doing && run.doing.text) || 'Working';
180 if (run.status === 'planning') return 'Starting';
181 if (run.status === 'waiting') return 'Your turn';
182 return { done: 'Done', failed: 'Did not finish', stopped: 'Stopped' }[run.status] || '';
183 }
184
185 // ── the stream: a fetch reader over server-sent events (EventSource cannot send a bearer) ──
186 function follow({ url, headers, after, onEvent, signal }) {
187 let last = String(after || 0);
188 let stopped = false;
189 const once = async () => {
190 const r = await fetch(url, { headers: { ...headers, Accept: 'text/event-stream', 'Last-Event-ID': last }, signal, credentials: 'omit' });
191 if (!r.ok || !r.body) throw new Error(`stream ${r.status}`);
192 const reader = r.body.getReader(); const dec = new TextDecoder(); let buf = '';
193 for (;;) {
194 const { value, done } = await reader.read(); if (done) return 'closed';
195 buf += dec.decode(value, { stream: true });
196 let i;
197 while ((i = buf.indexOf('\n\n')) >= 0) {
198 const raw = buf.slice(0, i); buf = buf.slice(i + 2);
199 let id = null, event = 'message', data = '';
200 for (const line of raw.split('\n')) {
201 if (line.startsWith('id:')) id = line.slice(3).trim();
202 else if (line.startsWith('event:')) event = line.slice(6).trim();
203 else if (line.startsWith('data:')) data += line.slice(5).trim();
204 }
205 let d = null; try { d = JSON.parse(data); } catch { d = null; }
206 if (event === 'step' && id && /^\d+$/.test(id) && Number(id) > Number(last)) last = id;
207 if (onEvent(event, d) === 'end') return 'end';
208 }
209 }
210 };
211 (async () => {
212 let failures = 0;
213 while (!stopped && !(signal && signal.aborted)) {
214 try { const how = await once(); failures = 0; if (how === 'end') return; }
215 catch { if (signal && signal.aborted) return; failures++; }
216 await new Promise((r) => setTimeout(r, Math.min(30000, 800 * 2 ** Math.min(failures, 5))));
217 }
218 })();
219 return () => { stopped = true; };
220 }
221
222 const FILTERS = [['all', 'All', 'spark'], ['chat', 'Chats', 'chat'], ['task', 'Tasks', 'robot'], ['code', 'Code', 'code']];
223 const FILTER_IDS = FILTERS.map(([id]) => id);
224
225 function mount(host, o = {}) {
226 styles();
227 const base = String(o.base || 'https://openvibe.network/api/frame').replace(/\/+$/, '');
228 const tokenOf = () => (typeof o.token === 'function' ? o.token() : o.token) || '';
229 const headers = () => { const t = tokenOf(); return t ? { Authorization: `Bearer ${t}` } : {}; };
230 const api = async (path, init = {}) => {
231 const r = await fetch(`${base}/runs${path}`, { ...init, credentials: 'omit', headers: { Accept: 'application/json', ...(init.body ? { 'Content-Type': 'application/json' } : {}), ...headers(), ...(init.headers || {}) } });
232 const j = await r.json().catch(() => ({}));
233 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; }
234 return j;
235 };
236 const box = el('div', 'ovr');
237 host.textContent = ''; host.append(box);
238 const actionsBase = o.actions ? String(o.actions.base || 'https://openvibe.network').replace(/\/+$/, '') : '';
239 const place = (p) => { try { if (typeof o.onPlace === 'function') o.onPlace(p); } catch { /* the host's */ } };
240 let topHandle = null, paneHandle = null, paneId = null;
241 const dropTop = () => { if (topHandle && typeof topHandle.destroy === 'function') { try { topHandle.destroy(); } catch { /* */ } } topHandle = null; };
242 const dropPane = () => { if (paneHandle && typeof paneHandle.destroy === 'function') { try { paneHandle.destroy(); } catch { /* */ } } paneHandle = null; paneId = null; };
243 const paneFor = (f) => (o.panes && typeof o.panes[f] === 'function' ? o.panes[f] : null);
244 /** The Action Center's items (common.action-item@1) that need you or are working, as rows beside the runs. */
245 async function actionRows(runs) {
246 if (!actionsBase || !['all', 'task'].includes(state.filter)) return [];
247 try {
248 const r = await fetch(`${actionsBase}/api/v1/me/actions`, { headers: { Accept: 'application/json', ...headers() }, credentials: 'omit' });
249 if (!r.ok) return [];
250 const doc = await r.json();
251 const shown = new Set(runs.map((x) => x.ref));
252 runs.forEach((x) => { if (x.waiting && x.waiting.approval) shown.add(`approval:${x.waiting.approval}`); });
253 const out = [];
254 for (const bucket of ['waiting', 'running']) {
255 for (const it of (doc && doc.buckets && Array.isArray(doc.buckets[bucket]) ? doc.buckets[bucket] : []).slice(0, 30)) {
256 if (!it || typeof it.id !== 'string' || shown.has(`${it.service}:${it.id}`) || (it.kind === 'approval' && shown.has(`approval:${it.id}`))) continue;
257 out.push({
258 action: it, id: it.id, kind: it.kind === 'code_task' ? 'code' : 'task', title: it.title, url: it.href,
259 status: bucket === 'waiting' ? 'waiting' : 'running', updated_at: it.updated_at,
260 waiting: bucket === 'waiting' ? { for: 'approval', what: it.summary || it.title } : null,
261 doing: bucket === 'running' ? { text: it.step || it.summary || 'Working', glyph: it.kind === 'code_task' ? 'code' : 'robot' } : null,
262 preview: it.summary ? { text: it.summary } : null,
263 agent: { id: `${it.service}`, name: it.agent || SERVICE_NAME[it.service] || 'OpenVibe', glyph: it.kind === 'code_task' ? 'code' : it.kind === 'ai_run' ? 'spark' : 'robot' },
264 });
265 }
266 }
267 return out;
268 } catch { return []; }
269 }
270 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 };
271
272 function clearStream() { if (state.stopStream) state.stopStream(); state.stopStream = null; if (state.abort) state.abort.abort(); state.abort = null; }
273
274 // ── the composer: the shared one when it is there, else a plain box ──
275 function composer(where, { placeholder, onSend }) {
276 const slot = el('div', 'ovr-compose');
277 where.append(slot);
278 const plain = () => {
279 const f = el('form', 'ovr-plain'); const ta = el('textarea'); ta.rows = 2; ta.placeholder = placeholder; ta.setAttribute('aria-label', placeholder);
280 const go = el('button', 'ovr-send'); go.type = 'submit'; go.setAttribute('aria-label', 'Send'); go.append(svg('send'));
281 f.append(ta, go);
282 f.addEventListener('submit', async (e) => {
283 e.preventDefault(); const t = ta.value.trim(); if (!t) return; go.disabled = true;
284 const old = slot.querySelector('.ovr-note'); if (old) old.remove();
285 try { if (await onSend({ text: t }) !== false) ta.value = ''; }
286 catch (err) { slot.append(el('p', 'ovr-note is-err', (err && err.message) || 'That did not send')); }
287 finally { go.disabled = false; }
288 });
289 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 })); } });
290 slot.append(f);
291 };
292 const C = root.OpenVibeComposer;
293 const real = () => {
294 try {
295 // The composer notes a failed send itself and keeps the words.
296 root.OpenVibeComposer.mount(slot, { preset: 'agent', placeholder, label: placeholder, examples: o.examples, user: o.me, noDraft: true,
297 onSubmit: (v) => onSend({ text: v.text, files: v.files, mode: v.mode }) });
298 } catch { slot.textContent = ''; plain(); }
299 };
300 if (C && C.mount) real();
301 else if (typeof o.sibling === 'function') {
302 let sc = document.getElementById('ov-composer-script');
303 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); }
304 plain();
305 sc.addEventListener('load', () => { if (slot.isConnected && root.OpenVibeComposer) { slot.textContent = ''; real(); } }, { once: true });
306 } else plain();
307 }
308
309 /** A new run with these words (Vibe answers, or hands the work on), then its thread. */
310 async function start(text) {
311 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) }) });
312 open(run.ref || run.id, run);
313 return run;
314 }
315
316 // ── the list ──
317 async function list(filter) {
318 if (filter && FILTER_IDS.includes(filter)) state.filter = filter;
319 clearStream();
320 const gen = ++state.gen;
321 // The list's frame (filters, the box) is drawn once; a filter only reloads the rows, so the words being typed stay.
322 if (state.view !== 'list' || !box.querySelector('.ovr-rows')) {
323 state.view = 'list'; state.run = null;
324 box.textContent = ''; box.className = 'ovr is-list';
325 const head = el('div', 'ovr-head');
326 const tabs = el('div', 'ovr-filters'); tabs.setAttribute('role', 'tablist'); tabs.setAttribute('aria-label', 'Show');
327 for (const [id, label, ic] of FILTERS) {
328 const b = el('button', 'ovr-filter'); b.type = 'button'; b.setAttribute('role', 'tab'); b.dataset.filter = id;
329 b.append(svg(ic), el('span', '', label));
330 b.addEventListener('click', () => { if (state.filter !== id || state.view !== 'list') list(id); });
331 tabs.append(b);
332 }
333 tabs.addEventListener('keydown', (e) => {
334 if (e.key !== 'ArrowRight' && e.key !== 'ArrowLeft') return;
335 const all = [...tabs.children]; const k = all.indexOf(document.activeElement); if (k < 0) return;
336 const next = all[(k + (e.key === 'ArrowRight' ? 1 : all.length - 1)) % all.length];
337 e.preventDefault(); next.focus(); next.click();
338 });
339 head.append(tabs);
340 const rows = el('ul', 'ovr-rows'); rows.setAttribute('aria-label', 'Your runs');
341 const foot = el('div', 'ovr-list-note');
342 const top = el('div', 'ovr-top');
343 const pane = el('div', 'ovr-pane'); pane.hidden = true;
344 box.append(head, top, rows, foot, pane);
345 dropTop(); dropPane();
346 if (Array.isArray(o.starters) && o.starters.length) {
347 const st = el('div', 'ovr-starters'); st.setAttribute('role', 'group'); st.setAttribute('aria-label', 'Things to ask');
348 o.starters.slice(0, 8).forEach((x) => {
349 const [ic, words] = Array.isArray(x) ? x : ['spark', String(x)];
350 const b = el('button', 'ovr-starter'); b.type = 'button'; b.append(svg(G[ic] ? ic : 'spark'), el('span', '', words));
351 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)); } });
352 st.append(b);
353 });
354 box.append(st);
355 }
356 composer(box, { placeholder: o.placeholder || 'Ask, or say what to make', onSend: (v) => start(v.text) });
357 }
358 place({ filter: state.filter });
359 box.querySelectorAll('.ovr-filter').forEach((b) => { const on = b.dataset.filter === state.filter; b.setAttribute('aria-selected', String(on)); b.tabIndex = on ? 0 : -1; });
360 // A filter the host draws itself (a pane): it takes the place of the rows, the starters and the box. The host's
361 // pieces may share one owner (the Dock lends one panel at a time), so the old one goes before the new one comes.
362 const pf = paneFor(state.filter);
363 const topEl = box.querySelector('.ovr-top'), paneEl = box.querySelector('.ovr-pane');
364 const wantTop = !pf && ['all', 'task'].includes(state.filter) && typeof o.top === 'function';
365 if (!wantTop) { dropTop(); topEl.textContent = ''; }
366 if (paneId !== (pf ? state.filter : null)) { dropPane(); paneEl.textContent = ''; }
367 box.classList.toggle('has-pane', !!pf);
368 paneEl.hidden = !pf; topEl.hidden = !wantTop;
369 { const st = box.querySelector('.ovr-starters'); if (st) st.hidden = !!pf || !['all', 'chat'].includes(state.filter); }
370 if (pf) {
371 if (!paneHandle) { paneId = state.filter; try { paneHandle = pf(paneEl) || {}; } catch { paneHandle = {}; } }
372 const rows0 = box.querySelector('.ovr-rows'); rows0.textContent = ''; rows0.removeAttribute('aria-busy'); box.querySelector('.ovr-list-note').textContent = '';
373 return;
374 }
375 if (wantTop && !topHandle) { try { topHandle = o.top(topEl) || null; } catch { topHandle = null; } }
376 const rows = box.querySelector('.ovr-rows'); const foot = box.querySelector('.ovr-list-note');
377 rows.textContent = ''; foot.textContent = ''; rows.setAttribute('aria-busy', 'true');
378 for (let i = 0; i < 3; i++) rows.append(el('li', 'ovr-row is-ghost'));
379 let out;
380 try { out = await api(`?limit=40${state.filter !== 'all' ? `&kind=${state.filter}` : ''}`); }
381 catch (err) {
382 if (gen !== state.gen) return;
383 rows.textContent = ''; rows.removeAttribute('aria-busy');
384 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.'));
385 return;
386 }
387 if (gen !== state.gen) return;
388 state.runs = Array.isArray(out.runs) ? out.runs : [];
389 const extra = await actionRows(state.runs);
390 if (gen !== state.gen) return;
391 if (extra.length) {
392 // What needs you first, then newest: the Action Center's rows join in that order.
393 const needs = (r) => (needsPerson(r) ? 1 : 0);
394 state.runs = state.runs.concat(extra).sort((x, y) => needs(y) - needs(x) || String(y.updated_at || '').localeCompare(String(x.updated_at || '')));
395 }
396 rows.textContent = ''; rows.removeAttribute('aria-busy');
397 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.'));
398 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); });
399 if (Array.isArray(out.partial) && out.partial.length) foot.append(el('p', 'ovr-note', `Some of your runs did not load (${out.partial.join(', ')}).`));
400 }
401 function row(r, i) {
402 const away = r.action ? httpsUrl(r.url) : '';
403 const b = away ? el('a', `ovr-row${needsPerson(r) ? ' is-needs' : ''}`) : el('button', `ovr-row${needsPerson(r) ? ' is-needs' : ''}`);
404 if (away) { b.href = away; b.target = '_blank'; b.rel = 'noopener'; } else b.type = 'button';
405 b.style.setProperty('--i', String(Math.min(i, 12)));
406 const t = el('span', 'ovr-row-t');
407 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)));
408 const line = el('span', 'ovr-row-line', needsPerson(r) || r.status === 'running' ? statusLine(r) : (r.preview && r.preview.text) || statusLine(r));
409 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(' · ')));
410 if (needsPerson(r)) meta.append(el('i', 'ovr-pill is-needs', 'Needs you'));
411 else if (r.status === 'running') meta.append(el('i', 'ovr-pill is-working', 'Working'));
412 else if (r.status === 'failed') meta.append(el('i', 'ovr-pill is-failed', 'Did not finish'));
413 t.append(top, line, meta);
414 b.append(face(r), t);
415 if (!away) b.addEventListener('click', () => open(r.ref || r.id, r));
416 return b;
417 }
418 /** A run that asks before it acts is answered from the list (the Action Center's approve in place). */
419 function answerRow(r) {
420 const w = r && r.waiting;
421 const it = r && r.action;
422 if (it && !(needsPerson(r) && Array.isArray(it.actions) && it.actions.includes('approve'))) return null;
423 if (!it && (!needsPerson(r) || w.for !== 'approval' || !w.approval || !/^[A-Za-z0-9._~-]{1,160}$/.test(String(w.approval)))) return null;
424 const box2 = el('div', 'ovr-row-acts');
425 const say = (text, err) => { box2.textContent = ''; box2.append(el('p', `ovr-note${err ? ' is-err' : ''}`, text)); };
426 [['approve', 'Approve', true], ['decline', 'Not now', false]].forEach(([id, label, primary]) => {
427 const b = el('button', primary ? 'ovr-btn is-primary' : 'ovr-btn', label); b.type = 'button';
428 b.setAttribute('aria-label', `${label}: ${r.title || 'this run'}`);
429 b.addEventListener('click', async () => {
430 [...box2.querySelectorAll('button')].forEach((x) => { x.disabled = true; });
431 try {
432 if (it) {
433 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: '{}' });
434 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})`); }
435 } else await api(`/${encodeURIComponent(r.ref || r.id)}/approvals/${encodeURIComponent(w.approval)}`, { method: 'POST', body: JSON.stringify({ answer: id }) });
436 say(id === 'approve' ? 'Approved' : 'Declined');
437 }
438 catch (err) { [...box2.querySelectorAll('button')].forEach((x) => { x.disabled = false; }); box2.append(el('p', 'ovr-note is-err', err.message)); }
439 });
440 box2.append(b);
441 });
442 return box2;
443 }
444 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; }
445
446 // ── the thread ──
447 async function open(ref, known) {
448 clearStream();
449 dropTop(); dropPane();
450 const gen = ++state.gen;
451 state.view = 'thread'; state.run = known || null; state.steps = new Map();
452 place({ ref });
453 box.textContent = ''; box.className = 'ovr is-thread';
454 const head = el('div', 'ovr-thead');
455 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'));
456 back.addEventListener('click', () => list());
457 const who = el('div', 'ovr-who'); const title = el('b'); const sub = el('small');
458 who.append(title, sub);
459 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'));
460 const faceSlot = el('span', 'ovr-face-slot');
461 head.append(back, faceSlot, who, away);
462 const thread = el('div', 'ovr-thread'); thread.setAttribute('role', 'log'); thread.setAttribute('aria-live', 'polite');
463 box.append(head, thread);
464 const paintHead = (r) => {
465 state.run = r;
466 faceSlot.textContent = ''; faceSlot.append(face(r, 's'));
467 title.textContent = r.title || (r.agent && r.agent.name) || 'Run';
468 sub.textContent = [KIND_WORD[r.kind] || 'Task', statusLine(r)].filter(Boolean).join(' · ');
469 const url = httpsUrl(r.url); away.hidden = !url; if (url) away.href = url;
470 box.classList.toggle('is-needs', needsPerson(r));
471 };
472 if (known) paintHead(known);
473 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); } });
474 const add = (s) => {
475 if (!s || !Number.isInteger(s.seq)) return;
476 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; }
477 const wasNear = thread.scrollHeight - thread.scrollTop - thread.clientHeight < 160;
478 const none = thread.querySelector(':scope > .ovr-none'); if (none) none.remove();
479 const n = stepNode(s);
480 state.steps.set(s.seq, n);
481 const after = [...state.steps.keys()].filter((k) => k > s.seq).sort((a, b) => a - b)[0];
482 if (after !== undefined && state.steps.get(after).parentNode === thread) thread.insertBefore(n, state.steps.get(after)); else thread.append(n);
483 if (wasNear) requestAnimationFrame(() => { thread.scrollTop = thread.scrollHeight; });
484 };
485 try {
486 const [r, page] = await Promise.all([api(`/${encodeURIComponent(ref)}`), api(`/${encodeURIComponent(ref)}/steps?limit=200`)]);
487 if (gen !== state.gen) return;
488 paintHead(r);
489 (page.steps || []).forEach(add);
490 requestAnimationFrame(() => { thread.scrollTop = thread.scrollHeight; });
491 if (!(page.steps || []).length) thread.append(el('p', 'ovr-note ovr-none', 'Nothing here yet.'));
492 } catch (err) { if (gen !== state.gen) return; thread.append(empty('This run did not load', err.message)); return; }
493 if (isEnd(state.run)) return;
494 const last = Math.max(0, ...state.steps.keys());
495 state.abort = new AbortController();
496 state.stopStream = follow({ url: `${base}/runs/${encodeURIComponent(ref)}/events`, headers: headers(), after: last, signal: state.abort.signal,
497 onEvent(event, d) {
498 if (gen !== state.gen) return 'end';
499 if (event === 'step' && d && d.step) add(d.step);
500 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 || ''))); }
501 else if (event === 'run' && d && d.run) paintHead(d.run);
502 else if (event === 'end') { if (state.run) paintHead({ ...state.run, status: (d && d.status) || 'done' }); return 'end'; }
503 return null;
504 } });
505 }
506
507 function stepNode(s) {
508 const from = s.from || (s.kind === 'message' ? 'agent' : 'agent');
509 if (s.kind === 'message' && from === 'person') {
510 const n = el('div', 'ovr-step is-you'); const b = el('div', 'ovr-bubble'); b.append(words(s.text)); n.append(b);
511 attachments(n, s); return n;
512 }
513 if (s.kind === 'message' || (s.kind !== 'tool' && s.kind !== 'think' && s.kind !== 'approval' && s.kind !== 'child' && s.kind !== 'file' && s.text)) {
514 const n = el('div', `ovr-step is-agent${s.status === 'running' ? ' is-writing' : ''}`);
515 if (s.author && s.author.name) n.append(el('small', 'ovr-author', s.author.name));
516 n.append(words(s.text, s.sources));
517 cardFor(n, s);
518 if (Array.isArray(s.sources) && s.sources.length) {
519 const src = el('div', 'ovr-sources');
520 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); });
521 n.append(src);
522 }
523 attachments(n, s); return n;
524 }
525 if (s.kind === 'think') {
526 const d = el('details', 'ovr-step is-think'); const sm = el('summary'); sm.append(svg('think'), el('span', '', s.title || 'Thinking'));
527 d.append(sm); if (s.text) d.append(words(s.text)); return d;
528 }
529 if (s.kind === 'approval') {
530 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'));
531 n.append(head); if (s.text) n.append(words(s.text));
532 const r = s.result && typeof s.result === 'object' ? s.result : {};
533 const opts = Array.isArray(r.options) ? r.options : [{ id: 'approve', label: 'Approve' }, { id: 'decline', label: 'Not now' }];
534 const row = el('div', 'ovr-approval-acts');
535 opts.slice(0, 4).forEach((op, i) => {
536 const b = el('button', i === 0 ? 'ovr-btn is-primary' : 'ovr-btn', op.label || op.id); b.type = 'button';
537 b.addEventListener('click', async () => {
538 [...row.children].forEach((x) => { x.disabled = true; });
539 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}`)); }
540 catch (err) { [...row.children].forEach((x) => { x.disabled = false; }); n.append(el('p', 'ovr-note is-err', err.message)); }
541 });
542 row.append(b);
543 });
544 if (!r.answer) n.append(row); else n.append(el('p', 'ovr-note', `Answered: ${r.answer}`));
545 return n;
546 }
547 // tool, child, file: one quiet row
548 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' : ''}`);
549 const line = el(s.text && s.kind === 'tool' ? 'summary' : 'div', 'ovr-row-step');
550 const glyph = G[s.glyph] ? s.glyph : s.kind === 'child' ? 'child' : s.kind === 'file' ? 'file' : 'tool';
551 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')));
552 if (s.status === 'running') line.append(el('i', 'ovr-pulse'));
553 if (s.status === 'failed') line.append(svg('x', 'ovr-fail'));
554 n.append(line);
555 if (s.text && s.kind === 'tool') n.append(words(s.text));
556 if (s.kind === 'child' && s.child_run) { n.classList.add('is-link'); n.addEventListener('click', () => open(s.child_run)); }
557 return n;
558 }
559 function cardFor(n, s) {
560 if (!s.ui || !s.ui.type) return;
561 const render = CARDS.get(s.ui.type); if (!render) return;
562 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 */ }
563 }
564 function attachments(n, s) {
565 const list = Array.isArray(s.attachments) ? s.attachments : []; if (!list.length) return;
566 const row = el('div', 'ovr-files');
567 list.slice(0, 8).forEach((a) => {
568 const url = httpsUrl(a.url);
569 if (a.kind === 'image' && url) { const i = el('img', 'ovr-thumb'); i.src = url; i.alt = a.alt || ''; i.loading = 'lazy'; row.append(i); }
570 else { const c = el('span', 'ovr-chip'); c.append(svg('file'), el('span', '', a.name || a.kind)); row.append(c); }
571 });
572 n.append(row);
573 }
574
575 /** Words in the list's box, unsent, with the focus there (a page's "Ask your agent"). */
576 function prefill(text) {
577 if (state.view !== 'list' || paneFor(state.filter)) list(paneFor(state.filter) ? 'all' : undefined);
578 const put = (n = 0) => {
579 const ta = box.querySelector('.ovr-compose textarea');
580 if (!ta) { if (n < 40) setTimeout(() => put(n + 1), 50); return; }
581 ta.value = String(text || '').slice(0, 16000);
582 ta.dispatchEvent(new Event('input', { bubbles: true }));
583 try { ta.focus({ preventScroll: true }); ta.setSelectionRange(ta.value.length, ta.value.length); } catch { /* */ }
584 };
585 put();
586 }
587 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 }; } };
588 if (o.open) open(o.open); else list();
589 return view;
590 }
591
592 let styled = false;
593 function styles() {
594 if (styled || document.getElementById('ovr-styles')) { styled = true; return; }
595 styled = true;
596 const st = document.createElement('style'); st.id = 'ovr-styles';
597 st.textContent = `
598.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); }
599.ovr svg { width: 18px; height: 18px; flex: none; fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
600.ovr-head { padding: 4px 4px 8px; }
601.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); }
602.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; }
603.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); }
604.ovr-filter[aria-selected="true"] svg { color: var(--accent-light, var(--accent, #60a5fa)); }
605.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; }
606@media (max-width: 380px) { .ovr-filter { font-size: 13px; gap: 5px; } .ovr-filter svg { width: 16px; height: 16px; } }
607.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; }
608.ovr-rows > li { display: grid; min-width: 0; }
609.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); }
610.ovr-rows > li.has-acts > .ovr-row { background: transparent; box-shadow: none; }
611.ovr-row-acts { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 0 12px 12px 72px; }
612.ovr-row-acts .ovr-btn { min-height: 38px; font-size: 13.5px; }
613.ovr-row-acts .ovr-note { margin: 0; }
614@media (max-width: 380px) { .ovr-row-acts { padding-left: 12px; } }
615.ovr-rows:empty { display: none; } .ovr-list-note:empty { display: none; } .ovr-list-note { padding: 0 4px; }
616.ovr.is-list > .ovr-compose, .ovr.is-list > .ovr-starters:not([hidden]) { margin-top: auto; }
617.ovr.is-list > .ovr-starters:not([hidden]) + .ovr-compose { margin-top: 0; }
618/* On a wide screen the list, the thread and the box keep a reading column. */
619.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; }
620.ovr-thread { padding-inline: max(4px, calc((100% - 772px) / 2)); }
621.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); }
622.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); }
623.ovr-row:active { transform: scale(.985); }
624@media (hover: hover) and (pointer: fine) { .ovr-row:hover { background: color-mix(in srgb, var(--text-primary, #e6edf7) 8%, transparent); } }
625.ovr-row.is-ghost { min-height: 72px; animation: ovr-shimmer 1.4s ease-in-out infinite; cursor: default; }
626.ovr-row-t { flex: 1; min-width: 0; display: grid; gap: 3px; }
627.ovr-row-top { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
628.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; }
629.ovr-row-top time { flex: none; font-size: 12px; color: var(--text-muted, #93a0b4); }
630.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; }
631.ovr-row-meta { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-muted, #93a0b4); }
632.ovr-row-meta svg { width: 14px; height: 14px; }
633.ovr-pill { font-style: normal; font-size: 11.5px; font-weight: 750; padding: 1px 8px; border-radius: 999px; margin-left: auto; }
634.ovr-pill.is-needs { color: #fbbf24; background: color-mix(in srgb, #f59e0b 16%, transparent); }
635.ovr-pill.is-working { color: var(--accent-light, #60a5fa); background: color-mix(in srgb, var(--accent, #3b82f6) 16%, transparent); }
636.ovr-pill.is-failed { color: #f87171; background: color-mix(in srgb, #ef4444 14%, transparent); }
637.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); }
638:root[data-theme="light"] .ovr-face { color: hsl(var(--ovr-h, 215) 70% 30%); }
639@media (prefers-color-scheme: light) { :root:not([data-theme="dark"]) .ovr-face { color: hsl(var(--ovr-h, 215) 70% 30%); } }
640.ovr-face.is-m { width: 48px; height: 48px; } .ovr-face.is-m > svg { width: 26px; height: 26px; }
641.ovr-face.is-s { width: 38px; height: 38px; } .ovr-face.is-s > svg { width: 21px; height: 21px; }
642.ovr-face img { width: 100%; height: 100%; border-radius: inherit; object-fit: cover; }
643.ovr-face.is-needs { box-shadow: 0 0 0 2px #f59e0b; }
644.ovr-face.is-working { animation: ovr-breathe 2.4s ease-in-out infinite; }
645.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); }
646.ovr-doing svg { width: 12px; height: 12px; stroke-width: 2.3; }
647.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); }
648.ovr-empty > svg { width: 24px; height: 24px; color: var(--accent-light, #60a5fa); }
649.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); }
650.ovr-note { margin: 6px 4px; font-size: 13px; color: var(--text-muted, #93a0b4); } .ovr-note.is-err { color: #f87171; }
651.ovr-compose { flex: none; padding: 8px 4px 4px; }
652.ovr-top { flex: none; }
653.ovr-pane { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; overflow: hidden; } .ovr-pane[hidden] { display: none; }
654.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; }
655.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); }
656.ovr-starters::-webkit-scrollbar { display: none; }
657.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; }
658.ovr-starter svg { width: 16px; height: 16px; color: var(--accent-light, #60a5fa); }
659.ovr-starter:disabled { opacity: .5; cursor: default; } .ovr-starter:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 2px; }
660@media (hover: hover) and (pointer: fine) { .ovr-starter:hover { color: var(--text-primary, #e6edf7); background: color-mix(in srgb, var(--accent, #3b82f6) 12%, transparent); } }
661a.ovr-row { color: inherit; text-decoration: none; }
662.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)); }
663.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; }
664.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)); }
665.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)); }
666.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; }
667.ovr-ib[hidden] { display: none; } .ovr-ib:active { transform: scale(.92); }
668@media (hover: hover) { .ovr-ib:hover { color: var(--text-primary, #e6edf7); background: color-mix(in srgb, var(--text-primary, #e6edf7) 13%, transparent); } }
669.ovr-who { flex: 1; min-width: 0; display: grid; gap: 1px; }
670.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; }
671.ovr-who small { font-size: 12.5px; color: var(--text-secondary, #a8b3c4); overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
672.ovr.is-needs .ovr-who small { color: #fbbf24; }
673.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; }
674.ovr-step { max-width: min(640px, 92%); animation: ovr-rise .26s cubic-bezier(.2,.8,.2,1) both; }
675.ovr-step.is-you { align-self: flex-end; }
676.ovr-bubble { padding: 9px 13px; border-radius: 18px 18px 6px 18px; color: var(--on-accent-strong, #fff); background: var(--accent-strong, var(--accent, #2563eb)); }
677.ovr-bubble .ovr-a { color: inherit; text-decoration-color: color-mix(in srgb, currentColor 55%, transparent); text-underline-offset: 2px; }
678.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); }
679.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; }
680.ovr-author { display: block; margin-bottom: 3px; font-size: 12px; font-weight: 750; color: var(--text-muted, #93a0b4); }
681.ovr-words { font-size: 15px; line-height: 1.5; overflow-wrap: anywhere; }
682.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; }
683.ovr-words ul, .ovr-words ol { margin: 0; padding-left: 20px; } .ovr-words li + li { margin-top: 3px; }
684.ovr-a { color: var(--accent-light, #60a5fa); text-decoration: underline; text-underline-offset: 2px; }
685.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); }
686.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); }
687.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); }
688.ovr-sources { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
689.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); }
690.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); }
691.ovr-source span { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; max-width: 220px; }
692.ovr-card { margin-top: 10px; }
693.ovr-chips { display: flex; flex-wrap: wrap; gap: 6px; }
694.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); }
695.ovr-chip svg { width: 15px; height: 15px; color: var(--accent-light, #60a5fa); } .ovr-chip.is-undone { opacity: .5; text-decoration: line-through; }
696.ovr-table-wrap { overflow-x: auto; } .ovr-table { border-collapse: collapse; font-size: 13.5px; min-width: 100%; }
697.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); }
698.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); }
699.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); }
700.ovr-step.is-row, .ovr-step.is-think { align-self: flex-start; max-width: 100%; }
701.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; }
702.ovr-step.is-think > summary, details.ovr-step > .ovr-row-step { cursor: pointer; }
703.ovr-step.is-think > summary::-webkit-details-marker, details.ovr-step > summary::-webkit-details-marker { display: none; }
704.ovr-row-step svg, .ovr-step.is-think > summary svg { width: 16px; height: 16px; color: var(--text-muted, #93a0b4); }
705.ovr-step.is-row.is-running .ovr-row-step { color: var(--text-primary, #e6edf7); }
706.ovr-step.is-row.is-failed .ovr-row-step { color: #f87171; }
707.ovr-step.is-row.is-link { cursor: pointer; }
708.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); }
709.ovr-pulse { width: 8px; height: 8px; border-radius: 50%; background: var(--accent-light, #60a5fa); animation: ovr-pulse 1.2s ease-in-out infinite; }
710.ovr-fail { width: 15px; height: 15px; color: #f87171; }
711.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); }
712.ovr-approval-h { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; } .ovr-approval-h svg { color: #fbbf24; }
713.ovr-approval-acts { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
714.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); }
715.ovr-btn.is-primary { color: var(--on-accent-strong, #fff); background: var(--accent-strong, var(--accent, #2563eb)); } .ovr-btn:disabled { opacity: .5; cursor: default; }
716.ovr-files { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; } .ovr-step.is-you .ovr-files { justify-content: flex-end; }
717.ovr-thumb { width: 120px; height: 90px; object-fit: cover; border-radius: 12px; }
718@keyframes ovr-rise { from { opacity: 0; transform: translateY(6px); } }
719@keyframes ovr-shimmer { 50% { opacity: .55; } }
720@keyframes ovr-breathe { 50% { box-shadow: 0 0 0 5px color-mix(in srgb, var(--accent, #3b82f6) 22%, transparent); } }
721@keyframes ovr-pulse { 50% { opacity: .35; transform: scale(.7); } }
722@keyframes ovr-caret { 50% { opacity: 0; } }
723html[data-ov-motion=reduced] .ovr-row, html[data-ov-motion=reduced] .ovr-step { animation-name: ovr-fade; animation-delay: 0s; }
724@keyframes ovr-fade { from { opacity: 0; } }
725html[data-ov-motion=reduced] .ovr-face.is-working { animation-duration: 5s; }
726html[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; }
727`;
728 document.head.appendChild(st);
729 }
730
731 root.OpenVibeRun = { mount, card, version: 1 };
732 if (typeof module !== 'undefined' && module.exports) module.exports = root.OpenVibeRun;
733})(typeof window !== 'undefined' ? window : globalThis);
734