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)

OP OpenVibers committed 1 days ago · 55edb3caa9 · 21 files, +346 −23

* 4.211.0: a composer's count shows only near its limit

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.211.0: an agent's sheet in the one window, opened from the Dock's Actor row

agent-sheet.js (new): the agent's face, state and instructions, then Now, Asks (first when it waits for you, with the
count), Work, Schedule, Memory and Computer as tabs, each with its own words when empty; the mark's wait while it
reads, Retry when a read fails. Contracts actor.agent-sheet@1 / codes.agent-sheet@1 through Network's relay with the
bearer and no cookies; all text. The Dock's Actor row opens it (the Actor's page is the sheet's Open; a modified click
still goes straight there). dock-actor.js loads ov-window.js and agent-sheet.js from beside itself by content hash
(ACTOR_REV, scripts/sibling-revs.js). dock-actor-chrome covers the sheet: it opens, Asks first with its count, the
parts' empty words, the bearer, text only. Navbar budget 50 KB brotli (hash table only).

* 4.211.0: the You tab leads with you; notifications are a three-row preview

The You tab opened on the whole notification list (search, six filters, five rows, Load more: about 1,000 px) before
the person's own bar. Its notifications are now the newest three with nothing else until "Show all N" opens the list in
place; the person's bar, their AI and the rest are a glance below. Their name and handle wrap instead of being cut.
Checked over production with these files: the bar moved from ~1,240 px down to ~640 px on a phone.

* notification-ui budget 25.9 KB (the You tab's preview); a shorter comment

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex a5d00c4..ed6c56e 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -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
ddiff --git a/README.md b/README.md
iindex 9f145d9..90e14ec 100644
---- a/README.md
++++ b/README.md
@@@ -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 deployed
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
the tag it pins.
ddiff --git a/STATUS.json b/STATUS.json
iindex b6abddc..323dc1a 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -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)",
ddiff --git a/agent-sheet.js b/agent-sheet.js
nnew file mode 100644
iindex 0000000..97fb6eb
---- /dev/null
++++ b/agent-sheet.js
@@@ -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);
ddiff --git a/dock-actor.js b/dock-actor.js
iindex 3df031a..82e9c2b 100644
---- a/dock-actor.js
++++ b/dock-actor.js
@@@ -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'));
ddiff --git a/dock-vibe.js b/dock-vibe.js
iindex d5a9ddc..c11b429 100644
---- a/dock-vibe.js
++++ b/dock-vibe.js
@@@ -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 box
form.setAttribute('data-ovc-no-files', '');
ddiff --git a/dock.js b/dock.js
iindex 9c11289..9d8c9fd 100644
---- a/dock.js
++++ b/dock.js
@@@ -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);
ddiff --git a/files.js b/files.js
iindex 3c5c1d2..c4eaae1 100644
---- a/files.js
++++ b/files.js
@@@ -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)
ddiff --git a/navbar.js b/navbar.js
iindex 19d7542..2cc6a5c 100644
---- a/navbar.js
++++ b/navbar.js
@@@ -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.
ddiff --git a/notification-ui.js b/notification-ui.js
iindex a7cfe26..70d8e27 100644
---- a/notification-ui.js
++++ b/notification-ui.js
@@@ -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);
ddiff --git a/ov-composer.js b/ov-composer.js
iindex e96e2b7..96e3abb 100644
---- a/ov-composer.js
++++ b/ov-composer.js
@@@ -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) {
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();
}
ddiff --git a/ov-home.js b/ov-home.js
iindex a6946d3..6f8e42d 100644
---- a/ov-home.js
++++ b/ov-home.js
@@@ -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; }
ddiff --git a/ov-thread.js b/ov-thread.js
iindex 7536097..d4494d2 100644
---- a/ov-thread.js
++++ b/ov-thread.js
@@@ -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() {
ddiff --git a/package-lock.json b/package-lock.json
iindex 3f2242b..289206d 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -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",
ddiff --git a/package.json b/package.json
iindex e4dca99..229cff1 100644
---- a/package.json
++++ b/package.json
@@@ -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",
ddiff --git a/scripts/sibling-revs.js b/scripts/sibling-revs.js
iindex 03e3099..1f77062 100644
---- a/scripts/sibling-revs.js
++++ b/scripts/sibling-revs.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)) {
ddiff --git a/search.js b/search.js
iindex a5e4baa..c0ce50e 100644
---- a/search.js
++++ b/search.js
@@@ -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;
}
ddiff --git a/test/dock-actor-chrome.test.js b/test/dock-actor-chrome.test.js
iindex 1fc5f65..3c61354 100644
---- a/test/dock-actor-chrome.test.js
++++ b/test/dock-actor-chrome.test.js
@@@ -44,10 +44,23 @@ const page = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta n
const 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();
ddiff --git a/test/nav-icons.test.js b/test/nav-icons.test.js
iindex 274a334..6222d8e 100644
---- a/test/nav-icons.test.js
++++ b/test/nav-icons.test.js
@@@ -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 ──────────────────────────
{
ddiff --git a/test/notification-live.test.js b/test/notification-live.test.js
iindex b2894d2..2bc3cb4 100644
---- a/test/notification-live.test.js
++++ b/test/notification-live.test.js
@@@ -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); });
ddiff --git a/test/ov-composer-chrome.test.js b/test/ov-composer-chrome.test.js
iindex 7a33801..21b0192 100644
---- a/test/ov-composer-chrome.test.js
++++ b/test/ov-composer-chrome.test.js
@@@ -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 });