openvibers/openvibe.shared Commits

The Dock's Agent panel reads the timeline again, and can open at a step (4.134.0) (#322)

OP OpenVibers committed 6 days ago · 5621eb2fc3 · 8 files, +75 −14

* The Dock's Agent panel reads the timeline again, and can open at a step

The navbar hands the Dock the person's token as a function (it may change when they
sign in again), but the panel used it as a string: every call went out as
"Bearer function…" and the panel said the agent was not reachable. The token is
now resolved when a call is made.

OpenVibeNavbar.openDock('agent', { task: 'tsk_…' }) marks that entry in the
timeline and brings it into view, so a site can send the person to the exact task
it started for them (Space's builder does).

* The release files name 4.134.0: STATUS.json and the README's status line

* The Dock's Agent panel: the step a site opened the Dock at is marked and brought into view

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex e9472e4..6c34ca7 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -1,5 +1,10 @@
# Changelog
++## 4.134.0 — 2026-11-07
++
++- **Fixed: the Dock's Agent panel could not read the timeline.** The navbar hands the Dock the person's token as a function (it may change when they sign in again); the panel used it as a string, so every call went out as `Bearer function…` and the panel said the agent was not reachable. The token is now resolved when a call is made.
++- The Dock can open at a step: `OpenVibeNavbar.openDock('agent', { task: 'tsk_…' })` marks that entry in the timeline and brings it into view, so a site can send the person to the exact task it started for them.
++
## 4.133.0 — 2026-11-07
- The menu's network cards draw an entry's picture when the site sent one (`frame.menu-card@1`'s optional item `image`, openvibe-contracts 0.320.0): a 64×36 rounded thumbnail, `object-fit: cover`, `loading="lazy"`, `decoding="async"`, empty alt, before the title and after the site's icon. The `src` is checked again in the menu (https only); a card or entry without a picture renders exactly as before.
ddiff --git a/README.md b/README.md
iindex e53492b..e0ed543 100644
---- a/README.md
++++ b/README.md
@@@ -2,7 +2,7 @@
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
--**Status:** alpha, v4.133.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
++**Status:** alpha, v4.134.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 f8c74be..f4f618b 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -4,13 +4,14 @@
"code": true,
"wave": "W2",
"package": "openvibe-shared",
-- "version": "4.133.0",
++ "version": "4.134.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)",
"contracts": "openvibe-contracts v0.312.0 (devDependency: the release manifest contract)",
"sdk": "openvibe-sdk v0.94.0",
"features": [
++ "the Dock's Agent panel reads the timeline again (the token is resolved when a call is made), and the Dock can open at a step: OpenVibeNavbar.openDock('agent', { task }) marks it and brings it into view (4.134.0)",
"the OpenVibe Frame: navbar, footer, panels, account switcher, notification UI with realtime",
"the launcher beside the brand lists every site of the network, in groups; Ask Actor and the Vibes chip in the account menu",
"themes, design tokens, icons and ring icons",
ddiff --git a/dock-timeline.js b/dock-timeline.js
iindex 2e5bdfa..e94c233 100644
---- a/dock-timeline.js
++++ b/dock-timeline.js
@@@ -1,11 +1,15 @@
// ═══════════════════════════════════════════════════════════════
// OpenVibe — the Dock's Agent panel (design 2026-11-07: one agent, one timeline behind the Dock), Shared 4.126.0
//
--// OpenVibeDockTimeline.mount(container, { token, base, active }) → { refresh(), destroy() }
++// OpenVibeDockTimeline.mount(container, { token, base, active, focus }) → { refresh(), destroy() }
//
++// `token` is the person's own Network token: a string, or the function the navbar hands over (which
++// resolves it when it is needed).
// `base` is OpenVibe.Actor's API: GET /api/v1/agent/timeline (the person's own steps, newest first, with the cursor
// and the mode and needs-you filters), POST /api/v1/agent/approvals/:product/:id/decide (an approval is answered in
// the mode that owns it) and POST /api/v1/agent/kill-switch (stop what is open and pause the agent).
++// `focus` is the id of the step to point at (the task id a site opened the Dock with): the entry that
++// carries it is marked and brought into view.
//
// The panel is what the person reads:
// Now one card for what the agent is doing this moment: the mode's word, the step in plain words, how long,
@@@ -90,6 +94,7 @@
'.ovdt-chips{display:flex;gap:8px;overflow-x:auto;margin:4px -12px 0;padding:6px 12px 10px;scrollbar-width:none;-webkit-overflow-scrolling:touch}',
'.ovdt-chips::-webkit-scrollbar{display:none}',
'.ovdt-fold{width:100%;text-align:left;min-height:44px;margin:0 0 8px;padding:0 14px;border-radius:12px;border:1px dashed var(--border,rgba(255,255,255,.16));background:transparent;color:var(--text-secondary,#a8b3c4);font:inherit;font-weight:650;cursor:pointer}',
++ '.ovdt-card.ovdt-focus{border-color:color-mix(in srgb,var(--accent,#3b82f6) 55%,transparent)}',
'.ovdt-empty{color:var(--text-muted,#93a0b4);padding:4px 2px 6px}',
'.ovdt-card.ovdt-empty{padding:12px 14px}',
'.ovdt-link{color:color-mix(in srgb,var(--accent,#3b82f6) 72%,var(--text-primary,#ececf1));font-weight:650;text-decoration:none}',
@@@ -103,12 +108,20 @@
const doc = container.ownerDocument || document;
styles(doc);
const base = String(opts.base || 'https://openvibe.actor').replace(/\/+$/, '');
-- const token = opts.token;
++ const rawToken = opts.token;
const active = typeof opts.active === 'function' ? opts.active : () => true;
++ const focus = typeof opts.focus === 'function' ? opts.focus : () => opts.focus || null;
const filters = new Set();
let timer = null;
let destroyed = false;
++ // The navbar hands the Dock a function (the token may change when the person signs in again),
++ // so it is resolved when a call is made, never read once at mount.
++ async function bearer() {
++ const t = typeof rawToken === 'function' ? await rawToken() : rawToken;
++ return t ? 'Bearer ' + t : null;
++ }
++
const wrap = el(doc, 'div', 'ovdt');
const nowHead = el(doc, 'h3', null, 'Now');
const nowBox = el(doc, 'div', 'ovdt-now');
@@@ -120,7 +133,8 @@
container.appendChild(wrap);
async function api(path, options = {}) {
-- const res = await fetch(base + path, Object.assign({ headers: { Accept: 'application/json', Authorization: 'Bearer ' + token } }, options));
++ const auth = await bearer();
++ const res = await fetch(base + path, Object.assign({ headers: { Accept: 'application/json', ...(auth ? { Authorization: auth } : {}) } }, options));
const body = await res.json().catch(() => ({}));
if (!res.ok) throw new Error((body && (body.detail || body.title)) || ('HTTP ' + res.status));
return body;
@@@ -165,7 +179,8 @@
stop.type = 'button';
stop.addEventListener('click', async () => {
stop.disabled = true;
-- try { await api('/api/v1/agent/kill-switch', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token }, body: JSON.stringify({ action: 'stop' }) }); } catch (err) { /* the panel refreshes either way */ }
++ const auth = await bearer();
++ try { await api('/api/v1/agent/kill-switch', { method: 'POST', headers: { 'Content-Type': 'application/json', ...(auth ? { Authorization: auth } : {}) }, body: JSON.stringify({ action: 'stop' }) }); } catch (err) { /* the panel refreshes either way */ }
refresh();
});
buttons.appendChild(stop);
@@@ -191,10 +206,11 @@
b.addEventListener('click', async () => {
b.disabled = true;
const approval = item.approval || {};
++ const auth = await bearer();
try {
await api('/api/v1/agent/approvals/' + encodeURIComponent(approval.product || item.product) + '/' + encodeURIComponent(approval.id) + '/decide', {
method: 'POST',
-- headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token },
++ headers: { 'Content-Type': 'application/json', ...(auth ? { Authorization: auth } : {}) },
body: JSON.stringify({ decision }),
});
} catch (err) { /* the panel refreshes either way */ }
@@@ -229,9 +245,26 @@
a.target = '_blank';
card.appendChild(a);
}
++ // The step a site opened the Dock at (its task id): it is marked, so the person sees where they landed.
++ const wanted = focusId();
++ if (wanted && (entry.id === wanted || entry.ref === wanted)) card.classList.add('ovdt-focus');
return card;
}
++ /** The step to point at: the id (a task id) a site opened the Dock with, if it is still there. */
++ function focusId() {
++ try { return String(focus() || '').trim() || null; } catch (err) { return null; }
++ }
++
++ /** Bring the focused step into view (the Dock's panel is short; the step may be below the fold). */
++ function scrollToFocus() {
++ const wanted = focusId();
++ if (!wanted) return;
++ const node = list.querySelector('.ovdt-focus');
++ if (!node || !node.scrollIntoView) return;
++ try { node.scrollIntoView({ block: 'nearest' }); } catch (err) { node.scrollIntoView(); }
++ }
++
function renderList(entries) {
list.textContent = '';
if (!entries.length) {
@@@ -322,6 +355,7 @@
renderNeeds(data.needs_you || []);
renderChips(data.counts || {});
renderList(data.entries || []);
++ scrollToFocus();
writeUrl();
}
ddiff --git a/dock.js b/dock.js
iindex f341ec0..75c19d4 100644
---- a/dock.js
++++ b/dock.js
@@@ -454,7 +454,8 @@
return;
}
if (id === 'chats') { chats.panel = panel; mounts.chats = mounts.chats || { refresh: null }; return state().chat && chats.open !== state().chat ? openChat(state().chat, false) : chats.open ? openChat(chats.open, false) : chatList(); }
-- if (mounts[id]) { if (mounts[id].refresh) mounts[id].refresh(); return; }
++ // The Agent panel reads the step.
++ if (mounts[id]) { if (mounts[id].refresh) mounts[id].refresh(); if (mounts[id + 'Timeline']) mounts[id + 'Timeline'].refresh(); return; }
mounts[id] = { refresh: null };
panel.innerHTML = '<div class="ovdk-wait"><span></span></div>';
if (id === 'agent') {
@@@ -465,7 +466,7 @@
// The Agent panel: Now, Needs you and the one timeline (design 2026-11-07). It is first in the tab, so on a
// phone Now and Needs you are on screen before anything else.
load('dock-timeline.js', () => !!(root.OpenVibeDockTimeline && root.OpenVibeDockTimeline.mount)).then((on) => {
-- if (on && !mounts[id + 'Timeline'] && panel.isConnected) mounts[id + 'Timeline'] = root.OpenVibeDockTimeline.mount(panel.querySelector('.ovdk-timeline'), { token: o.token, base: actorBase(), active: () => isOpen() && tab === 'agent' });
++ if (on && !mounts[id + 'Timeline'] && panel.isConnected) mounts[id + 'Timeline'] = root.OpenVibeDockTimeline.mount(panel.querySelector('.ovdk-timeline'), { token: o.token, base: actorBase(), active: () => isOpen() && tab === 'agent', focus: () => o.task || null });
});
load('dock-agent.js', () => !!(root.OpenVibeDockAgent && root.OpenVibeDockAgent.mount)).then((on) => {
if (on && !mounts[id + 'View'] && panel.isConnected) mounts[id + 'View'] = root.OpenVibeDockAgent.mount(panel.firstChild, { token: o.token, base: agentBase(), active: () => isOpen() && tab === 'agent' });
ddiff --git a/navbar.js b/navbar.js
iindex 34217a8..6e93030 100644
---- a/navbar.js
++++ b/navbar.js
@@@ -1235,8 +1235,11 @@
if (document.getElementById('ov-presence-loader')) return;
const sc = document.createElement('script'); sc.id = 'ov-presence-loader'; sc.src = sibling('presence.js'); sc.async = true; sc.onload = go; document.head.appendChild(sc);
}
-- /** Open the Dock on a tab: 'agent' | 'activity'. */
-- function openDock(tab) { loadDock(() => { root.OpenVibeDock.mount(dockOpts()); root.OpenVibeDock.open({ tab }); }); }
++ /**
++ * Open the Dock on a tab: 'agent' | 'activity'. opts may name the step to point at (opts.task): the
++ * Agent panel marks that entry and brings it into view, so a site can open the Dock at its own task.
++ */
++ function openDock(tab, opts) { loadDock(() => { root.OpenVibeDock.mount(dockOpts()); root.OpenVibeDock.open(Object.assign({ tab }, opts || {})); }); }
/** Custom sections for the user menu: [{ label, items: [{ label, icon, href, id, elId, hidden, danger, value, onClick }] }]. */
function sectionsHTML(sections) {
@@@ -2396,8 +2399,12 @@
* settles; a page that draws such a box later may call this). Returns how many it marked. */
focusScrollers() { try { return focusScrollers(); } catch { return 0; } },
-- /** Open the Dock (your agents and activity), on a tab: 'agent' | 'activity'. Signed-in people only. */
-- openDock(tab) { if (_config.token && _config.dock !== false) openDock(tab); },
++ /**
++ * Open the Dock (your agents and activity), on a tab: 'agent' | 'activity'. Signed-in people only.
++ * opts may name the step to point at (opts.task): the Agent panel marks that entry and brings it
++ * into view, so a site can open the Dock at its own task.
++ */
++ openDock(tab, opts) { if (_config.token && _config.dock !== false) openDock(tab, opts); },
/** Get the bell mount point for OpenVibeNotifications. */
getBellMount() {
ddiff --git a/package.json b/package.json
iindex 7f1bffd..f093418 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-shared",
-- "version": "4.133.0",
++ "version": "4.134.0",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"bin": {
ddiff --git a/test/dock-timeline.test.js b/test/dock-timeline.test.js
iindex 3ec9e99..a5fe9a0 100644
---- a/test/dock-timeline.test.js
++++ b/test/dock-timeline.test.js
@@@ -49,5 +49,18 @@ assert.ok(dock.includes("panel.querySelector('.ovdk-timeline')"), 'the panel has
assert.ok(dock.indexOf('ovdk-timeline') < dock.indexOf('ovdk-agent'), 'the panel comes before the desk view');
assert.ok(dock.includes('https://openvibe.actor'), 'the panel reads OpenVibe.Actor');
++// The person's token: the navbar hands the Dock a function, so the panel resolves it when it calls.
++assert.ok(/typeof rawToken === 'function' \? await rawToken\(\)/.test(panel), 'a token given as a function is resolved at the call');
++assert.ok(!panel.includes("'Bearer ' + token }"), 'the panel never sends a function as a bearer');
++assert.ok(panel.includes('async function bearer()'), 'the bearer is resolved once per call');
++
++// Opening the Dock at a task: the entry that carries it is marked and brought into view.
++assert.ok(panel.includes('ovdt-focus'), 'the focused entry is marked');
++assert.ok(panel.includes('scrollIntoView'), 'the focused entry is brought into view');
++assert.ok(panel.includes('entry.ref === wanted'), 'the step is found by the id the site opened the Dock with');
++assert.ok(dock.includes('focus: () => o.task || null'), 'the Dock tells the panel which step to point at');
++const navbar = fs.readFileSync(path.join(__dirname, '..', 'navbar.js'), 'utf8');
++assert.ok(/function openDock\(tab, opts\)/.test(navbar), 'the navbar opens the Dock with the step to point at');
++
console.log('dock timeline: now, needs you, the timeline, the folds and the filters');
console.log('all passed');