openvibers/openvibe.shared Commits
4.211.0: an agent's sheet; the You tab leads with you; a composer's count only near its limit (#472)
Changes
@@@ -1,5 +1,22 @@# Changelog++## 4.211.0 — 2026-11-11++++- **The menu's You tab leads with you (ask 158b, "the You tab still sucks").** Its notifications are a preview of the++ newest three, with no search, filters, foot or Load more until "Show all N" opens the whole list in place; the++ person's bar, their AI and the rest are a glance below instead of a thousand pixels down. The person's name and handle++ wrap instead of being cut (rule names-are-never-cut).++- **An agent's sheet (agent-sheet.js, new; the Dock's Actor row opens it).** One window (ov-window.js: a panel beside++ the row on a computer, a sheet on a phone) with everything about one agent: its face, state and instructions, then++ Now, Asks (first when it waits for you, with the count), Work, Schedule, Memory and Computer as tabs (arrow keys move++ between them), each with its own words when empty; the mark's wait while it reads and Retry when a read fails. Data:++ Contracts actor.agent-sheet@1 / codes.agent-sheet@1 through Network's relay (/api/frame/agents/:id/sheet,++ /api/frame/codes-agents/:id/sheet) with the bearer, no cookies; all text, never markup.++ `OVAgentSheet.open({ kind, id, name, token, frameBase, base, from, openHref })`. The row used to leave for the++ Actor's page; that page is the sheet's Open now (a modified click still goes straight there).++- **A composer's count shows only near its limit (ov-composer.js).** The last 15% of the box, at most 200 and at least++ 20 characters: a 120-character search box said "120 left" while it was empty (Exchange's /models).++## 4.210.0 — 2026-11-11- **A page's title and its line wrap before they are cut (kit.css `.ovk-head`).** The title takes up to two lines and@@@ -5,7 +5,7 @@**Docs:** https://openvibe.services · **For AI:** https://openvibe.network/llms.txt**Install and test:** `npm ci && npm test`--**Status:** alpha, v4.210.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed++**Status:** alpha, v4.211.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployedconsumer installs a tagged release and none keeps a vendored copy([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` namesthe tag it pins.@@@ -4,7 +4,7 @@"code": true,"wave": "W2","package": "openvibe-shared",-- "version": "4.210.0",++ "version": "4.211.0","plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)","deployed": false,"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",nnew file mode 100644@@@ -0,0 +1,236 @@++// ═══════════════════════════════════════════════════════════════++// OpenVibe — an agent's sheet (openvibe-shared 4.211.0; Opus, 2026-11-11)++//++// One window (ov-window.js: a panel beside what opened it on a computer, a sheet on a phone) that says everything about++// one agent: who it is and its state, what it is doing now, what it asks you, the work it did, when it runs, what it++// remembers and its computer. The data is the agent's sheet (Contracts actor.agent-sheet@1 / codes.agent-sheet@1),++// read through Network's same-origin relay with the person's token:++// OVAgentSheet.open({ kind: 'actor' | 'codes', id, name, token, frameBase, base, from, openHref }) → { close() }++// The parts are tabs (a segmented row; arrow keys move between them); a part with nothing in it says so in its own++// words (asks_empty) rather than disappearing. While it reads, the window shows the mark's wait; a failed read says so++// with Retry. Everything an agent or a person wrote is set as text, never markup.++// ═══════════════════════════════════════════════════════════════++(function (root) {++ 'use strict';++ if (typeof document === 'undefined' || root.OVAgentSheet) return;++++ const CSS = `.ovas { display: grid; gap: 12px; padding: 4px 2px 6px; }++.ovas-who { display: flex; align-items: center; gap: 12px; min-width: 0; }++.ovas-face { flex: none; width: 44px; height: 44px; border-radius: 50%; display: grid; place-items: center; overflow: hidden; font-weight: 800; font-size: 17px;++ color: #fff; background: linear-gradient(135deg, var(--accent, #3b82f6), color-mix(in srgb, var(--accent, #3b82f6) 45%, #7c3aed)); box-shadow: 0 0 0 3px var(--ovas-ring, transparent); }++.ovas-face img { width: 100%; height: 100%; object-fit: cover; }++.ovas[data-state="working"] { --ovas-ring: color-mix(in srgb, #22c55e 55%, transparent); }++.ovas[data-state="paused"] { --ovas-ring: color-mix(in srgb, #f59e0b 55%, transparent); }++.ovas-name { display: grid; gap: 2px; min-width: 0; flex: 1; }++.ovas-name b { font-size: 16px; overflow-wrap: anywhere; }++.ovas-name small { color: var(--text-secondary, #a8b3c4); font-size: 12.5px; overflow-wrap: anywhere; }++.ovas-state { display: inline-flex; align-items: center; gap: 6px; padding: 2px 9px; border-radius: 999px; font-size: 12px; font-weight: 650; width: fit-content;++ background: color-mix(in srgb, var(--text-primary, #e6edf7) 8%, transparent); }++.ovas-state::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ovas-dot, #94a3b8); }++.ovas[data-state="working"] .ovas-state { --ovas-dot: #22c55e; }++.ovas[data-state="paused"] .ovas-state { --ovas-dot: #f59e0b; }++.ovas-open { flex: none; display: inline-flex; align-items: center; min-height: 36px; padding: 0 12px; border-radius: 10px; color: var(--accent-light, #60a5fa);++ font-size: 13px; font-weight: 650; text-decoration: none; }++.ovas-open:hover { background: color-mix(in srgb, var(--accent, #3b82f6) 12%, transparent); }++.ovas-tabs { display: flex; gap: 4px; padding: 4px; border-radius: 999px; overflow-x: auto; scrollbar-width: none; background: color-mix(in srgb, var(--text-primary, #e6edf7) 6%, transparent); }++.ovas-tabs::-webkit-scrollbar { display: none; }++@media (max-width: 520px) { .ovas-tabs { -webkit-mask-image: linear-gradient(90deg, #000 85%, transparent); mask-image: linear-gradient(90deg, #000 85%, transparent); } }++.ovas-tab { flex: none; display: inline-flex; align-items: center; gap: 6px; min-height: 34px; padding: 0 13px; border: 0; border-radius: 999px; background: none;++ color: var(--text-secondary, #a8b3c4); font: inherit; font-size: 13px; cursor: pointer; }++.ovas-tab[aria-selected="true"] { background: var(--bg-primary, #0b1220); color: var(--text-primary, #e6edf7); font-weight: 650; box-shadow: 0 1px 3px rgba(0, 0, 0, .25); }++.ovas-tab em { font-style: normal; font-size: 11px; font-weight: 700; padding: 0 6px; border-radius: 999px; background: color-mix(in srgb, #f59e0b 22%, transparent); color: #fbbf24; }++.ovas-tab:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 1px; }++.ovas-part { display: grid; align-content: start; gap: 8px; min-height: 90px; }++.ovas-part[hidden] { display: none; }++.ovas-empty { margin: 0; padding: 18px 8px; text-align: center; color: var(--text-secondary, #a8b3c4); font-size: 13.5px; }++.ovas-card { display: grid; gap: 4px; padding: 11px 13px; border-radius: 14px; border: 1px solid var(--border, rgba(255, 255, 255, .1));++ background: color-mix(in srgb, var(--text-primary, #e6edf7) 3%, transparent); min-width: 0; }++.ovas-card b { font-size: 14px; font-weight: 650; overflow-wrap: anywhere; }++.ovas-card p { margin: 0; color: var(--text-secondary, #a8b3c4); font-size: 13px; overflow-wrap: anywhere; }++.ovas-line { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; color: var(--text-muted, #7d8aa0); font-size: 12px; font-variant-numeric: tabular-nums; }++.ovas-chip { display: inline-flex; align-items: center; padding: 1px 8px; border-radius: 999px; font-size: 11.5px; font-weight: 650;++ background: color-mix(in srgb, var(--text-primary, #e6edf7) 8%, transparent); color: var(--text-primary, #e6edf7); }++.ovas-chip.is-ok { background: color-mix(in srgb, #22c55e 18%, transparent); color: #4ade80; }++.ovas-chip.is-bad { background: color-mix(in srgb, #ef4444 18%, transparent); color: #f87171; }++.ovas-chip.is-run { background: color-mix(in srgb, var(--accent, #3b82f6) 18%, transparent); color: var(--accent-light, #60a5fa); }++.ovas-card.is-now { border-color: color-mix(in srgb, #22c55e 45%, transparent); }++.ovas-card.is-ask { border-color: color-mix(in srgb, #f59e0b 50%, transparent); background: color-mix(in srgb, #f59e0b 7%, transparent); }++.ovas-go { justify-self: start; display: inline-flex; align-items: center; min-height: 40px; padding: 0 16px; border-radius: 12px; text-decoration: none; font-weight: 650; font-size: 14px;++ color: #fff; background: var(--accent, #3b82f6); }++.ovas-wait { display: grid; justify-items: center; gap: 10px; padding: 26px 8px; color: var(--text-secondary, #a8b3c4); font-size: 13.5px; }++.ovas-retry { min-height: 40px; padding: 0 16px; border-radius: 12px; border: 1px solid var(--border, rgba(255, 255, 255, .14)); background: none; color: inherit; font: inherit; cursor: pointer; }++@media (pointer: coarse) { .ovas-tab { min-height: 44px; } .ovas-open { min-height: 44px; } }`;++++ function styles(d) {++ if (d.getElementById('ovas-css')) return;++ const s = d.createElement('style');++ s.id = 'ovas-css';++ s.textContent = CSS;++ d.head.appendChild(s);++ }++ const money = (n) => (Number(n) > 0 ? `$${Number(n).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: Number(n) < 0.1 ? 4 : 2 })}` : '$0');++ function when(iso) {++ const t = Date.parse(iso || '');++ if (!Number.isFinite(t)) return '';++ const s = (t - Date.now()) / 1000;++ const a = Math.abs(s);++ const unit = a < 3600 ? [Math.max(1, Math.round(a / 60)), 'min'] : a < 86400 ? [Math.round(a / 3600), 'h'] : [Math.round(a / 86400), a < 172800 ? 'day' : 'days'];++ if (a < 60) return s > 0 ? 'in a moment' : 'just now';++ return s > 0 ? `in ${unit[0]} ${unit[1]}` : `${unit[0]} ${unit[1]} ago`;++ }++ const tone = (state) => (/^(done|succeeded|ok|passed|completed)$/i.test(state) ? 'is-ok' : /^(failed|error|refused|stopped)$/i.test(state) ? 'is-bad' : /^(running|working|started)$/i.test(state) ? 'is-run' : '');++ const STATE_WORD = { working: 'Working', idle: 'Resting', paused: 'Paused', ready: 'Ready' };++++ function open(opts = {}) {++ const W = root.OVWindow;++ if (!W) throw new Error('ov-window.js is not loaded');++ const d = document;++ styles(d);++ const h = (tag, cls, text) => { const n = d.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = String(text); return n; };++ const kind = opts.kind === 'codes' ? 'codes' : 'actor';++ const frameBase = String(opts.frameBase || 'https://openvibe.network/api/frame').replace(/\/+$/, '');++ const aid = encodeURIComponent(String(opts.id || ''));++ // Through Network's relay on a site; `base` reaches the service itself (its own site, or a test).++ const direct = opts.base ? String(opts.base).replace(/\/+$/, '') : '';++ const url = direct ? (kind === 'codes' ? `${direct}/api/v1/me/agents/${aid}/sheet` : `${direct}/api/v1/me/agents/${aid}?view=sheet`)++ : `${frameBase}/${kind === 'codes' ? 'codes-agents' : 'agents'}/${aid}/sheet`;++ const body = h('div', 'ovas');++ // The window's title names what it is; the agent's own name leads the sheet under it (said once).++ const win = W.open({ id: `agent-sheet-${kind}`, title: kind === 'codes' ? 'Coding agent' : 'Your Actor', body, kind: 'panel', size: 'md', from: opts.from || null,++ onClose: () => { gone = true; } });++ let gone = false;++++ function wait() {++ const box = h('div', 'ovas-wait');++ const mark = h('span', 'ov-mark');++ mark.setAttribute('data-state', 'busy'); mark.setAttribute('data-size', '34'); mark.setAttribute('aria-hidden', 'true');++ box.append(mark, h('span', '', 'Reading your agent…'));++ body.replaceChildren(box);++ }++ function failed(text) {++ const box = h('div', 'ovas-wait');++ const retry = h('button', 'ovas-retry', 'Retry'); retry.type = 'button';++ retry.addEventListener('click', load);++ box.append(h('span', '', text), retry);++ body.replaceChildren(box);++ }++++ function card(cls, title, line, more) {++ const c = h('div', `ovas-card${cls ? ` ${cls}` : ''}`);++ c.append(h('b', '', title));++ if (more) c.append(h('p', '', more));++ if (line && line.childNodes.length) c.append(line);++ return c;++ }++ function workCard(w, isNow) {++ const line = h('div', 'ovas-line');++ if (w.state) line.append(h('span', `ovas-chip ${tone(w.state)}`, w.state));++ if (Number.isFinite(Number(w.cost_usd))) line.append(h('span', '', money(w.cost_usd)));++ if (w.updated_at || w.started_at || w.at) line.append(h('span', '', when(w.updated_at || w.started_at || w.at)));++ const result = typeof w.result === 'string' ? w.result : w.result && (w.result.summary || w.result.text) ? (w.result.summary || w.result.text) : '';++ return card(isNow ? 'is-now' : '', w.task || 'A task', line, result ? String(result).slice(0, 400) : '');++ }++++ function render(sheet) {++ const a = sheet.agent || {};++ body.dataset.state = a.state || '';++ const who = h('div', 'ovas-who');++ const face = h('span', 'ovas-face');++ if (a.face && /^https:\/\//.test(a.face)) { const img = h('img'); img.src = a.face; img.alt = ''; face.append(img); } else face.textContent = String(a.name || opts.name || 'A').trim().charAt(0).toUpperCase();++ const name = h('span', 'ovas-name');++ name.append(h('b', '', a.name || opts.name || 'Your agent'), h('span', 'ovas-state', STATE_WORD[a.state] || a.state || 'Ready'));++ if (a.instructions) name.append(h('small', '', String(a.instructions).slice(0, 160)));++ else if (a.kind) name.append(h('small', '', a.kind));++ who.append(face, name);++ if (opts.openHref) { const go = h('a', 'ovas-open', 'Open ↗'); go.href = opts.openHref; go.target = '_blank'; go.rel = 'noopener'; who.append(go); }++++ const asks = Array.isArray(sheet.asks) ? sheet.asks : [];++ const work = Array.isArray(sheet.work) ? sheet.work : [];++ const parts = [++ ['now', 'Now', () => (sheet.now ? [workCard(sheet.now, true)] : [h('p', 'ovas-empty', a.state === 'paused' ? 'Paused. Nothing runs until you resume it.' : 'Nothing running right now.')])],++ ['asks', 'Asks', () => (asks.length ? asks.map((x) => {++ const line = h('div', 'ovas-line');++ if (x.state) line.append(h('span', 'ovas-chip', x.state));++ if (x.touch && typeof x.touch === 'string') line.append(h('span', '', x.touch));++ return card('is-ask', x.what || 'A request', line, x.why || '');++ }) : [h('p', 'ovas-empty', sheet.asks_empty || 'Nothing to approve.')]), asks.length],++ ['work', 'Work', () => (work.length ? work.map((w) => workCard(w, false)) : [h('p', 'ovas-empty', 'No work yet.')])],++ ['when', 'Schedule', () => {++ const rows = (Array.isArray(sheet.schedules) ? sheet.schedules : []).filter((s) => s && s.schedule !== 'off');++ if (!rows.length) return [h('p', 'ovas-empty', 'It runs when you ask, on no schedule.')];++ return rows.map((s) => {++ const line = h('div', 'ovas-line');++ if (s.next_run_at) line.append(h('span', '', `Next ${when(s.next_run_at)}`));++ if (s.last_run_at) line.append(h('span', '', `Last ${when(s.last_run_at)}`));++ if (s.last_result) line.append(h('span', `ovas-chip ${tone(s.last_result)}`, s.last_result));++ return card('', String(s.schedule || 'On a schedule'), line, '');++ });++ }],++ ['memory', 'Memory', () => {++ const rows = Array.isArray(sheet.memory) ? sheet.memory : [];++ return rows.length ? rows.slice(0, 30).map((m) => { const line = h('div', 'ovas-line'); if (m.source) line.append(h('span', '', m.source)); return card('', String(m.text || '').slice(0, 300), line, ''); }) : [h('p', 'ovas-empty', 'Nothing remembered yet.')];++ }],++ ['computer', 'Computer', () => {++ const c = sheet.computer;++ if (!c) return [h('p', 'ovas-empty', 'No computer is attached to this agent.')];++ const out = [card('', c.name || 'Its computer', null, c.state || c.status || '')];++ const href = [c.url, c.open].find((u) => typeof u === 'string' && /^https:\/\//.test(u));++ if (href) { const go = h('a', 'ovas-go', 'Watch its computer'); go.href = href; go.target = '_blank'; go.rel = 'noopener'; out.push(go); }++ return out;++ }],++ ];++ const tabs = h('div', 'ovas-tabs');++ tabs.setAttribute('role', 'tablist');++ tabs.setAttribute('aria-label', 'About this agent');++ const panes = [];++ const first = asks.length ? 'asks' : 'now';++ parts.forEach(([id, label, build, count]) => {++ const tab = h('button', 'ovas-tab', label);++ tab.type = 'button';++ tab.setAttribute('role', 'tab');++ tab.id = `ovas-t-${id}`;++ tab.setAttribute('aria-controls', `ovas-p-${id}`);++ if (count) tab.append(h('em', '', count));++ const pane = h('div', 'ovas-part');++ pane.id = `ovas-p-${id}`;++ pane.setAttribute('role', 'tabpanel');++ pane.setAttribute('aria-labelledby', tab.id);++ pane.append(...build());++ const on = id === first;++ tab.setAttribute('aria-selected', on ? 'true' : 'false');++ tab.tabIndex = on ? 0 : -1;++ pane.hidden = !on;++ tab.addEventListener('click', () => select(id));++ tabs.append(tab);++ panes.push([id, tab, pane]);++ });++ function select(id) {++ panes.forEach(([pid, tab, pane]) => { const on = pid === id; tab.setAttribute('aria-selected', on ? 'true' : 'false'); tab.tabIndex = on ? 0 : -1; pane.hidden = !on; });++ }++ tabs.addEventListener('keydown', (e) => {++ if (e.key !== 'ArrowRight' && e.key !== 'ArrowLeft') return;++ const i = panes.findIndex(([, tab]) => tab.getAttribute('aria-selected') === 'true');++ const next = panes[(i + (e.key === 'ArrowRight' ? 1 : panes.length - 1)) % panes.length];++ select(next[0]); next[1].focus(); e.preventDefault();++ });++ body.replaceChildren(who, tabs, ...panes.map(([, , pane]) => pane));++ }++++ async function load() {++ wait();++ try {++ const t = typeof opts.token === 'function' ? await opts.token() : opts.token;++ if (!t) { failed('Sign in to see your agent.'); return; }++ const r = await fetch(url, { headers: { accept: 'application/json', authorization: `Bearer ${t}` }, credentials: 'omit' });++ const b = await r.json().catch(() => null);++ if (gone) return;++ if (!r.ok || !b) { failed(r.status === 404 ? 'This agent is not there any more.' : 'Its sheet could not be read just now.'); return; }++ render(b.sheet || b);++ } catch { if (!gone) failed('Its sheet could not be read just now.'); }++ }++ load();++ return { close: () => win.close() };++ }++++ root.OVAgentSheet = { open };++})(typeof window !== 'undefined' ? window : globalThis);@@@ -26,6 +26,8 @@if (typeof document === 'undefined' || root.OpenVibeDockActor) return;const POLL_MS = 15000;++ const ACTOR_REV = { 'ov-window.js': 'd3d8f9e3e6d2', 'agent-sheet.js': '8d150490ad64' };++ const SCRIPT_BASE = document.currentScript && document.currentScript.src ? new URL('.', document.currentScript.src).href : '/shared/';const STATE = { working: 'Working', resting: 'Resting', paused: 'Paused', waiting: 'Waiting for you' };// The lines Actor sends as "the current step" when there is none: the state already says them.const NOT_A_STEP = /^(paused|resting\b.*)\.?$/i;@@@ -191,6 +193,26 @@// ── The parts, built once ────────────────────────────────────────────────────────────────────────────────const head = h('div', 'ovdac-head');const id = h('a', 'ovdac-id'); id.href = openBase + '/you'; id.target = '_blank'; id.rel = 'noopener';++ function loadScript(file, ready) {++ if (ready()) return Promise.resolve(true);++ return new Promise((resolve) => {++ const sid = `ovdac-${file}`;++ let script = d.getElementById(sid);++ if (!script) { script = d.createElement('script'); script.id = sid; script.src = SCRIPT_BASE + file + (ACTOR_REV[file] ? `?v=${ACTOR_REV[file]}` : ''); script.async = true; d.head.appendChild(script); }++ script.addEventListener('load', () => resolve(!!ready()), { once: true });++ script.addEventListener('error', () => resolve(false), { once: true });++ });++ }++ // The row opens its sheet (agent-sheet.js in the one window: now, asks, work, schedule, memory, computer); the++ // Actor's page is the sheet's "Open". A modified click, or a sheet that cannot load, goes to the page as before.++ id.addEventListener('click', async (e) => {++ if (!actor || !(actor.agent_id || actor.id) || e.metaKey || e.ctrlKey || e.shiftKey || e.button) return;++ e.preventDefault();++ const ok = (await Promise.all([loadScript('ov-window.js', () => !!root.OVWindow), loadScript('agent-sheet.js', () => !!root.OVAgentSheet)])).every(Boolean);++ if (!ok) { d.defaultView.open(id.href, '_blank', 'noopener'); return; }++ root.OVAgentSheet.open({ kind: 'actor', id: actor.agent_id || actor.id, name: actor.name || 'Your Actor', token: o.token,++ frameBase: o.frameBase, base: direct || null, from: id, openHref: openBase + '/you' });++ });const face = h('span', 'ovdac-face');const who = h('span', 'ovdac-who');const name = h('span', 'ovdac-name'); const nameText = h('span'); name.append(nameText, icon('go'));@@@ -58,7 +58,7 @@// 4.203.0 (ask 349, "a better composer system unified across all sites"): Vibe's box is the one composer too, for// drafts, "/" commands and your own, the marks, what sends and your settings; its picture keeps its own upload.const selfSrc = (typeof document !== 'undefined' && document.currentScript && document.currentScript.src) || '';-- const VIBE_REV = { 'ov-composer.js': '50bdd857445c' };++ const VIBE_REV = { 'ov-composer.js': '9b3472802903' };function joinComposer(form) {if (!form || form.ownerDocument !== document) return; // a popped-out Dock keeps its plain boxform.setAttribute('data-ovc-no-files', '');@@@ -89,7 +89,7 @@}// Each sibling's content hash (scripts/sibling-revs.js writes it), so a release gives each a new URL.-- const DOCK_REV = { 'dock-vibe.js': '061fd680f4f7', 'dock-code.js': '7a768982eff3', 'dock-mount.js': 'fb07dccadb8c', 'dock-runs.js': '4368f039df7c', 'ai-marks-client.js': 'f4a880a9f206', 'action-center.js': 'e40346ec1f16', 'dock-timeline.js': 'bffaed38fccf', 'dock-actor.js': '8ed09c416a48', 'dock-agent.js': '8787080a04d3', 'dock-timer.js': '460d68aa8321', 'dock-share.js': 'e83b2e27b3ef', 'dock-card.js': '2643c20b0016', 'dock-chat-edits.js': '21e125e3f1e9' };++ const DOCK_REV = { 'dock-vibe.js': 'ee110f2a0c1d', 'dock-code.js': '7a768982eff3', 'dock-mount.js': 'fb07dccadb8c', 'dock-runs.js': '4368f039df7c', 'ai-marks-client.js': 'f4a880a9f206', 'action-center.js': 'e40346ec1f16', 'dock-timeline.js': 'bffaed38fccf', 'dock-actor.js': 'f9aea671c7ad', 'dock-agent.js': '8787080a04d3', 'dock-timer.js': '460d68aa8321', 'dock-share.js': 'e83b2e27b3ef', 'dock-card.js': '2643c20b0016', 'dock-chat-edits.js': '21e125e3f1e9' };function load(src, test) {return new Promise((ok) => {if (test()) return ok(true);@@@ -58,6 +58,7 @@ const BROWSER = Object.freeze(['ov-composer.js', // shared composer'ov-combo.js', // shared field dropdown'ov-window.js', // shared dialog, sheet, and panel++ 'agent-sheet.js', // an agent's sheet in the one window (4.211.0)'ov-float.js', // the move engine behind a PiP or a panel: dock/float, tuck, size, place (4.208.0)'ov-icons.js', // ring icons (also required on the server as openvibe-shared/icons)'ov-home.js', // the menu's home screen and wall (search.js loads it for You and Feed)@@@ -607,7 +607,7 @@* serves its own pinned copy at /shared/ gets its own), else the network's copy.*/// The files loaded from beside this one, by content (scripts/sibling-revs.js): a release changes their URLs.-- const SIBLING_REV = { 'ov-mark.js': 'f34f731c3a94', 'ov-glyphs.js': '771efd0e3963', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': 'dd84a30bb646', 'ov-fx.js': '947820d69097', 'ov-labels.js': 'd40f7ca14bc4', 'ov-forms.js': '2eb9e2604c3e', 'ov-forms.css': '06208fe89eac', 'ov-share.js': 'd419508a797e', 'ov-share.css': 'd609a71f611a', 'search.js': '0dbfc679c2a2', 'dock.js': '0ec4ae476c63', 'presence.js': '5f195d41f8c5', 'panels.js': 'd90bfe987d81', 'surfaces.js': 'a94e76bd1200', 'notification-ui.js': '4b39796c5145', 'history.js': 'f9fb9e096c65', 'bottom.js': 'b9f708d04350', 'appbar.js': 'c21485176ae9', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': 'aaa688bf6fc7', 'sso-client.js': '52439265b980', 'overlay.js': 'c72c0e31505d', 'ov-home.js': 'cdacf9b1154b', 'nav-slots.js': '7b11e6916732', 'ov-wallet.js': 'b5a78ad67810', 'ov-wallet.css': 'be8daa24c3a8', 'purse.js': '749120bfd24b', 'ov-drop.js': '445689a5eb80', 'ov-tip.js': '1d918e7de69d', 'soft.js': '98ab9b8080b1' };++ const SIBLING_REV = { 'ov-mark.js': 'f34f731c3a94', 'ov-glyphs.js': '771efd0e3963', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': 'dd84a30bb646', 'ov-fx.js': '947820d69097', 'ov-labels.js': 'd40f7ca14bc4', 'ov-forms.js': '2eb9e2604c3e', 'ov-forms.css': '06208fe89eac', 'ov-share.js': 'd419508a797e', 'ov-share.css': 'd609a71f611a', 'search.js': '129aad6157d1', 'dock.js': '7782907c85d4', 'presence.js': '5f195d41f8c5', 'panels.js': 'd90bfe987d81', 'surfaces.js': 'a94e76bd1200', 'notification-ui.js': '3b6d2e2933de', 'history.js': 'f9fb9e096c65', 'bottom.js': 'b9f708d04350', 'appbar.js': 'c21485176ae9', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': 'aaa688bf6fc7', 'sso-client.js': '52439265b980', 'overlay.js': 'c72c0e31505d', 'ov-home.js': '0724f4913525', 'nav-slots.js': '7b11e6916732', 'ov-wallet.js': 'b5a78ad67810', 'ov-wallet.css': 'be8daa24c3a8', 'purse.js': '749120bfd24b', 'ov-drop.js': '445689a5eb80', 'ov-tip.js': '1d918e7de69d', 'soft.js': '98ab9b8080b1' };const siblingV = (name) => `${sibling(name)}${SIBLING_REV[name] ? `?v=${SIBLING_REV[name]}` : ''}`;// ov-tip.js loads at the first hover over a tooltip's owner.@@@ -205,6 +205,7 @@.openvibe-notif-dock { display: flex; flex-direction: column; min-height: 0; flex: 1; color: var(--text-primary, #ececf1); }.openvibe-notif-dock .openvibe-nv-head { display: none; }.openvibe-notif-dock .openvibe-nv-tools { padding-top: 10px; }++.openvibe-notif-dock.is-preview .openvibe-nv-tools, .openvibe-notif-dock.is-preview .openvibe-nv-foot, .openvibe-notif-dock.is-preview .openvibe-nv-head, .openvibe-notif-dock.is-preview .openvibe-nv-more { display: none; }.openvibe-notif-dock .openvibe-nv-foot { gap: 12px; }.openvibe-notif-dock .openvibe-nv-foot [data-role="status"] { margin-right: auto; }.ovn-foot-read { display: inline-flex; align-items: center; gap: 6px; min-height: 32px; padding: 0; border: 0; background: none; font: inherit; font-weight: 700; color: var(--text-secondary, #b6b6c0); cursor: pointer; }@@@ -1489,15 +1490,15 @@reload(v);return v;},-- /** Compact five-row preview for the one menu, expandable without leaving You. */++ /** The You tab's preview (4.211.0): the newest three, no tools; Show all opens the whole list in place. */renderMenuNotifications(container, onCount) {injectStyles();if (!container) return null;-- container.classList.add('openvibe-notif-dock');++ container.classList.add('openvibe-notif-dock', 'is-preview');const v = buildView(container, { mode: 'dock' });-- v.pageSize = 5; v.onCount = onCount;++ v.pageSize = 3; v.onCount = onCount;reload(v);-- return { showAll() { v.pageSize = PAGE_SIZE; reload(v); } };++ return { showAll() { container.classList.remove('is-preview'); v.pageSize = PAGE_SIZE; reload(v); } };},unmountDock(container) {const v = _views.get(container);@@@ -470,7 +470,9 @@ html[data-ov-motion=off] .ovc-file, html[data-ov-motion=off] .ovc-pop, html[dataif (text.maxLength > 0 && row) {count = el('span', 'ovc-count'); count.setAttribute('aria-live', 'polite');row.insertBefore(count, send && send.parentNode === row ? send : null);-- const paintCount = () => { const left = text.maxLength - text.value.length; count.hidden = left > Math.max(200, text.maxLength * 0.1); count.textContent = `${left} left`; count.classList.toggle('is-low', left < 20); };++ // The count shows only near the limit: the last 15% of a box, at most 200 and at least 20 characters (a 120-character++ // search box said "120 left" while empty, 2026-11-11).++ const paintCount = () => { const left = text.maxLength - text.value.length; count.hidden = left > Math.min(200, Math.max(20, Math.round(text.maxLength * 0.15))); count.textContent = `${left} left`; count.classList.toggle('is-low', left < 20); };text.addEventListener('input', paintCount); paintCount();}@@@ -183,8 +183,8 @@ html[data-ov-motion=reduced] .ovh.is-edit .ovh-grid:not(.ovh-recent) .ovh-ic { a.ovh-av img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; }.ovh-av::after { content: ""; position: absolute; right: -1px; bottom: -1px; width: 10px; height: 10px; border-radius: 50%; background: var(--success, #22c55e); box-shadow: 0 0 0 2px var(--bg-secondary, #141a24); }.ovh-who-txt { min-width: 0; display: grid; gap: 0; line-height: 1.25; }--.ovh-who-txt b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 15px; font-weight: 800; letter-spacing: -.01em; }--.ovh-who-txt small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; color: var(--text-secondary, #a8b3c4); }++.ovh-who-txt b { overflow-wrap: anywhere; font-size: 15px; font-weight: 800; letter-spacing: -.01em; }++.ovh-who-txt small { overflow-wrap: anywhere; font-size: 12px; color: var(--text-secondary, #a8b3c4); }.ovh-ib { flex: none; display: grid; place-items: center; width: 44px; height: 44px; padding: 0; border: 0; border-radius: 50%; cursor: pointer; text-decoration: none;background: color-mix(in srgb, var(--text-primary, #e8ecf3) 8%, transparent); color: var(--text-primary, #e8ecf3); transition: background .16s, color .16s, transform .16s cubic-bezier(.2,1.4,.4,1); }.ovh-ib svg { display: block; width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }@@@ -24,7 +24,7 @@// Motion off (html[data-ov-motion=off], theme-loader.js) is the only setting that skips an effect; Calm plays it.var reduce = document.documentElement.getAttribute('data-ov-motion') === 'off';var canCopy = !!(root.navigator && navigator.clipboard && navigator.clipboard.writeText);-- const THREAD_REV = { 'ov-composer.js': '50bdd857445c', 'ov-combo.js': '3e426e56fc75' };++ const THREAD_REV = { 'ov-composer.js': '9b3472802903', 'ov-combo.js': '3e426e56fc75' };var selfSrc = (document.currentScript && document.currentScript.src) || '';// 4.202.0: the one dropdown for a field (ov-combo.js: a repository, a person), wherever a page asks for it.function loadCombo() {@@@ -1,12 +1,12 @@{"name": "openvibe-shared",-- "version": "4.210.0",++ "version": "4.211.0","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-shared",-- "version": "4.210.0",++ "version": "4.211.0","license": "MIT","dependencies": {"clean-css": "^5.3.3",@@@ -1,6 +1,6 @@{"name": "openvibe-shared",-- "version": "4.210.0",++ "version": "4.211.0","description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site","main": "index.js","bin": {@@@ -144,6 +144,7 @@"./messages": "./messages.js","./ov-thread": "./ov-thread.js","./ov-window": "./ov-window.js",++ "./agent-sheet": "./agent-sheet.js","./ov-float": "./ov-float.js","./ov-combo": "./ov-combo.js","./ov-composer": "./ov-composer.js",@@@ -27,7 +27,7 @@ const FILES = ['ov-mark.js', 'ov-glyphs.js', 'ov-brand.woff2', 'ov-look.js', 'ov// The files dock.js and notification-ui.js load from beside themselves: their own tables, in their own files.const OWN = { 'dock.js': { name: 'DOCK_REV', files: ['dock-vibe.js', 'dock-code.js', 'dock-mount.js', 'dock-runs.js', 'ai-marks-client.js', 'action-center.js', 'dock-timeline.js', 'dock-actor.js', 'dock-agent.js', 'dock-timer.js', 'dock-share.js', 'dock-card.js', 'dock-chat-edits.js'] },'notification-ui.js': { name: 'NOTIFY_REV', files: ['notification-live.js'] },-- 'footer.js': { name: 'FOOTER_REV', files: ['shipped.js'] }, 'ov-thread.js': { name: 'THREAD_REV', files: ['ov-composer.js', 'ov-combo.js'] }, 'ov-composer.js': { name: 'COMPOSER_REV', files: ['ov-window.js'] }, 'ov-user.js': { name: 'USER_REV', files: ['ov-window.js'] }, 'ov-wallet.js': { name: 'WALLET_REV', files: ['ov-window.js'] }, 'ov-window.js': { name: 'WINDOW_REV', files: ['ov-float.js'] }, 'dock-vibe.js': { name: 'VIBE_REV', files: ['ov-composer.js'] }, 'boost.js': { name: 'BOOST_REV', files: ['route-transition.js'] } };++ 'footer.js': { name: 'FOOTER_REV', files: ['shipped.js'] }, 'ov-thread.js': { name: 'THREAD_REV', files: ['ov-composer.js', 'ov-combo.js'] }, 'ov-composer.js': { name: 'COMPOSER_REV', files: ['ov-window.js'] }, 'ov-user.js': { name: 'USER_REV', files: ['ov-window.js'] }, 'ov-wallet.js': { name: 'WALLET_REV', files: ['ov-window.js'] }, 'ov-window.js': { name: 'WINDOW_REV', files: ['ov-float.js'] }, 'dock-vibe.js': { name: 'VIBE_REV', files: ['ov-composer.js'] }, 'boost.js': { name: 'BOOST_REV', files: ['route-transition.js'] }, 'dock-actor.js': { name: 'ACTOR_REV', files: ['ov-window.js', 'agent-sheet.js'] } };const table = (name, files) => ` const ${name} = { ${files.map((f) => `'${f}': '${hashAsset(fs.readFileSync(path.join(ROOT, f)))}'`).join(', ')} };`;function writeOwn() {for (const [file, { name, files }] of Object.entries(OWN)) {@@@ -721,7 +721,7 @@head.append(title, unread, more);li.insertBefore(head, host);let compact = null;-- try { compact = N.renderMenuNotifications(host, (total) => { if (total > 5) { more.textContent = `Show all ${total}`; more.hidden = false; } }); } catch { host.textContent = 'Notifications are unavailable right now.'; }++ try { compact = N.renderMenuNotifications(host, (total) => { if (total > 3) { more.textContent = `Show all ${total}`; more.hidden = false; } }); } catch { host.textContent = 'Notifications are unavailable right now.'; }more.addEventListener('click', () => { if (compact) compact.showAll(); more.hidden = true; });listEl.appendChild(li); return;}@@@ -44,10 +44,23 @@ const page = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta nconst server = http.createServer((req, res) => {if (req.url === '/') { res.writeHead(200, { 'content-type': 'text/html' }); return res.end(page); }-- if (req.url === '/dock-actor.js') { res.writeHead(200, { 'content-type': 'text/javascript' }); return res.end(fs.readFileSync(path.join(ROOT, 'dock-actor.js'))); }++ const file = req.url.split('?')[0];++ if (['/dock-actor.js', '/ov-window.js', '/agent-sheet.js'].includes(file)) { res.writeHead(200, { 'content-type': 'text/javascript' }); return res.end(fs.readFileSync(path.join(ROOT, file.slice(1)))); }let body = '';req.on('data', (c) => { body += c; });req.on('end', () => {++ if (req.url.startsWith('/api/v1/me/agents/')) {++ calls.push({ method: req.method, url: req.url, auth: req.headers.authorization || '', cookie: req.headers.cookie || '', body: null });++ res.writeHead(200, { 'content-type': 'application/json' });++ return res.end(JSON.stringify({++ agent: { id: 'act_1', name: 'Juniper', face: null, state: 'working', instructions: 'Find remote support jobs <b>now</b>', voice: null },++ now: { id: 'w1', task: 'Reading the third job post', state: 'running', cost_usd: 0.42 },++ work: [{ id: 'w0', task: 'Saved two posts', state: 'done', cost_usd: 0.1 }],++ asks: [{ id: 'ask_1', state: 'waiting', what: 'Apply to Northwind?', why: 'It matches remote and support.' }],++ asks_empty: 'Requests to approve will appear here.', computer: null,++ schedules: [{ schedule: 'off', next_run_at: null, last_run_at: null, last_result: null }], memory: [], activity: [],++ }));++ }if (req.url.startsWith('/api/v1/me/actor')) {calls.push({ method: req.method, url: req.url, auth: req.headers.authorization || '', cookie: req.headers.cookie || '', body: body ? JSON.parse(body) : null });if (mode === 'none') { res.writeHead(404, { 'content-type': 'application/json' }); return res.end('{"detail":"no actor"}'); }@@@ -176,6 +189,33 @@ const wait = async (pg, expr, ms = 4000) => {assert.ok(await wait(pz, `/Resumed/.test(document.querySelector('.ovdac-note').textContent)`), 'it says what happened');await pz.close();++ // The row opens the agent's sheet in the one window (agent-sheet.js): its state, the ask first, the parts as tabs++ // with their own empty words, the bearer and no cookie, the instructions as text.++ mode = 'actor';++ const sh = await h.openPage(browser, context, { width: 390, height: 800 });++ await sh.goto(`${base}/`, { maxMs: 5000 });++ await wait(sh, `!!document.querySelector('#box.on .ovdac-id')`);++ calls.length = 0;++ await sh.evaluate(`document.querySelector('.ovdac-id').click()`);++ assert.ok(await wait(sh, `!!document.querySelector('.ovw .ovas-tabs')`), 'the sheet opens');++ const sheet = await sh.evaluate(`(() => { const w = document.querySelector('.ovw'); const sel = w.querySelector('.ovas-tab[aria-selected=true]');++ const shown = [...w.querySelectorAll('.ovas-part')].find((p) => !p.hidden);++ return { name: w.querySelector('.ovas-name b').textContent, state: w.querySelector('.ovas-state').textContent,++ tabs: [...w.querySelectorAll('.ovas-tab')].map((t) => t.firstChild.textContent), first: sel.firstChild.textContent, count: (sel.querySelector('em') || {}).textContent,++ ask: shown.textContent, bold: w.querySelectorAll('.ovas-name b b').length, open: (w.querySelector('.ovas-open') || {}).href || '' }; })()`);++ assert.deepStrictEqual({ name: sheet.name, state: sheet.state, tabs: sheet.tabs, first: sheet.first, count: sheet.count },++ { name: 'Juniper', state: 'Working', tabs: ['Now', 'Asks', 'Work', 'Schedule', 'Memory', 'Computer'], first: 'Asks', count: '1' });++ assert.ok(sheet.ask.includes('Apply to Northwind?') && sheet.ask.includes('It matches remote and support.'), 'the ask with its reason');++ assert.strictEqual(sheet.bold, 0, 'the instructions are text');++ assert.ok(sheet.open.endsWith('/you'), 'the Actor\'s page is one tap further');++ const read = calls.find((c) => c.url.startsWith('/api/v1/me/agents/'));++ assert.ok(read && read.url === '/api/v1/me/agents/act_1?view=sheet' && read.auth === 'Bearer tok-1' && read.cookie === '', 'the sheet read carries the bearer, no cookie');++ await sh.evaluate(`document.querySelector('#ovas-t-memory').click()`);++ assert.strictEqual(await sh.evaluate(`document.querySelector('#ovas-p-memory').textContent`), 'Nothing remembered yet.', 'an empty part says so');++ await sh.evaluate(`document.querySelector('#ovas-t-now').click()`);++ assert.ok((await sh.evaluate(`document.querySelector('#ovas-p-now').textContent`)).includes('Reading the third job post'), 'Now shows the work in progress');++ await sh.close();++// No Actor: nothing shows.mode = 'none';const none = await h.openPage(browser, context, { width: 390, height: 800 });@@@ -184,7 +224,7 @@ const wait = async (pg, expr, ms = 4000) => {const empty = await none.evaluate(`(() => { const b = document.getElementById('box'); return { on: b.classList.contains('on'), kids: b.children.length, shown: getComputedStyle(b).display }; })()`);assert.deepStrictEqual(empty, { on: false, kids: 0, shown: 'none' }, 'a person with no Actor sees nothing');await none.close();-- console.log('dock-actor-chrome: the Actor block shows the step, asks with their buttons, tells, pauses and resumes with the bearer, keeps the typed line, text only, 44 px, nothing without an Actor ok');++ console.log('dock-actor-chrome: the Actor block shows the step, asks with their buttons, tells, pauses and resumes with the bearer, keeps the typed line, opens its sheet, text only, 44 px, nothing without an Actor ok');} finally {await browser.close();server.close();@@@ -13,7 +13,7 @@ const { brotli } = require('../scripts/size-report');const ROOT = path.join(__dirname, '..');const NAVBAR_PATH = path.join(ROOT, 'navbar.js');const ICONS = require('../nav-icons');--const NAVBAR_BROTLI_BUDGET = 49.9 * 1024; // 49.8 KB with the page view a guest sends at once (4.191.0, 2026-11-10); 49.7 KB with the links that step out when the bar is too narrow (4.190.0, 2026-11-10: it lays out the first paint, so it cannot be a sibling; a diet of the bar is owed); 49.3 KB with Sign In's drawn icon and the menu button's idle hop (4.191.0, 2026-11-10; a signed-out first paint, so it cannot be a sibling); 49.0 KB with the press on the site name that opens its pages (4.191.0, 2026-11-10: the list itself is ov-drop.js); 48.7 KB with the page-view beacon's signed-in marker (4.189.0, 2026-11-10); 48.6 KB with the wallet's cookie mode and its two content hashes (4.173.0, 2026-11-09); 48.2 KB with the same-site prerender rule, cross-site prefetch and the file-cache worker registration (4.165.0, 2026-11-09); 47.6 KB with the six glyph styles' refined paths and the sheets' surface loader (4.162.0, 2026-11-09); 47.1 KB with the shared pop motion every site's menus use and the account menu's Wallet and Profile (4.161.0, 2026-11-09); 46.6 KB with the keyboard watcher every site needs and the slots' loader (4.160.0, 2026-11-09; the slots themselves are a sibling, nav-slots.js); 45.0 KB with the floating cards' loader and link warming (4.152.0, 2026-11-08; the cards themselves are a sibling, overlay.js); 44.3 KB with the phone's menu button drawn as the orb (4.148.0, 2026-11-08); 43.5 KB with the menu warmed at idle and on intent (4.137.0); 43.2 KB with every sibling's ?v= content hash (4.137.0, 2026-11-07: the owner's menu was a day stale without them); 42.8 KB keeping the menu's Frame fields (4.132.0, 2026-11-07); 42.6 KB with Messages and the balances on every site (4.130.0, 2026-11-07); 42.1 KB with the featured link's phone button (4.119.0, 2026-11-06); 41.4 KB with the page-move code (4.70.0: busy(), the mark on plain links, prefetch rules on unboosted sites); was 41 KB with the Dock's loader (4.51.0)++const NAVBAR_BROTLI_BUDGET = 50 * 1024; // 49.9 KB with the content hashes of 4.211.0's siblings (2026-11-11: no new code in the bar; the agent sheet is a sibling); 49.8 KB with the page view a guest sends at once (4.191.0, 2026-11-10); 49.7 KB with the links that step out when the bar is too narrow (4.190.0, 2026-11-10: it lays out the first paint, so it cannot be a sibling; a diet of the bar is owed); 49.3 KB with Sign In's drawn icon and the menu button's idle hop (4.191.0, 2026-11-10; a signed-out first paint, so it cannot be a sibling); 49.0 KB with the press on the site name that opens its pages (4.191.0, 2026-11-10: the list itself is ov-drop.js); 48.7 KB with the page-view beacon's signed-in marker (4.189.0, 2026-11-10); 48.6 KB with the wallet's cookie mode and its two content hashes (4.173.0, 2026-11-09); 48.2 KB with the same-site prerender rule, cross-site prefetch and the file-cache worker registration (4.165.0, 2026-11-09); 47.6 KB with the six glyph styles' refined paths and the sheets' surface loader (4.162.0, 2026-11-09); 47.1 KB with the shared pop motion every site's menus use and the account menu's Wallet and Profile (4.161.0, 2026-11-09); 46.6 KB with the keyboard watcher every site needs and the slots' loader (4.160.0, 2026-11-09; the slots themselves are a sibling, nav-slots.js); 45.0 KB with the floating cards' loader and link warming (4.152.0, 2026-11-08; the cards themselves are a sibling, overlay.js); 44.3 KB with the phone's menu button drawn as the orb (4.148.0, 2026-11-08); 43.5 KB with the menu warmed at idle and on intent (4.137.0); 43.2 KB with every sibling's ?v= content hash (4.137.0, 2026-11-07: the owner's menu was a day stale without them); 42.8 KB keeping the menu's Frame fields (4.132.0, 2026-11-07); 42.6 KB with Messages and the balances on every site (4.130.0, 2026-11-07); 42.1 KB with the featured link's phone button (4.119.0, 2026-11-06); 41.4 KB with the page-move code (4.70.0: busy(), the mark on plain links, prefetch rules on unboosted sites); was 41 KB with the Dock's loader (4.51.0)// ── size budget and generated block ──────────────────────────{@@@ -492,7 +492,8 @@ const created = (subject, over = {}) => ({// 25.5 → 25.7 KB on 2026-11-10 (4.187.0): new messages are counted on the bell (25.51 KB measured).// 25.7 → 25.75 KB on 2026-11-10 (4.201.0): the one menu's notifications (the count event, the five-row preview// with its count, per-view page sizes; 25.72 KB measured). It cannot be trimmed: every line feeds the menu.-- assert.ok(size('notification-ui.js') <= 25.75 * 1024, `notification-ui.js is ${(size('notification-ui.js') / 1024).toFixed(2)} KB brotli, budget 25.75 KB`);++ // 25.75 → 25.9 KB on 2026-11-11 (4.211.0): the You tab's three-row preview (is-preview, its rule, Show all).++ assert.ok(size('notification-ui.js') <= 25.9 * 1024, `notification-ui.js is ${(size('notification-ui.js') / 1024).toFixed(2)} KB brotli, budget 25.9 KB`);console.log('notification live: all checks passed');})().catch((err) => { console.error(err); process.exit(1); });@@@ -190,8 +190,10 @@ const server = http.createServer((req, res) => {f = await page.evaluate(`({ mine: [...document.querySelectorAll('.ovw .ovc-set-l b')].map((b) => b.textContent), box: document.querySelector('#c1 textarea').value })`);assert.deepEqual(f, { mine: ['/sig'], box: '' }, 'the sheet lists your commands; the /settings word left the box');-- // The count near the limit (600: it shows from 200 left).++ // The count near the limit (600: it shows from 90 left, the last 15%); far from it, nothing.await page.evaluate(`OVWindow.closeAll()`);++ await page.evaluate(`(() => { const t = document.querySelector('#c1 textarea'); t.value = 'x'.repeat(450); t.dispatchEvent(new Event('input', { bubbles: true })); })()`);++ assert.equal(await page.evaluate(`document.querySelector('#c1 .ovc-count').hidden`), true, '150 left of 600 is not near the limit');await page.evaluate(`(() => { const t = document.querySelector('#c1 textarea'); t.value = 'x'.repeat(590); t.dispatchEvent(new Event('input', { bubbles: true })); })()`);f = await page.evaluate(`({ shown: !document.querySelector('#c1 .ovc-count').hidden, text: document.querySelector('#c1 .ovc-count').textContent, low: document.querySelector('#c1 .ovc-count').classList.contains('is-low') })`);assert.deepEqual(f, { shown: true, text: '10 left', low: true });
@@ -1,5 +1,22 @@
@@ -1,5 +1,22 @@
# Changelog
# Changelog
+## 4.211.0 — 2026-11-11
+
+- **The menu's You tab leads with you (ask 158b, "the You tab still sucks").** Its notifications are a preview of the
+ newest three, with no search, filters, foot or Load more until "Show all N" opens the whole list in place; the
+ person's bar, their AI and the rest are a glance below instead of a thousand pixels down. The person's name and handle
+ wrap instead of being cut (rule names-are-never-cut).
+- **An agent's sheet (agent-sheet.js, new; the Dock's Actor row opens it).** One window (ov-window.js: a panel beside
+ the row on a computer, a sheet on a phone) with everything about one agent: its face, state and instructions, then
+ Now, Asks (first when it waits for you, with the count), Work, Schedule, Memory and Computer as tabs (arrow keys move
+ between them), each with its own words when empty; the mark's wait while it reads and Retry when a read fails. Data:
+ Contracts actor.agent-sheet@1 / codes.agent-sheet@1 through Network's relay (/api/frame/agents/:id/sheet,
+ /api/frame/codes-agents/:id/sheet) with the bearer, no cookies; all text, never markup.
+ `OVAgentSheet.open({ kind, id, name, token, frameBase, base, from, openHref })`. The row used to leave for the
+ Actor's page; that page is the sheet's Open now (a modified click still goes straight there).
+- **A composer's count shows only near its limit (ov-composer.js).** The last 15% of the box, at most 200 and at least
+ 20 characters: a 120-character search box said "120 left" while it was empty (Exchange's /models).
+
## 4.210.0 — 2026-11-11
## 4.210.0 — 2026-11-11
- **A page's title and its line wrap before they are cut (kit.css `.ovk-head`).** The title takes up to two lines and
- **A page's title and its line wrap before they are cut (kit.css `.ovk-head`).** The title takes up to two lines and
@@ -5,7 +5,7 @@
@@ -5,7 +5,7 @@
**Docs:** https://openvibe.services · **For AI:** https://openvibe.network/llms.txt
**Docs:** https://openvibe.services · **For AI:** https://openvibe.network/llms.txt
**Install and test:** `npm ci && npm test`
**Install and test:** `npm ci && npm test`
-**Status:** alpha, v4.210.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
+**Status:** alpha, v4.211.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
consumer installs a tagged release and none keeps a vendored copy
consumer installs a tagged release and none keeps a vendored copy
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
the tag it pins.
the tag it pins.
@@ -4,7 +4,7 @@
@@ -4,7 +4,7 @@
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
"package": "openvibe-shared",
"package": "openvibe-shared",
- "version": "4.210.0",
+ "version": "4.211.0",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"deployed": false,
"deployed": false,
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
new file mode 100644
new file mode 100644
@@ -0,0 +1,236 @@
@@ -0,0 +1,236 @@
+// ═══════════════════════════════════════════════════════════════
+// OpenVibe — an agent's sheet (openvibe-shared 4.211.0; Opus, 2026-11-11)
+//
+// One window (ov-window.js: a panel beside what opened it on a computer, a sheet on a phone) that says everything about
+// one agent: who it is and its state, what it is doing now, what it asks you, the work it did, when it runs, what it
+// remembers and its computer. The data is the agent's sheet (Contracts actor.agent-sheet@1 / codes.agent-sheet@1),
+// read through Network's same-origin relay with the person's token:
+// OVAgentSheet.open({ kind: 'actor' | 'codes', id, name, token, frameBase, base, from, openHref }) → { close() }
+// The parts are tabs (a segmented row; arrow keys move between them); a part with nothing in it says so in its own
+// words (asks_empty) rather than disappearing. While it reads, the window shows the mark's wait; a failed read says so
+// with Retry. Everything an agent or a person wrote is set as text, never markup.
+// ═══════════════════════════════════════════════════════════════
+(function (root) {
+ 'use strict';
+ if (typeof document === 'undefined' || root.OVAgentSheet) return;
+
+ const CSS = `.ovas { display: grid; gap: 12px; padding: 4px 2px 6px; }
+.ovas-who { display: flex; align-items: center; gap: 12px; min-width: 0; }
+.ovas-face { flex: none; width: 44px; height: 44px; border-radius: 50%; display: grid; place-items: center; overflow: hidden; font-weight: 800; font-size: 17px;
+ color: #fff; background: linear-gradient(135deg, var(--accent, #3b82f6), color-mix(in srgb, var(--accent, #3b82f6) 45%, #7c3aed)); box-shadow: 0 0 0 3px var(--ovas-ring, transparent); }
+.ovas-face img { width: 100%; height: 100%; object-fit: cover; }
+.ovas[data-state="working"] { --ovas-ring: color-mix(in srgb, #22c55e 55%, transparent); }
+.ovas[data-state="paused"] { --ovas-ring: color-mix(in srgb, #f59e0b 55%, transparent); }
+.ovas-name { display: grid; gap: 2px; min-width: 0; flex: 1; }
+.ovas-name b { font-size: 16px; overflow-wrap: anywhere; }
+.ovas-name small { color: var(--text-secondary, #a8b3c4); font-size: 12.5px; overflow-wrap: anywhere; }
+.ovas-state { display: inline-flex; align-items: center; gap: 6px; padding: 2px 9px; border-radius: 999px; font-size: 12px; font-weight: 650; width: fit-content;
+ background: color-mix(in srgb, var(--text-primary, #e6edf7) 8%, transparent); }
+.ovas-state::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ovas-dot, #94a3b8); }
+.ovas[data-state="working"] .ovas-state { --ovas-dot: #22c55e; }
+.ovas[data-state="paused"] .ovas-state { --ovas-dot: #f59e0b; }
+.ovas-open { flex: none; display: inline-flex; align-items: center; min-height: 36px; padding: 0 12px; border-radius: 10px; color: var(--accent-light, #60a5fa);
+ font-size: 13px; font-weight: 650; text-decoration: none; }
+.ovas-open:hover { background: color-mix(in srgb, var(--accent, #3b82f6) 12%, transparent); }
+.ovas-tabs { display: flex; gap: 4px; padding: 4px; border-radius: 999px; overflow-x: auto; scrollbar-width: none; background: color-mix(in srgb, var(--text-primary, #e6edf7) 6%, transparent); }
+.ovas-tabs::-webkit-scrollbar { display: none; }
+@media (max-width: 520px) { .ovas-tabs { -webkit-mask-image: linear-gradient(90deg, #000 85%, transparent); mask-image: linear-gradient(90deg, #000 85%, transparent); } }
+.ovas-tab { flex: none; display: inline-flex; align-items: center; gap: 6px; min-height: 34px; padding: 0 13px; border: 0; border-radius: 999px; background: none;
+ color: var(--text-secondary, #a8b3c4); font: inherit; font-size: 13px; cursor: pointer; }
+.ovas-tab[aria-selected="true"] { background: var(--bg-primary, #0b1220); color: var(--text-primary, #e6edf7); font-weight: 650; box-shadow: 0 1px 3px rgba(0, 0, 0, .25); }
+.ovas-tab em { font-style: normal; font-size: 11px; font-weight: 700; padding: 0 6px; border-radius: 999px; background: color-mix(in srgb, #f59e0b 22%, transparent); color: #fbbf24; }
+.ovas-tab:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 1px; }
+.ovas-part { display: grid; align-content: start; gap: 8px; min-height: 90px; }
+.ovas-part[hidden] { display: none; }
+.ovas-empty { margin: 0; padding: 18px 8px; text-align: center; color: var(--text-secondary, #a8b3c4); font-size: 13.5px; }
+.ovas-card { display: grid; gap: 4px; padding: 11px 13px; border-radius: 14px; border: 1px solid var(--border, rgba(255, 255, 255, .1));
+ background: color-mix(in srgb, var(--text-primary, #e6edf7) 3%, transparent); min-width: 0; }
+.ovas-card b { font-size: 14px; font-weight: 650; overflow-wrap: anywhere; }
+.ovas-card p { margin: 0; color: var(--text-secondary, #a8b3c4); font-size: 13px; overflow-wrap: anywhere; }
+.ovas-line { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; color: var(--text-muted, #7d8aa0); font-size: 12px; font-variant-numeric: tabular-nums; }
+.ovas-chip { display: inline-flex; align-items: center; padding: 1px 8px; border-radius: 999px; font-size: 11.5px; font-weight: 650;
+ background: color-mix(in srgb, var(--text-primary, #e6edf7) 8%, transparent); color: var(--text-primary, #e6edf7); }
+.ovas-chip.is-ok { background: color-mix(in srgb, #22c55e 18%, transparent); color: #4ade80; }
+.ovas-chip.is-bad { background: color-mix(in srgb, #ef4444 18%, transparent); color: #f87171; }
+.ovas-chip.is-run { background: color-mix(in srgb, var(--accent, #3b82f6) 18%, transparent); color: var(--accent-light, #60a5fa); }
+.ovas-card.is-now { border-color: color-mix(in srgb, #22c55e 45%, transparent); }
+.ovas-card.is-ask { border-color: color-mix(in srgb, #f59e0b 50%, transparent); background: color-mix(in srgb, #f59e0b 7%, transparent); }
+.ovas-go { justify-self: start; display: inline-flex; align-items: center; min-height: 40px; padding: 0 16px; border-radius: 12px; text-decoration: none; font-weight: 650; font-size: 14px;
+ color: #fff; background: var(--accent, #3b82f6); }
+.ovas-wait { display: grid; justify-items: center; gap: 10px; padding: 26px 8px; color: var(--text-secondary, #a8b3c4); font-size: 13.5px; }
+.ovas-retry { min-height: 40px; padding: 0 16px; border-radius: 12px; border: 1px solid var(--border, rgba(255, 255, 255, .14)); background: none; color: inherit; font: inherit; cursor: pointer; }
+@media (pointer: coarse) { .ovas-tab { min-height: 44px; } .ovas-open { min-height: 44px; } }`;
+
+ function styles(d) {
+ if (d.getElementById('ovas-css')) return;
+ const s = d.createElement('style');
+ s.id = 'ovas-css';
+ s.textContent = CSS;
+ d.head.appendChild(s);
+ }
+ const money = (n) => (Number(n) > 0 ? `$${Number(n).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: Number(n) < 0.1 ? 4 : 2 })}` : '$0');
+ function when(iso) {
+ const t = Date.parse(iso || '');
+ if (!Number.isFinite(t)) return '';
+ const s = (t - Date.now()) / 1000;
+ const a = Math.abs(s);
+ const unit = a < 3600 ? [Math.max(1, Math.round(a / 60)), 'min'] : a < 86400 ? [Math.round(a / 3600), 'h'] : [Math.round(a / 86400), a < 172800 ? 'day' : 'days'];
+ if (a < 60) return s > 0 ? 'in a moment' : 'just now';
+ return s > 0 ? `in ${unit[0]} ${unit[1]}` : `${unit[0]} ${unit[1]} ago`;
+ }
+ const tone = (state) => (/^(done|succeeded|ok|passed|completed)$/i.test(state) ? 'is-ok' : /^(failed|error|refused|stopped)$/i.test(state) ? 'is-bad' : /^(running|working|started)$/i.test(state) ? 'is-run' : '');
+ const STATE_WORD = { working: 'Working', idle: 'Resting', paused: 'Paused', ready: 'Ready' };
+
+ function open(opts = {}) {
+ const W = root.OVWindow;
+ if (!W) throw new Error('ov-window.js is not loaded');
+ const d = document;
+ styles(d);
+ const h = (tag, cls, text) => { const n = d.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = String(text); return n; };
+ const kind = opts.kind === 'codes' ? 'codes' : 'actor';
+ const frameBase = String(opts.frameBase || 'https://openvibe.network/api/frame').replace(/\/+$/, '');
+ const aid = encodeURIComponent(String(opts.id || ''));
+ // Through Network's relay on a site; `base` reaches the service itself (its own site, or a test).
+ const direct = opts.base ? String(opts.base).replace(/\/+$/, '') : '';
+ const url = direct ? (kind === 'codes' ? `${direct}/api/v1/me/agents/${aid}/sheet` : `${direct}/api/v1/me/agents/${aid}?view=sheet`)
+ : `${frameBase}/${kind === 'codes' ? 'codes-agents' : 'agents'}/${aid}/sheet`;
+ const body = h('div', 'ovas');
+ // The window's title names what it is; the agent's own name leads the sheet under it (said once).
+ const win = W.open({ id: `agent-sheet-${kind}`, title: kind === 'codes' ? 'Coding agent' : 'Your Actor', body, kind: 'panel', size: 'md', from: opts.from || null,
+ onClose: () => { gone = true; } });
+ let gone = false;
+
+ function wait() {
+ const box = h('div', 'ovas-wait');
+ const mark = h('span', 'ov-mark');
+ mark.setAttribute('data-state', 'busy'); mark.setAttribute('data-size', '34'); mark.setAttribute('aria-hidden', 'true');
+ box.append(mark, h('span', '', 'Reading your agent…'));
+ body.replaceChildren(box);
+ }
+ function failed(text) {
+ const box = h('div', 'ovas-wait');
+ const retry = h('button', 'ovas-retry', 'Retry'); retry.type = 'button';
+ retry.addEventListener('click', load);
+ box.append(h('span', '', text), retry);
+ body.replaceChildren(box);
+ }
+
+ function card(cls, title, line, more) {
+ const c = h('div', `ovas-card${cls ? ` ${cls}` : ''}`);
+ c.append(h('b', '', title));
+ if (more) c.append(h('p', '', more));
+ if (line && line.childNodes.length) c.append(line);
+ return c;
+ }
+ function workCard(w, isNow) {
+ const line = h('div', 'ovas-line');
+ if (w.state) line.append(h('span', `ovas-chip ${tone(w.state)}`, w.state));
+ if (Number.isFinite(Number(w.cost_usd))) line.append(h('span', '', money(w.cost_usd)));
+ if (w.updated_at || w.started_at || w.at) line.append(h('span', '', when(w.updated_at || w.started_at || w.at)));
+ const result = typeof w.result === 'string' ? w.result : w.result && (w.result.summary || w.result.text) ? (w.result.summary || w.result.text) : '';
+ return card(isNow ? 'is-now' : '', w.task || 'A task', line, result ? String(result).slice(0, 400) : '');
+ }
+
+ function render(sheet) {
+ const a = sheet.agent || {};
+ body.dataset.state = a.state || '';
+ const who = h('div', 'ovas-who');
+ const face = h('span', 'ovas-face');
+ if (a.face && /^https:\/\//.test(a.face)) { const img = h('img'); img.src = a.face; img.alt = ''; face.append(img); } else face.textContent = String(a.name || opts.name || 'A').trim().charAt(0).toUpperCase();
+ const name = h('span', 'ovas-name');
+ name.append(h('b', '', a.name || opts.name || 'Your agent'), h('span', 'ovas-state', STATE_WORD[a.state] || a.state || 'Ready'));
+ if (a.instructions) name.append(h('small', '', String(a.instructions).slice(0, 160)));
+ else if (a.kind) name.append(h('small', '', a.kind));
+ who.append(face, name);
+ if (opts.openHref) { const go = h('a', 'ovas-open', 'Open ↗'); go.href = opts.openHref; go.target = '_blank'; go.rel = 'noopener'; who.append(go); }
+
+ const asks = Array.isArray(sheet.asks) ? sheet.asks : [];
+ const work = Array.isArray(sheet.work) ? sheet.work : [];
+ const parts = [
+ ['now', 'Now', () => (sheet.now ? [workCard(sheet.now, true)] : [h('p', 'ovas-empty', a.state === 'paused' ? 'Paused. Nothing runs until you resume it.' : 'Nothing running right now.')])],
+ ['asks', 'Asks', () => (asks.length ? asks.map((x) => {
+ const line = h('div', 'ovas-line');
+ if (x.state) line.append(h('span', 'ovas-chip', x.state));
+ if (x.touch && typeof x.touch === 'string') line.append(h('span', '', x.touch));
+ return card('is-ask', x.what || 'A request', line, x.why || '');
+ }) : [h('p', 'ovas-empty', sheet.asks_empty || 'Nothing to approve.')]), asks.length],
+ ['work', 'Work', () => (work.length ? work.map((w) => workCard(w, false)) : [h('p', 'ovas-empty', 'No work yet.')])],
+ ['when', 'Schedule', () => {
+ const rows = (Array.isArray(sheet.schedules) ? sheet.schedules : []).filter((s) => s && s.schedule !== 'off');
+ if (!rows.length) return [h('p', 'ovas-empty', 'It runs when you ask, on no schedule.')];
+ return rows.map((s) => {
+ const line = h('div', 'ovas-line');
+ if (s.next_run_at) line.append(h('span', '', `Next ${when(s.next_run_at)}`));
+ if (s.last_run_at) line.append(h('span', '', `Last ${when(s.last_run_at)}`));
+ if (s.last_result) line.append(h('span', `ovas-chip ${tone(s.last_result)}`, s.last_result));
+ return card('', String(s.schedule || 'On a schedule'), line, '');
+ });
+ }],
+ ['memory', 'Memory', () => {
+ const rows = Array.isArray(sheet.memory) ? sheet.memory : [];
+ return rows.length ? rows.slice(0, 30).map((m) => { const line = h('div', 'ovas-line'); if (m.source) line.append(h('span', '', m.source)); return card('', String(m.text || '').slice(0, 300), line, ''); }) : [h('p', 'ovas-empty', 'Nothing remembered yet.')];
+ }],
+ ['computer', 'Computer', () => {
+ const c = sheet.computer;
+ if (!c) return [h('p', 'ovas-empty', 'No computer is attached to this agent.')];
+ const out = [card('', c.name || 'Its computer', null, c.state || c.status || '')];
+ const href = [c.url, c.open].find((u) => typeof u === 'string' && /^https:\/\//.test(u));
+ if (href) { const go = h('a', 'ovas-go', 'Watch its computer'); go.href = href; go.target = '_blank'; go.rel = 'noopener'; out.push(go); }
+ return out;
+ }],
+ ];
+ const tabs = h('div', 'ovas-tabs');
+ tabs.setAttribute('role', 'tablist');
+ tabs.setAttribute('aria-label', 'About this agent');
+ const panes = [];
+ const first = asks.length ? 'asks' : 'now';
+ parts.forEach(([id, label, build, count]) => {
+ const tab = h('button', 'ovas-tab', label);
+ tab.type = 'button';
+ tab.setAttribute('role', 'tab');
+ tab.id = `ovas-t-${id}`;
+ tab.setAttribute('aria-controls', `ovas-p-${id}`);
+ if (count) tab.append(h('em', '', count));
+ const pane = h('div', 'ovas-part');
+ pane.id = `ovas-p-${id}`;
+ pane.setAttribute('role', 'tabpanel');
+ pane.setAttribute('aria-labelledby', tab.id);
+ pane.append(...build());
+ const on = id === first;
+ tab.setAttribute('aria-selected', on ? 'true' : 'false');
+ tab.tabIndex = on ? 0 : -1;
+ pane.hidden = !on;
+ tab.addEventListener('click', () => select(id));
+ tabs.append(tab);
+ panes.push([id, tab, pane]);
+ });
+ function select(id) {
+ panes.forEach(([pid, tab, pane]) => { const on = pid === id; tab.setAttribute('aria-selected', on ? 'true' : 'false'); tab.tabIndex = on ? 0 : -1; pane.hidden = !on; });
+ }
+ tabs.addEventListener('keydown', (e) => {
+ if (e.key !== 'ArrowRight' && e.key !== 'ArrowLeft') return;
+ const i = panes.findIndex(([, tab]) => tab.getAttribute('aria-selected') === 'true');
+ const next = panes[(i + (e.key === 'ArrowRight' ? 1 : panes.length - 1)) % panes.length];
+ select(next[0]); next[1].focus(); e.preventDefault();
+ });
+ body.replaceChildren(who, tabs, ...panes.map(([, , pane]) => pane));
+ }
+
+ async function load() {
+ wait();
+ try {
+ const t = typeof opts.token === 'function' ? await opts.token() : opts.token;
+ if (!t) { failed('Sign in to see your agent.'); return; }
+ const r = await fetch(url, { headers: { accept: 'application/json', authorization: `Bearer ${t}` }, credentials: 'omit' });
+ const b = await r.json().catch(() => null);
+ if (gone) return;
+ if (!r.ok || !b) { failed(r.status === 404 ? 'This agent is not there any more.' : 'Its sheet could not be read just now.'); return; }
+ render(b.sheet || b);
+ } catch { if (!gone) failed('Its sheet could not be read just now.'); }
+ }
+ load();
+ return { close: () => win.close() };
+ }
+
+ root.OVAgentSheet = { open };
+})(typeof window !== 'undefined' ? window : globalThis);
@@ -26,6 +26,8 @@
@@ -26,6 +26,8 @@
if (typeof document === 'undefined' || root.OpenVibeDockActor) return;
if (typeof document === 'undefined' || root.OpenVibeDockActor) return;
const POLL_MS = 15000;
const POLL_MS = 15000;
+ const ACTOR_REV = { 'ov-window.js': 'd3d8f9e3e6d2', 'agent-sheet.js': '8d150490ad64' };
+ const SCRIPT_BASE = document.currentScript && document.currentScript.src ? new URL('.', document.currentScript.src).href : '/shared/';
const STATE = { working: 'Working', resting: 'Resting', paused: 'Paused', waiting: 'Waiting for you' };
const STATE = { working: 'Working', resting: 'Resting', paused: 'Paused', waiting: 'Waiting for you' };
// The lines Actor sends as "the current step" when there is none: the state already says them.
// The lines Actor sends as "the current step" when there is none: the state already says them.
const NOT_A_STEP = /^(paused|resting\b.*)\.?$/i;
const NOT_A_STEP = /^(paused|resting\b.*)\.?$/i;
@@ -191,6 +193,26 @@
@@ -191,6 +193,26 @@
// ── The parts, built once ────────────────────────────────────────────────────────────────────────────────
// ── The parts, built once ────────────────────────────────────────────────────────────────────────────────
const head = h('div', 'ovdac-head');
const head = h('div', 'ovdac-head');
const id = h('a', 'ovdac-id'); id.href = openBase + '/you'; id.target = '_blank'; id.rel = 'noopener';
const id = h('a', 'ovdac-id'); id.href = openBase + '/you'; id.target = '_blank'; id.rel = 'noopener';
+ function loadScript(file, ready) {
+ if (ready()) return Promise.resolve(true);
+ return new Promise((resolve) => {
+ const sid = `ovdac-${file}`;
+ let script = d.getElementById(sid);
+ if (!script) { script = d.createElement('script'); script.id = sid; script.src = SCRIPT_BASE + file + (ACTOR_REV[file] ? `?v=${ACTOR_REV[file]}` : ''); script.async = true; d.head.appendChild(script); }
+ script.addEventListener('load', () => resolve(!!ready()), { once: true });
+ script.addEventListener('error', () => resolve(false), { once: true });
+ });
+ }
+ // The row opens its sheet (agent-sheet.js in the one window: now, asks, work, schedule, memory, computer); the
+ // Actor's page is the sheet's "Open". A modified click, or a sheet that cannot load, goes to the page as before.
+ id.addEventListener('click', async (e) => {
+ if (!actor || !(actor.agent_id || actor.id) || e.metaKey || e.ctrlKey || e.shiftKey || e.button) return;
+ e.preventDefault();
+ const ok = (await Promise.all([loadScript('ov-window.js', () => !!root.OVWindow), loadScript('agent-sheet.js', () => !!root.OVAgentSheet)])).every(Boolean);
+ if (!ok) { d.defaultView.open(id.href, '_blank', 'noopener'); return; }
+ root.OVAgentSheet.open({ kind: 'actor', id: actor.agent_id || actor.id, name: actor.name || 'Your Actor', token: o.token,
+ frameBase: o.frameBase, base: direct || null, from: id, openHref: openBase + '/you' });
+ });
const face = h('span', 'ovdac-face');
const face = h('span', 'ovdac-face');
const who = h('span', 'ovdac-who');
const who = h('span', 'ovdac-who');
const name = h('span', 'ovdac-name'); const nameText = h('span'); name.append(nameText, icon('go'));
const name = h('span', 'ovdac-name'); const nameText = h('span'); name.append(nameText, icon('go'));
@@ -58,7 +58,7 @@
@@ -58,7 +58,7 @@
// 4.203.0 (ask 349, "a better composer system unified across all sites"): Vibe's box is the one composer too, for
// 4.203.0 (ask 349, "a better composer system unified across all sites"): Vibe's box is the one composer too, for
// drafts, "/" commands and your own, the marks, what sends and your settings; its picture keeps its own upload.
// drafts, "/" commands and your own, the marks, what sends and your settings; its picture keeps its own upload.
const selfSrc = (typeof document !== 'undefined' && document.currentScript && document.currentScript.src) || '';
const selfSrc = (typeof document !== 'undefined' && document.currentScript && document.currentScript.src) || '';
- const VIBE_REV = { 'ov-composer.js': '50bdd857445c' };
+ const VIBE_REV = { 'ov-composer.js': '9b3472802903' };
function joinComposer(form) {
function joinComposer(form) {
if (!form || form.ownerDocument !== document) return; // a popped-out Dock keeps its plain box
if (!form || form.ownerDocument !== document) return; // a popped-out Dock keeps its plain box
form.setAttribute('data-ovc-no-files', '');
form.setAttribute('data-ovc-no-files', '');
@@ -89,7 +89,7 @@
@@ -89,7 +89,7 @@
}
}
// Each sibling's content hash (scripts/sibling-revs.js writes it), so a release gives each a new URL.
// Each sibling's content hash (scripts/sibling-revs.js writes it), so a release gives each a new URL.
- const DOCK_REV = { 'dock-vibe.js': '061fd680f4f7', 'dock-code.js': '7a768982eff3', 'dock-mount.js': 'fb07dccadb8c', 'dock-runs.js': '4368f039df7c', 'ai-marks-client.js': 'f4a880a9f206', 'action-center.js': 'e40346ec1f16', 'dock-timeline.js': 'bffaed38fccf', 'dock-actor.js': '8ed09c416a48', 'dock-agent.js': '8787080a04d3', 'dock-timer.js': '460d68aa8321', 'dock-share.js': 'e83b2e27b3ef', 'dock-card.js': '2643c20b0016', 'dock-chat-edits.js': '21e125e3f1e9' };
+ const DOCK_REV = { 'dock-vibe.js': 'ee110f2a0c1d', 'dock-code.js': '7a768982eff3', 'dock-mount.js': 'fb07dccadb8c', 'dock-runs.js': '4368f039df7c', 'ai-marks-client.js': 'f4a880a9f206', 'action-center.js': 'e40346ec1f16', 'dock-timeline.js': 'bffaed38fccf', 'dock-actor.js': 'f9aea671c7ad', 'dock-agent.js': '8787080a04d3', 'dock-timer.js': '460d68aa8321', 'dock-share.js': 'e83b2e27b3ef', 'dock-card.js': '2643c20b0016', 'dock-chat-edits.js': '21e125e3f1e9' };
function load(src, test) {
function load(src, test) {
return new Promise((ok) => {
return new Promise((ok) => {
if (test()) return ok(true);
if (test()) return ok(true);
@@ -58,6 +58,7 @@ const BROWSER = Object.freeze([
@@ -58,6 +58,7 @@ const BROWSER = Object.freeze([
'ov-composer.js', // shared composer
'ov-composer.js', // shared composer
'ov-combo.js', // shared field dropdown
'ov-combo.js', // shared field dropdown
'ov-window.js', // shared dialog, sheet, and panel
'ov-window.js', // shared dialog, sheet, and panel
+ 'agent-sheet.js', // an agent's sheet in the one window (4.211.0)
'ov-float.js', // the move engine behind a PiP or a panel: dock/float, tuck, size, place (4.208.0)
'ov-float.js', // the move engine behind a PiP or a panel: dock/float, tuck, size, place (4.208.0)
'ov-icons.js', // ring icons (also required on the server as openvibe-shared/icons)
'ov-icons.js', // ring icons (also required on the server as openvibe-shared/icons)
'ov-home.js', // the menu's home screen and wall (search.js loads it for You and Feed)
'ov-home.js', // the menu's home screen and wall (search.js loads it for You and Feed)
@@ -607,7 +607,7 @@
@@ -607,7 +607,7 @@
* serves its own pinned copy at /shared/ gets its own), else the network's copy.
* serves its own pinned copy at /shared/ gets its own), else the network's copy.
*/
*/
// The files loaded from beside this one, by content (scripts/sibling-revs.js): a release changes their URLs.
// The files loaded from beside this one, by content (scripts/sibling-revs.js): a release changes their URLs.
- const SIBLING_REV = { 'ov-mark.js': 'f34f731c3a94', 'ov-glyphs.js': '771efd0e3963', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': 'dd84a30bb646', 'ov-fx.js': '947820d69097', 'ov-labels.js': 'd40f7ca14bc4', 'ov-forms.js': '2eb9e2604c3e', 'ov-forms.css': '06208fe89eac', 'ov-share.js': 'd419508a797e', 'ov-share.css': 'd609a71f611a', 'search.js': '0dbfc679c2a2', 'dock.js': '0ec4ae476c63', 'presence.js': '5f195d41f8c5', 'panels.js': 'd90bfe987d81', 'surfaces.js': 'a94e76bd1200', 'notification-ui.js': '4b39796c5145', 'history.js': 'f9fb9e096c65', 'bottom.js': 'b9f708d04350', 'appbar.js': 'c21485176ae9', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': 'aaa688bf6fc7', 'sso-client.js': '52439265b980', 'overlay.js': 'c72c0e31505d', 'ov-home.js': 'cdacf9b1154b', 'nav-slots.js': '7b11e6916732', 'ov-wallet.js': 'b5a78ad67810', 'ov-wallet.css': 'be8daa24c3a8', 'purse.js': '749120bfd24b', 'ov-drop.js': '445689a5eb80', 'ov-tip.js': '1d918e7de69d', 'soft.js': '98ab9b8080b1' };
+ const SIBLING_REV = { 'ov-mark.js': 'f34f731c3a94', 'ov-glyphs.js': '771efd0e3963', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': 'dd84a30bb646', 'ov-fx.js': '947820d69097', 'ov-labels.js': 'd40f7ca14bc4', 'ov-forms.js': '2eb9e2604c3e', 'ov-forms.css': '06208fe89eac', 'ov-share.js': 'd419508a797e', 'ov-share.css': 'd609a71f611a', 'search.js': '129aad6157d1', 'dock.js': '7782907c85d4', 'presence.js': '5f195d41f8c5', 'panels.js': 'd90bfe987d81', 'surfaces.js': 'a94e76bd1200', 'notification-ui.js': '3b6d2e2933de', 'history.js': 'f9fb9e096c65', 'bottom.js': 'b9f708d04350', 'appbar.js': 'c21485176ae9', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': 'aaa688bf6fc7', 'sso-client.js': '52439265b980', 'overlay.js': 'c72c0e31505d', 'ov-home.js': '0724f4913525', 'nav-slots.js': '7b11e6916732', 'ov-wallet.js': 'b5a78ad67810', 'ov-wallet.css': 'be8daa24c3a8', 'purse.js': '749120bfd24b', 'ov-drop.js': '445689a5eb80', 'ov-tip.js': '1d918e7de69d', 'soft.js': '98ab9b8080b1' };
const siblingV = (name) => `${sibling(name)}${SIBLING_REV[name] ? `?v=${SIBLING_REV[name]}` : ''}`;
const siblingV = (name) => `${sibling(name)}${SIBLING_REV[name] ? `?v=${SIBLING_REV[name]}` : ''}`;
// ov-tip.js loads at the first hover over a tooltip's owner.
// ov-tip.js loads at the first hover over a tooltip's owner.
@@ -205,6 +205,7 @@
@@ -205,6 +205,7 @@
.openvibe-notif-dock { display: flex; flex-direction: column; min-height: 0; flex: 1; color: var(--text-primary, #ececf1); }
.openvibe-notif-dock { display: flex; flex-direction: column; min-height: 0; flex: 1; color: var(--text-primary, #ececf1); }
.openvibe-notif-dock .openvibe-nv-head { display: none; }
.openvibe-notif-dock .openvibe-nv-head { display: none; }
.openvibe-notif-dock .openvibe-nv-tools { padding-top: 10px; }
.openvibe-notif-dock .openvibe-nv-tools { padding-top: 10px; }
+.openvibe-notif-dock.is-preview .openvibe-nv-tools, .openvibe-notif-dock.is-preview .openvibe-nv-foot, .openvibe-notif-dock.is-preview .openvibe-nv-head, .openvibe-notif-dock.is-preview .openvibe-nv-more { display: none; }
.openvibe-notif-dock .openvibe-nv-foot { gap: 12px; }
.openvibe-notif-dock .openvibe-nv-foot { gap: 12px; }
.openvibe-notif-dock .openvibe-nv-foot [data-role="status"] { margin-right: auto; }
.openvibe-notif-dock .openvibe-nv-foot [data-role="status"] { margin-right: auto; }
.ovn-foot-read { display: inline-flex; align-items: center; gap: 6px; min-height: 32px; padding: 0; border: 0; background: none; font: inherit; font-weight: 700; color: var(--text-secondary, #b6b6c0); cursor: pointer; }
.ovn-foot-read { display: inline-flex; align-items: center; gap: 6px; min-height: 32px; padding: 0; border: 0; background: none; font: inherit; font-weight: 700; color: var(--text-secondary, #b6b6c0); cursor: pointer; }
@@ -1489,15 +1490,15 @@
@@ -1489,15 +1490,15 @@
reload(v);
reload(v);
return v;
return v;
},
},
- /** Compact five-row preview for the one menu, expandable without leaving You. */
+ /** The You tab's preview (4.211.0): the newest three, no tools; Show all opens the whole list in place. */
renderMenuNotifications(container, onCount) {
renderMenuNotifications(container, onCount) {
injectStyles();
injectStyles();
if (!container) return null;
if (!container) return null;
- container.classList.add('openvibe-notif-dock');
+ container.classList.add('openvibe-notif-dock', 'is-preview');
const v = buildView(container, { mode: 'dock' });
const v = buildView(container, { mode: 'dock' });
- v.pageSize = 5; v.onCount = onCount;
+ v.pageSize = 3; v.onCount = onCount;
reload(v);
reload(v);
- return { showAll() { v.pageSize = PAGE_SIZE; reload(v); } };
+ return { showAll() { container.classList.remove('is-preview'); v.pageSize = PAGE_SIZE; reload(v); } };
},
},
unmountDock(container) {
unmountDock(container) {
const v = _views.get(container);
const v = _views.get(container);
@@ -470,7 +470,9 @@ html[data-ov-motion=off] .ovc-file, html[data-ov-motion=off] .ovc-pop, html[data
@@ -470,7 +470,9 @@ html[data-ov-motion=off] .ovc-file, html[data-ov-motion=off] .ovc-pop, html[data
if (text.maxLength > 0 && row) {
if (text.maxLength > 0 && row) {
count = el('span', 'ovc-count'); count.setAttribute('aria-live', 'polite');
count = el('span', 'ovc-count'); count.setAttribute('aria-live', 'polite');
row.insertBefore(count, send && send.parentNode === row ? send : null);
row.insertBefore(count, send && send.parentNode === row ? send : null);
- const paintCount = () => { const left = text.maxLength - text.value.length; count.hidden = left > Math.max(200, text.maxLength * 0.1); count.textContent = `${left} left`; count.classList.toggle('is-low', left < 20); };
+ // The count shows only near the limit: the last 15% of a box, at most 200 and at least 20 characters (a 120-character
+ // search box said "120 left" while empty, 2026-11-11).
+ const paintCount = () => { const left = text.maxLength - text.value.length; count.hidden = left > Math.min(200, Math.max(20, Math.round(text.maxLength * 0.15))); count.textContent = `${left} left`; count.classList.toggle('is-low', left < 20); };
text.addEventListener('input', paintCount); paintCount();
text.addEventListener('input', paintCount); paintCount();
}
}
@@ -183,8 +183,8 @@ html[data-ov-motion=reduced] .ovh.is-edit .ovh-grid:not(.ovh-recent) .ovh-ic { a
@@ -183,8 +183,8 @@ html[data-ov-motion=reduced] .ovh.is-edit .ovh-grid:not(.ovh-recent) .ovh-ic { a
.ovh-av img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; }
.ovh-av img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; }
.ovh-av::after { content: ""; position: absolute; right: -1px; bottom: -1px; width: 10px; height: 10px; border-radius: 50%; background: var(--success, #22c55e); box-shadow: 0 0 0 2px var(--bg-secondary, #141a24); }
.ovh-av::after { content: ""; position: absolute; right: -1px; bottom: -1px; width: 10px; height: 10px; border-radius: 50%; background: var(--success, #22c55e); box-shadow: 0 0 0 2px var(--bg-secondary, #141a24); }
.ovh-who-txt { min-width: 0; display: grid; gap: 0; line-height: 1.25; }
.ovh-who-txt { min-width: 0; display: grid; gap: 0; line-height: 1.25; }
-.ovh-who-txt b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 15px; font-weight: 800; letter-spacing: -.01em; }
-.ovh-who-txt small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; color: var(--text-secondary, #a8b3c4); }
+.ovh-who-txt b { overflow-wrap: anywhere; font-size: 15px; font-weight: 800; letter-spacing: -.01em; }
+.ovh-who-txt small { overflow-wrap: anywhere; font-size: 12px; color: var(--text-secondary, #a8b3c4); }
.ovh-ib { flex: none; display: grid; place-items: center; width: 44px; height: 44px; padding: 0; border: 0; border-radius: 50%; cursor: pointer; text-decoration: none;
.ovh-ib { flex: none; display: grid; place-items: center; width: 44px; height: 44px; padding: 0; border: 0; border-radius: 50%; cursor: pointer; text-decoration: none;
background: color-mix(in srgb, var(--text-primary, #e8ecf3) 8%, transparent); color: var(--text-primary, #e8ecf3); transition: background .16s, color .16s, transform .16s cubic-bezier(.2,1.4,.4,1); }
background: color-mix(in srgb, var(--text-primary, #e8ecf3) 8%, transparent); color: var(--text-primary, #e8ecf3); transition: background .16s, color .16s, transform .16s cubic-bezier(.2,1.4,.4,1); }
.ovh-ib svg { display: block; width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
.ovh-ib svg { display: block; width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
@@ -24,7 +24,7 @@
@@ -24,7 +24,7 @@
// Motion off (html[data-ov-motion=off], theme-loader.js) is the only setting that skips an effect; Calm plays it.
// Motion off (html[data-ov-motion=off], theme-loader.js) is the only setting that skips an effect; Calm plays it.
var reduce = document.documentElement.getAttribute('data-ov-motion') === 'off';
var reduce = document.documentElement.getAttribute('data-ov-motion') === 'off';
var canCopy = !!(root.navigator && navigator.clipboard && navigator.clipboard.writeText);
var canCopy = !!(root.navigator && navigator.clipboard && navigator.clipboard.writeText);
- const THREAD_REV = { 'ov-composer.js': '50bdd857445c', 'ov-combo.js': '3e426e56fc75' };
+ const THREAD_REV = { 'ov-composer.js': '9b3472802903', 'ov-combo.js': '3e426e56fc75' };
var selfSrc = (document.currentScript && document.currentScript.src) || '';
var selfSrc = (document.currentScript && document.currentScript.src) || '';
// 4.202.0: the one dropdown for a field (ov-combo.js: a repository, a person), wherever a page asks for it.
// 4.202.0: the one dropdown for a field (ov-combo.js: a repository, a person), wherever a page asks for it.
function loadCombo() {
function loadCombo() {
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.210.0",
+ "version": "4.211.0",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.210.0",
+ "version": "4.211.0",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"clean-css": "^5.3.3",
"clean-css": "^5.3.3",
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.210.0",
+ "version": "4.211.0",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"main": "index.js",
"bin": {
"bin": {
@@ -144,6 +144,7 @@
@@ -144,6 +144,7 @@
"./messages": "./messages.js",
"./messages": "./messages.js",
"./ov-thread": "./ov-thread.js",
"./ov-thread": "./ov-thread.js",
"./ov-window": "./ov-window.js",
"./ov-window": "./ov-window.js",
+ "./agent-sheet": "./agent-sheet.js",
"./ov-float": "./ov-float.js",
"./ov-float": "./ov-float.js",
"./ov-combo": "./ov-combo.js",
"./ov-combo": "./ov-combo.js",
"./ov-composer": "./ov-composer.js",
"./ov-composer": "./ov-composer.js",
@@ -27,7 +27,7 @@ const FILES = ['ov-mark.js', 'ov-glyphs.js', 'ov-brand.woff2', 'ov-look.js', 'ov
@@ -27,7 +27,7 @@ const FILES = ['ov-mark.js', 'ov-glyphs.js', 'ov-brand.woff2', 'ov-look.js', 'ov
// The files dock.js and notification-ui.js load from beside themselves: their own tables, in their own files.
// The files dock.js and notification-ui.js load from beside themselves: their own tables, in their own files.
const OWN = { 'dock.js': { name: 'DOCK_REV', files: ['dock-vibe.js', 'dock-code.js', 'dock-mount.js', 'dock-runs.js', 'ai-marks-client.js', 'action-center.js', 'dock-timeline.js', 'dock-actor.js', 'dock-agent.js', 'dock-timer.js', 'dock-share.js', 'dock-card.js', 'dock-chat-edits.js'] },
const OWN = { 'dock.js': { name: 'DOCK_REV', files: ['dock-vibe.js', 'dock-code.js', 'dock-mount.js', 'dock-runs.js', 'ai-marks-client.js', 'action-center.js', 'dock-timeline.js', 'dock-actor.js', 'dock-agent.js', 'dock-timer.js', 'dock-share.js', 'dock-card.js', 'dock-chat-edits.js'] },
'notification-ui.js': { name: 'NOTIFY_REV', files: ['notification-live.js'] },
'notification-ui.js': { name: 'NOTIFY_REV', files: ['notification-live.js'] },
- 'footer.js': { name: 'FOOTER_REV', files: ['shipped.js'] }, 'ov-thread.js': { name: 'THREAD_REV', files: ['ov-composer.js', 'ov-combo.js'] }, 'ov-composer.js': { name: 'COMPOSER_REV', files: ['ov-window.js'] }, 'ov-user.js': { name: 'USER_REV', files: ['ov-window.js'] }, 'ov-wallet.js': { name: 'WALLET_REV', files: ['ov-window.js'] }, 'ov-window.js': { name: 'WINDOW_REV', files: ['ov-float.js'] }, 'dock-vibe.js': { name: 'VIBE_REV', files: ['ov-composer.js'] }, 'boost.js': { name: 'BOOST_REV', files: ['route-transition.js'] } };
+ 'footer.js': { name: 'FOOTER_REV', files: ['shipped.js'] }, 'ov-thread.js': { name: 'THREAD_REV', files: ['ov-composer.js', 'ov-combo.js'] }, 'ov-composer.js': { name: 'COMPOSER_REV', files: ['ov-window.js'] }, 'ov-user.js': { name: 'USER_REV', files: ['ov-window.js'] }, 'ov-wallet.js': { name: 'WALLET_REV', files: ['ov-window.js'] }, 'ov-window.js': { name: 'WINDOW_REV', files: ['ov-float.js'] }, 'dock-vibe.js': { name: 'VIBE_REV', files: ['ov-composer.js'] }, 'boost.js': { name: 'BOOST_REV', files: ['route-transition.js'] }, 'dock-actor.js': { name: 'ACTOR_REV', files: ['ov-window.js', 'agent-sheet.js'] } };
const table = (name, files) => ` const ${name} = { ${files.map((f) => `'${f}': '${hashAsset(fs.readFileSync(path.join(ROOT, f)))}'`).join(', ')} };`;
const table = (name, files) => ` const ${name} = { ${files.map((f) => `'${f}': '${hashAsset(fs.readFileSync(path.join(ROOT, f)))}'`).join(', ')} };`;
function writeOwn() {
function writeOwn() {
for (const [file, { name, files }] of Object.entries(OWN)) {
for (const [file, { name, files }] of Object.entries(OWN)) {
@@ -721,7 +721,7 @@
@@ -721,7 +721,7 @@
head.append(title, unread, more);
head.append(title, unread, more);
li.insertBefore(head, host);
li.insertBefore(head, host);
let compact = null;
let compact = null;
- try { compact = N.renderMenuNotifications(host, (total) => { if (total > 5) { more.textContent = `Show all ${total}`; more.hidden = false; } }); } catch { host.textContent = 'Notifications are unavailable right now.'; }
+ try { compact = N.renderMenuNotifications(host, (total) => { if (total > 3) { more.textContent = `Show all ${total}`; more.hidden = false; } }); } catch { host.textContent = 'Notifications are unavailable right now.'; }
more.addEventListener('click', () => { if (compact) compact.showAll(); more.hidden = true; });
more.addEventListener('click', () => { if (compact) compact.showAll(); more.hidden = true; });
listEl.appendChild(li); return;
listEl.appendChild(li); return;
}
}
@@ -44,10 +44,23 @@ const page = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta n
@@ -44,10 +44,23 @@ const page = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta n
const server = http.createServer((req, res) => {
const server = http.createServer((req, res) => {
if (req.url === '/') { res.writeHead(200, { 'content-type': 'text/html' }); return res.end(page); }
if (req.url === '/') { res.writeHead(200, { 'content-type': 'text/html' }); return res.end(page); }
- if (req.url === '/dock-actor.js') { res.writeHead(200, { 'content-type': 'text/javascript' }); return res.end(fs.readFileSync(path.join(ROOT, 'dock-actor.js'))); }
+ const file = req.url.split('?')[0];
+ if (['/dock-actor.js', '/ov-window.js', '/agent-sheet.js'].includes(file)) { res.writeHead(200, { 'content-type': 'text/javascript' }); return res.end(fs.readFileSync(path.join(ROOT, file.slice(1)))); }
let body = '';
let body = '';
req.on('data', (c) => { body += c; });
req.on('data', (c) => { body += c; });
req.on('end', () => {
req.on('end', () => {
+ if (req.url.startsWith('/api/v1/me/agents/')) {
+ calls.push({ method: req.method, url: req.url, auth: req.headers.authorization || '', cookie: req.headers.cookie || '', body: null });
+ res.writeHead(200, { 'content-type': 'application/json' });
+ return res.end(JSON.stringify({
+ agent: { id: 'act_1', name: 'Juniper', face: null, state: 'working', instructions: 'Find remote support jobs <b>now</b>', voice: null },
+ now: { id: 'w1', task: 'Reading the third job post', state: 'running', cost_usd: 0.42 },
+ work: [{ id: 'w0', task: 'Saved two posts', state: 'done', cost_usd: 0.1 }],
+ asks: [{ id: 'ask_1', state: 'waiting', what: 'Apply to Northwind?', why: 'It matches remote and support.' }],
+ asks_empty: 'Requests to approve will appear here.', computer: null,
+ schedules: [{ schedule: 'off', next_run_at: null, last_run_at: null, last_result: null }], memory: [], activity: [],
+ }));
+ }
if (req.url.startsWith('/api/v1/me/actor')) {
if (req.url.startsWith('/api/v1/me/actor')) {
calls.push({ method: req.method, url: req.url, auth: req.headers.authorization || '', cookie: req.headers.cookie || '', body: body ? JSON.parse(body) : null });
calls.push({ method: req.method, url: req.url, auth: req.headers.authorization || '', cookie: req.headers.cookie || '', body: body ? JSON.parse(body) : null });
if (mode === 'none') { res.writeHead(404, { 'content-type': 'application/json' }); return res.end('{"detail":"no actor"}'); }
if (mode === 'none') { res.writeHead(404, { 'content-type': 'application/json' }); return res.end('{"detail":"no actor"}'); }
@@ -176,6 +189,33 @@ const wait = async (pg, expr, ms = 4000) => {
@@ -176,6 +189,33 @@ const wait = async (pg, expr, ms = 4000) => {
assert.ok(await wait(pz, `/Resumed/.test(document.querySelector('.ovdac-note').textContent)`), 'it says what happened');
assert.ok(await wait(pz, `/Resumed/.test(document.querySelector('.ovdac-note').textContent)`), 'it says what happened');
await pz.close();
await pz.close();
+ // The row opens the agent's sheet in the one window (agent-sheet.js): its state, the ask first, the parts as tabs
+ // with their own empty words, the bearer and no cookie, the instructions as text.
+ mode = 'actor';
+ const sh = await h.openPage(browser, context, { width: 390, height: 800 });
+ await sh.goto(`${base}/`, { maxMs: 5000 });
+ await wait(sh, `!!document.querySelector('#box.on .ovdac-id')`);
+ calls.length = 0;
+ await sh.evaluate(`document.querySelector('.ovdac-id').click()`);
+ assert.ok(await wait(sh, `!!document.querySelector('.ovw .ovas-tabs')`), 'the sheet opens');
+ const sheet = await sh.evaluate(`(() => { const w = document.querySelector('.ovw'); const sel = w.querySelector('.ovas-tab[aria-selected=true]');
+ const shown = [...w.querySelectorAll('.ovas-part')].find((p) => !p.hidden);
+ return { name: w.querySelector('.ovas-name b').textContent, state: w.querySelector('.ovas-state').textContent,
+ tabs: [...w.querySelectorAll('.ovas-tab')].map((t) => t.firstChild.textContent), first: sel.firstChild.textContent, count: (sel.querySelector('em') || {}).textContent,
+ ask: shown.textContent, bold: w.querySelectorAll('.ovas-name b b').length, open: (w.querySelector('.ovas-open') || {}).href || '' }; })()`);
+ assert.deepStrictEqual({ name: sheet.name, state: sheet.state, tabs: sheet.tabs, first: sheet.first, count: sheet.count },
+ { name: 'Juniper', state: 'Working', tabs: ['Now', 'Asks', 'Work', 'Schedule', 'Memory', 'Computer'], first: 'Asks', count: '1' });
+ assert.ok(sheet.ask.includes('Apply to Northwind?') && sheet.ask.includes('It matches remote and support.'), 'the ask with its reason');
+ assert.strictEqual(sheet.bold, 0, 'the instructions are text');
+ assert.ok(sheet.open.endsWith('/you'), 'the Actor\'s page is one tap further');
+ const read = calls.find((c) => c.url.startsWith('/api/v1/me/agents/'));
+ assert.ok(read && read.url === '/api/v1/me/agents/act_1?view=sheet' && read.auth === 'Bearer tok-1' && read.cookie === '', 'the sheet read carries the bearer, no cookie');
+ await sh.evaluate(`document.querySelector('#ovas-t-memory').click()`);
+ assert.strictEqual(await sh.evaluate(`document.querySelector('#ovas-p-memory').textContent`), 'Nothing remembered yet.', 'an empty part says so');
+ await sh.evaluate(`document.querySelector('#ovas-t-now').click()`);
+ assert.ok((await sh.evaluate(`document.querySelector('#ovas-p-now').textContent`)).includes('Reading the third job post'), 'Now shows the work in progress');
+ await sh.close();
+
// No Actor: nothing shows.
// No Actor: nothing shows.
mode = 'none';
mode = 'none';
const none = await h.openPage(browser, context, { width: 390, height: 800 });
const none = await h.openPage(browser, context, { width: 390, height: 800 });
@@ -184,7 +224,7 @@ const wait = async (pg, expr, ms = 4000) => {
@@ -184,7 +224,7 @@ const wait = async (pg, expr, ms = 4000) => {
const empty = await none.evaluate(`(() => { const b = document.getElementById('box'); return { on: b.classList.contains('on'), kids: b.children.length, shown: getComputedStyle(b).display }; })()`);
const empty = await none.evaluate(`(() => { const b = document.getElementById('box'); return { on: b.classList.contains('on'), kids: b.children.length, shown: getComputedStyle(b).display }; })()`);
assert.deepStrictEqual(empty, { on: false, kids: 0, shown: 'none' }, 'a person with no Actor sees nothing');
assert.deepStrictEqual(empty, { on: false, kids: 0, shown: 'none' }, 'a person with no Actor sees nothing');
await none.close();
await none.close();
- console.log('dock-actor-chrome: the Actor block shows the step, asks with their buttons, tells, pauses and resumes with the bearer, keeps the typed line, text only, 44 px, nothing without an Actor ok');
+ console.log('dock-actor-chrome: the Actor block shows the step, asks with their buttons, tells, pauses and resumes with the bearer, keeps the typed line, opens its sheet, text only, 44 px, nothing without an Actor ok');
} finally {
} finally {
await browser.close();
await browser.close();
server.close();
server.close();
@@ -13,7 +13,7 @@ const { brotli } = require('../scripts/size-report');
@@ -13,7 +13,7 @@ const { brotli } = require('../scripts/size-report');
const ROOT = path.join(__dirname, '..');
const ROOT = path.join(__dirname, '..');
const NAVBAR_PATH = path.join(ROOT, 'navbar.js');
const NAVBAR_PATH = path.join(ROOT, 'navbar.js');
const ICONS = require('../nav-icons');
const ICONS = require('../nav-icons');
-const NAVBAR_BROTLI_BUDGET = 49.9 * 1024; // 49.8 KB with the page view a guest sends at once (4.191.0, 2026-11-10); 49.7 KB with the links that step out when the bar is too narrow (4.190.0, 2026-11-10: it lays out the first paint, so it cannot be a sibling; a diet of the bar is owed); 49.3 KB with Sign In's drawn icon and the menu button's idle hop (4.191.0, 2026-11-10; a signed-out first paint, so it cannot be a sibling); 49.0 KB with the press on the site name that opens its pages (4.191.0, 2026-11-10: the list itself is ov-drop.js); 48.7 KB with the page-view beacon's signed-in marker (4.189.0, 2026-11-10); 48.6 KB with the wallet's cookie mode and its two content hashes (4.173.0, 2026-11-09); 48.2 KB with the same-site prerender rule, cross-site prefetch and the file-cache worker registration (4.165.0, 2026-11-09); 47.6 KB with the six glyph styles' refined paths and the sheets' surface loader (4.162.0, 2026-11-09); 47.1 KB with the shared pop motion every site's menus use and the account menu's Wallet and Profile (4.161.0, 2026-11-09); 46.6 KB with the keyboard watcher every site needs and the slots' loader (4.160.0, 2026-11-09; the slots themselves are a sibling, nav-slots.js); 45.0 KB with the floating cards' loader and link warming (4.152.0, 2026-11-08; the cards themselves are a sibling, overlay.js); 44.3 KB with the phone's menu button drawn as the orb (4.148.0, 2026-11-08); 43.5 KB with the menu warmed at idle and on intent (4.137.0); 43.2 KB with every sibling's ?v= content hash (4.137.0, 2026-11-07: the owner's menu was a day stale without them); 42.8 KB keeping the menu's Frame fields (4.132.0, 2026-11-07); 42.6 KB with Messages and the balances on every site (4.130.0, 2026-11-07); 42.1 KB with the featured link's phone button (4.119.0, 2026-11-06); 41.4 KB with the page-move code (4.70.0: busy(), the mark on plain links, prefetch rules on unboosted sites); was 41 KB with the Dock's loader (4.51.0)
+const NAVBAR_BROTLI_BUDGET = 50 * 1024; // 49.9 KB with the content hashes of 4.211.0's siblings (2026-11-11: no new code in the bar; the agent sheet is a sibling); 49.8 KB with the page view a guest sends at once (4.191.0, 2026-11-10); 49.7 KB with the links that step out when the bar is too narrow (4.190.0, 2026-11-10: it lays out the first paint, so it cannot be a sibling; a diet of the bar is owed); 49.3 KB with Sign In's drawn icon and the menu button's idle hop (4.191.0, 2026-11-10; a signed-out first paint, so it cannot be a sibling); 49.0 KB with the press on the site name that opens its pages (4.191.0, 2026-11-10: the list itself is ov-drop.js); 48.7 KB with the page-view beacon's signed-in marker (4.189.0, 2026-11-10); 48.6 KB with the wallet's cookie mode and its two content hashes (4.173.0, 2026-11-09); 48.2 KB with the same-site prerender rule, cross-site prefetch and the file-cache worker registration (4.165.0, 2026-11-09); 47.6 KB with the six glyph styles' refined paths and the sheets' surface loader (4.162.0, 2026-11-09); 47.1 KB with the shared pop motion every site's menus use and the account menu's Wallet and Profile (4.161.0, 2026-11-09); 46.6 KB with the keyboard watcher every site needs and the slots' loader (4.160.0, 2026-11-09; the slots themselves are a sibling, nav-slots.js); 45.0 KB with the floating cards' loader and link warming (4.152.0, 2026-11-08; the cards themselves are a sibling, overlay.js); 44.3 KB with the phone's menu button drawn as the orb (4.148.0, 2026-11-08); 43.5 KB with the menu warmed at idle and on intent (4.137.0); 43.2 KB with every sibling's ?v= content hash (4.137.0, 2026-11-07: the owner's menu was a day stale without them); 42.8 KB keeping the menu's Frame fields (4.132.0, 2026-11-07); 42.6 KB with Messages and the balances on every site (4.130.0, 2026-11-07); 42.1 KB with the featured link's phone button (4.119.0, 2026-11-06); 41.4 KB with the page-move code (4.70.0: busy(), the mark on plain links, prefetch rules on unboosted sites); was 41 KB with the Dock's loader (4.51.0)
// ── size budget and generated block ──────────────────────────
// ── size budget and generated block ──────────────────────────
{
{
@@ -492,7 +492,8 @@ const created = (subject, over = {}) => ({
@@ -492,7 +492,8 @@ const created = (subject, over = {}) => ({
// 25.5 → 25.7 KB on 2026-11-10 (4.187.0): new messages are counted on the bell (25.51 KB measured).
// 25.5 → 25.7 KB on 2026-11-10 (4.187.0): new messages are counted on the bell (25.51 KB measured).
// 25.7 → 25.75 KB on 2026-11-10 (4.201.0): the one menu's notifications (the count event, the five-row preview
// 25.7 → 25.75 KB on 2026-11-10 (4.201.0): the one menu's notifications (the count event, the five-row preview
// with its count, per-view page sizes; 25.72 KB measured). It cannot be trimmed: every line feeds the menu.
// with its count, per-view page sizes; 25.72 KB measured). It cannot be trimmed: every line feeds the menu.
- assert.ok(size('notification-ui.js') <= 25.75 * 1024, `notification-ui.js is ${(size('notification-ui.js') / 1024).toFixed(2)} KB brotli, budget 25.75 KB`);
+ // 25.75 → 25.9 KB on 2026-11-11 (4.211.0): the You tab's three-row preview (is-preview, its rule, Show all).
+ assert.ok(size('notification-ui.js') <= 25.9 * 1024, `notification-ui.js is ${(size('notification-ui.js') / 1024).toFixed(2)} KB brotli, budget 25.9 KB`);
console.log('notification live: all checks passed');
console.log('notification live: all checks passed');
})().catch((err) => { console.error(err); process.exit(1); });
})().catch((err) => { console.error(err); process.exit(1); });
@@ -190,8 +190,10 @@ const server = http.createServer((req, res) => {
@@ -190,8 +190,10 @@ const server = http.createServer((req, res) => {
f = await page.evaluate(`({ mine: [...document.querySelectorAll('.ovw .ovc-set-l b')].map((b) => b.textContent), box: document.querySelector('#c1 textarea').value })`);
f = await page.evaluate(`({ mine: [...document.querySelectorAll('.ovw .ovc-set-l b')].map((b) => b.textContent), box: document.querySelector('#c1 textarea').value })`);
assert.deepEqual(f, { mine: ['/sig'], box: '' }, 'the sheet lists your commands; the /settings word left the box');
assert.deepEqual(f, { mine: ['/sig'], box: '' }, 'the sheet lists your commands; the /settings word left the box');
- // The count near the limit (600: it shows from 200 left).
+ // The count near the limit (600: it shows from 90 left, the last 15%); far from it, nothing.
await page.evaluate(`OVWindow.closeAll()`);
await page.evaluate(`OVWindow.closeAll()`);
+ await page.evaluate(`(() => { const t = document.querySelector('#c1 textarea'); t.value = 'x'.repeat(450); t.dispatchEvent(new Event('input', { bubbles: true })); })()`);
+ assert.equal(await page.evaluate(`document.querySelector('#c1 .ovc-count').hidden`), true, '150 left of 600 is not near the limit');
await page.evaluate(`(() => { const t = document.querySelector('#c1 textarea'); t.value = 'x'.repeat(590); t.dispatchEvent(new Event('input', { bubbles: true })); })()`);
await page.evaluate(`(() => { const t = document.querySelector('#c1 textarea'); t.value = 'x'.repeat(590); t.dispatchEvent(new Event('input', { bubbles: true })); })()`);
f = await page.evaluate(`({ shown: !document.querySelector('#c1 .ovc-count').hidden, text: document.querySelector('#c1 .ovc-count').textContent, low: document.querySelector('#c1 .ovc-count').classList.contains('is-low') })`);
f = await page.evaluate(`({ shown: !document.querySelector('#c1 .ovc-count').hidden, text: document.querySelector('#c1 .ovc-count').textContent, low: document.querySelector('#c1 .ovc-count').classList.contains('is-low') })`);
assert.deepEqual(f, { shown: true, text: '10 left', low: true });
assert.deepEqual(f, { shown: true, text: '10 left', low: true });