openvibers/openvibe.extensions Commits
0.8.1: the side panel opens on the page's Dock state, and follows it (#13)
Changes
@@@ -110,7 +110,8 @@ The **OpenVibe Dock** — the pill, the card with its Agents, Chats and Activitybrowser's side panel (Chrome's `side_panel`, Firefox's `sidebar_action`), on every site. It is thereleased OpenVibe.Shared Dock, bundled under `browser/shared/` because the extension's CSP allows noremote script, and it reads and writes the same state on the Network as the Dock on a site, so it--opens where the person left off.++opens where the person left off. It hands that state to the Dock as it opens (the page's tab, chat, agent and++unread counts) and re-reads it every 30 s, so a change on the page reaches the open side panel.It needs the person's own OpenVibe sign-in. Press **Sign in** in the panel or the settings page: theNetwork's account chooser opens, the extension is a public OAuth client (`companion`, PKCE S256, no@@@ -5,11 +5,11 @@"wave": "W18","plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)","adr": "OpenVibe.Contracts ADR-023 (first-party client surfaces)",-- "version": "0.8.0",++ "version": "0.8.1","surfaces": {"browser": {"name": "OpenVibe Companion",-- "version": "0.8.0",++ "version": "0.8.1","manifest": 3,"targets": ["chromium >= 114",@@@ -43,7 +43,7 @@"storeListing": "store/ (listing, privacy and permission answers, screenshot plan); support URL https://openvibe.coupons/companion","features": ["OpenVibe Dock one-click sign-in: public OAuth client companion (PKCE S256, no secret), read-only scopes dock/activity/chat.read, access token in storage.session, rotating refresh token in storage.local, Sign out revokes the family, the account page lists it under Signed-in apps",-- "the OpenVibe Dock in the side panel (Chrome side_panel, Firefox sidebar_action): the released OpenVibe.Shared Dock (v4.57.0) bundled under browser/shared/, reading and writing the same Network state as the Dock on a site, with the same tabs (Agents, Chats, Activity); approvals/sends are hidden with the read-only scopes"++ "the OpenVibe Dock in the side panel (Chrome side_panel, Firefox sidebar_action): the released OpenVibe.Shared Dock (v4.57.0) bundled under browser/shared/, reading and writing the same Network state as the Dock on a site, with the same tabs (Agents, Chats, Activity), and re-reading it every 30 s so the page, the pop-out and the panel agree; approvals/sends are hidden with the read-only scopes"],"pending": ["owner: create the Chrome Web Store and AMO developer listings and upload the store zips with store/'s answers and screenshots; the Chrome id is fixed by browser/manifest.json key",@@@ -64,10 +64,10 @@"watches a page on OpenVibe.Watch, and compares delivery apps on OpenVibe.Food","saves a review site's stated rating to OpenVibe.Reviews (read only when pressed, only on those sites)","an optional cpx_ install token, never an OpenVibe session; and the Dock one-click OAuth sign-in (scopes dock, activity, chat.read; access in storage.session, rotating refresh in storage.local; the 0.7.0 pasted token is still a fallback until a later release)",-- "the OpenVibe Dock in the side panel (Chrome side_panel, Firefox sidebar_action): the released OpenVibe.Shared Dock (v4.57.0) bundled under browser/shared/, reading and writing the same Network state as the Dock on a site, with the same tabs (Agents, Chats, Activity); approvals/sends are hidden with the read-only scopes",++ "the OpenVibe Dock in the side panel (Chrome side_panel, Firefox sidebar_action): the released OpenVibe.Shared Dock (v4.57.0) bundled under browser/shared/, reading and writing the same Network state as the Dock on a site, with the same tabs (Agents, Chats, Activity), and re-reading it every 30 s so the page, the pop-out and the panel agree; approvals/sends are hidden with the read-only scopes","store-ready: reproducible Chrome and Firefox store zips (npm run pack:stores), listing and privacy answers in store/, Mozilla web-ext lint in CI","kiosk companion (Firefox new-tab page and history bridge for Live's /kiosk), moved from OpenVibe.Live","hardware/: the Raspberry Pi streamer and controller scripts, moved from OpenVibe.Live"],-- "updated": "2026-10-30"++ "updated": "2026-11-06"}@@@ -2,7 +2,7 @@"manifest_version": 3,"name": "OpenVibe Companion","short_name": "OpenVibe",-- "version": "0.8.0",++ "version": "0.8.1","key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqVJohdlaysL2HgSsj2Fsd8PxPvXJkZHjEvNyGSYNSEpZBoIeZd7Cm7xznGVikDBbe7WseaNLt+HheX4WhN94jU/FrS2016AqQG9NTHoymJR8XSrPPDyhc85EUpICOFwqfFsSOykzaRyHZVcrvoepYwntwFjWdt1ul4VozwkqKovZoKUgCn398aNvZ6AH/J0xb0xWMiOiL9CvUWN/jK4wv0ShnJevpmyBBHL273CkQvfMS3VUBvBjq3seMU8BAHkE5RI3c2y/MRdjwYGsCoMU6MPYwt46PQZ0S8SPnFFozJndkpoqc/ue9oXhd77b12GKlJ1BPhHgn8BSx4IAvjeL1wIDAQAB","description": "Coupon codes for the shop you are on, tried in its coupon box when you ask; the same restaurant on every delivery app; a review site's rating saved to OpenVibe.Reviews; and one click to keep a rental listing on your OpenVibe.Rent board or watch a page. The OpenVibe Dock sits in the side panel, on every site. Reads the tab only when you open it.","homepage_url": "https://github.com/OpenVibers/OpenVibe.Extensions",@@@ -24,6 +24,7 @@const gateTitle = doc.getElementById('gate-title');const gateText = doc.getElementById('gate-text');const gateButton = doc.getElementById('gate-button');++ let current = null, mounted = false;function showGate(title, text, button) {gateTitle.textContent = title;@@@ -54,7 +55,7 @@return { mounted: false, reason: 'no-token' };}try {-- await opts.net.dock();++ current = await opts.net.dock();} catch (err) {showGate('OpenVibe.Network said no','That sign-in was not accepted. Sign in to OpenVibe.Network again.',@@@ -71,11 +72,25 @@chatBase: opts.config.CHAT_ORIGIN,sibling: opts.sibling,});-- opts.dock.open();++ // Hand the state we just read to the Dock as it opens, so its own first read cannot race it back to the++ // default tab. The page and the side panel then open on the same tab, chat and agent.++ opts.dock.open({ doc: current });++ mounted = true;gate.hidden = true;return { mounted: true, reason: null, mode: status.mode };}++ /** Re-read the Network's Dock state and hand it to the open Dock in place (the panel follows the page). */++ async function sync() {++ if (!mounted) return null;++ try {++ const next = await opts.net.dock();++ current = next;++ if (opts.dock.update) opts.dock.update(next);++ return next;++ } catch { return null; }++ }++async function signIn() {if (!opts.auth) return;gateButton.disabled = true;@@@ -95,7 +110,7 @@if (gateButton && typeof gateButton.addEventListener === 'function') {gateButton.addEventListener('click', function () { signIn(); });}-- return { start: start, signIn: signIn, showGate: showGate, messageFor: messageFor };++ return { start: start, sync: sync, signIn: signIn, showGate: showGate, messageFor: messageFor };}/** The page wiring: the real browser APIs and the real Dock. */@@@ -121,6 +136,13 @@openSettings: function () { ext.runtime.openOptionsPage(); },});panel.start();++ // The side panel stays open while the person moves between tabs and sites: re-read the Network's Dock state++ // again every 30 s, and when the browser shows the panel again, so the page, the pop-out and the panel agree.++ const syncT = setInterval(function () { panel.sync(); }, 30000);++ if (syncT && typeof syncT.unref === 'function') syncT.unref();++ root.document.addEventListener('visibilitychange', function () {++ if (!root.document.hidden) panel.sync();++ });return panel;}@@@ -1,6 +1,6 @@{"name": "openvibe-extensions",-- "version": "0.8.0",++ "version": "0.8.1","private": true,"description": "OpenVibe's first-party client surfaces (ADR-023). Today: the OpenVibe Companion, a Manifest V3 browser extension for Chromium and Firefox: coupon codes from OpenVibe.Coupons, and one click to keep a listing on the OpenVibe.Rent board or watch a page with OpenVibe.Watch.","scripts": {@@@ -21,24 +21,31 @@ function fakeDoc() {return { nodes, getElementById: (id) => nodes[id] || null };}--/** A stand-in for the Network's GET /api/v1/me/dock. */--async function startNetwork({ accept = TOKEN } = {}) {++/** A stand-in for the Network's GET /api/v1/me/dock. `setState` changes what the next GET answers. */++async function startNetwork({ accept = TOKEN, state, counts = { running: 1, waiting: 0, unread: 0 } } = {}) {const requests = [];++ let answer = {++ state: state || { open: false, tab: 'activity', pinned: false, agent: null, chat: null, place: { phone: { corner: 'bottom-right' }, desktop: { corner: 'bottom-right', width: null, height: null } }, updated_at: new Date().toISOString() },++ counts,++ };const server = http.createServer((req, res) => {requests.push({ url: req.url, auth: req.headers.authorization || null });const ok = req.headers.authorization === 'Bearer ' + accept;res.writeHead(ok ? 200 : 401, { 'content-type': 'application/json' });-- res.end(JSON.stringify(ok-- ? { state: { open: false, tab: 'activity', pinned: false, agent: null, chat: null, place: { phone: { corner: 'bottom-right' }, desktop: { corner: 'bottom-right', width: null, height: null } }, updated_at: new Date().toISOString() }, counts: { running: 1, waiting: 0, unread: 0 } }-- : { error: 'unauthorized' }));++ res.end(JSON.stringify(ok ? answer : { error: 'unauthorized' }));});await new Promise((ok) => server.listen(0, '127.0.0.1', ok));const origin = 'http://127.0.0.1:' + server.address().port;-- return { origin, requests, close: () => server.close() };++ return { origin, requests, state: () => answer, setState: (next) => { answer = next; }, close: () => server.close() };}function fakeDock() {-- const dock = { mounted: null, opened: 0, mount(opts) { dock.mounted = opts; }, open() { dock.opened++; } };++ const dock = {++ mounted: null, opened: 0, openedWith: null, updated: [],++ mount(opts) { dock.mounted = opts; },++ open(opts) { dock.opened++; dock.openedWith = opts || null; },++ update(next) { dock.updated.push(next); },++ };return dock;}@@@ -74,6 +81,32 @@ function fakeAuth({ mode = 'oauth', token = TOKEN } = {}) {assert.strictEqual(dock.mounted.sibling('dock.js'), 'chrome-extension://abc/shared/dock.js', 'the bundled Dock, from this extension');assert.strictEqual(doc.nodes.gate.hidden, true, 'the gate steps aside');assert.ok(net1.requests.some((r) => r.url === '/api/v1/me/dock' && r.auth === 'Bearer ' + TOKEN), 'the Network is asked with that sign-in');++ assert.deepStrictEqual(dock.openedWith.doc, net1.state(), 'the Dock opens on the state the Network keeps, before its own read could race it');++ assert.ok(!('tab' in dock.openedWith), 'the Dock itself chooses the tab from that state and what needs the person');++ });++++ await check('the panel follows the state the Network keeps', async () => {++ const first = {++ state: { open: true, tab: 'chats', pinned: true, agent: 'ags_01JAB2C3D4E5F6G7H8J9K0MNPQ', chat: '11', place: { phone: { corner: 'bottom-right' }, desktop: { corner: 'top-left', width: 500, height: 600 } }, updated_at: new Date().toISOString() },++ counts: { running: 0, waiting: 0, unread: 3 },++ };++ const net2 = await startNetwork({ state: first.state, counts: first.counts });++ try {++ const doc = fakeDoc();++ const dock = fakeDock();++ const net = createNetworkClient({ origin: net2.origin, fetch: (...a) => fetch(...a), getToken: async () => TOKEN });++ const panel = createPanel({ doc, config: { NETWORK_ORIGIN: 'https://openvibe.network', CHAT_ORIGIN: 'https://openvibe.chat' }, platform: { storage: { getNetworkToken: async () => null } }, auth: fakeAuth(), net, dock });++ await panel.start();++ assert.strictEqual(dock.openedWith.doc.state.tab, 'chats', 'the side panel opens on the page\'s tab');++ assert.deepStrictEqual(dock.openedWith.doc.counts, first.counts, 'and the unread counts the page sees');++ // The page changes the tab and an agent: the next read carries it to the open side panel.++ const next = { ...first, state: { ...first.state, tab: 'agent' }, counts: { running: 1, waiting: 0, unread: 3 } };++ net2.setState(next);++ assert.deepStrictEqual(await panel.sync(), next, 'the panel reads the same Network state again');++ assert.deepStrictEqual(dock.updated.at(-1), next, 'and hands it to the Dock in place, without a reload');++ } finally {++ net2.close();++ }});await check('without a sign-in the panel says what is missing', async () => {
@@ -110,7 +110,8 @@ The **OpenVibe Dock** — the pill, the card with its Agents, Chats and Activity
@@ -110,7 +110,8 @@ The **OpenVibe Dock** — the pill, the card with its Agents, Chats and Activity
browser's side panel (Chrome's `side_panel`, Firefox's `sidebar_action`), on every site. It is the
browser's side panel (Chrome's `side_panel`, Firefox's `sidebar_action`), on every site. It is the
released OpenVibe.Shared Dock, bundled under `browser/shared/` because the extension's CSP allows no
released OpenVibe.Shared Dock, bundled under `browser/shared/` because the extension's CSP allows no
remote script, and it reads and writes the same state on the Network as the Dock on a site, so it
remote script, and it reads and writes the same state on the Network as the Dock on a site, so it
-opens where the person left off.
+opens where the person left off. It hands that state to the Dock as it opens (the page's tab, chat, agent and
+unread counts) and re-reads it every 30 s, so a change on the page reaches the open side panel.
It needs the person's own OpenVibe sign-in. Press **Sign in** in the panel or the settings page: the
It needs the person's own OpenVibe sign-in. Press **Sign in** in the panel or the settings page: the
Network's account chooser opens, the extension is a public OAuth client (`companion`, PKCE S256, no
Network's account chooser opens, the extension is a public OAuth client (`companion`, PKCE S256, no
@@ -5,11 +5,11 @@
@@ -5,11 +5,11 @@
"wave": "W18",
"wave": "W18",
"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)",
"adr": "OpenVibe.Contracts ADR-023 (first-party client surfaces)",
"adr": "OpenVibe.Contracts ADR-023 (first-party client surfaces)",
- "version": "0.8.0",
+ "version": "0.8.1",
"surfaces": {
"surfaces": {
"browser": {
"browser": {
"name": "OpenVibe Companion",
"name": "OpenVibe Companion",
- "version": "0.8.0",
+ "version": "0.8.1",
"manifest": 3,
"manifest": 3,
"targets": [
"targets": [
"chromium >= 114",
"chromium >= 114",
@@ -43,7 +43,7 @@
@@ -43,7 +43,7 @@
"storeListing": "store/ (listing, privacy and permission answers, screenshot plan); support URL https://openvibe.coupons/companion",
"storeListing": "store/ (listing, privacy and permission answers, screenshot plan); support URL https://openvibe.coupons/companion",
"features": [
"features": [
"OpenVibe Dock one-click sign-in: public OAuth client companion (PKCE S256, no secret), read-only scopes dock/activity/chat.read, access token in storage.session, rotating refresh token in storage.local, Sign out revokes the family, the account page lists it under Signed-in apps",
"OpenVibe Dock one-click sign-in: public OAuth client companion (PKCE S256, no secret), read-only scopes dock/activity/chat.read, access token in storage.session, rotating refresh token in storage.local, Sign out revokes the family, the account page lists it under Signed-in apps",
- "the OpenVibe Dock in the side panel (Chrome side_panel, Firefox sidebar_action): the released OpenVibe.Shared Dock (v4.57.0) bundled under browser/shared/, reading and writing the same Network state as the Dock on a site, with the same tabs (Agents, Chats, Activity); approvals/sends are hidden with the read-only scopes"
+ "the OpenVibe Dock in the side panel (Chrome side_panel, Firefox sidebar_action): the released OpenVibe.Shared Dock (v4.57.0) bundled under browser/shared/, reading and writing the same Network state as the Dock on a site, with the same tabs (Agents, Chats, Activity), and re-reading it every 30 s so the page, the pop-out and the panel agree; approvals/sends are hidden with the read-only scopes"
],
],
"pending": [
"pending": [
"owner: create the Chrome Web Store and AMO developer listings and upload the store zips with store/'s answers and screenshots; the Chrome id is fixed by browser/manifest.json key",
"owner: create the Chrome Web Store and AMO developer listings and upload the store zips with store/'s answers and screenshots; the Chrome id is fixed by browser/manifest.json key",
@@ -64,10 +64,10 @@
@@ -64,10 +64,10 @@
"watches a page on OpenVibe.Watch, and compares delivery apps on OpenVibe.Food",
"watches a page on OpenVibe.Watch, and compares delivery apps on OpenVibe.Food",
"saves a review site's stated rating to OpenVibe.Reviews (read only when pressed, only on those sites)",
"saves a review site's stated rating to OpenVibe.Reviews (read only when pressed, only on those sites)",
"an optional cpx_ install token, never an OpenVibe session; and the Dock one-click OAuth sign-in (scopes dock, activity, chat.read; access in storage.session, rotating refresh in storage.local; the 0.7.0 pasted token is still a fallback until a later release)",
"an optional cpx_ install token, never an OpenVibe session; and the Dock one-click OAuth sign-in (scopes dock, activity, chat.read; access in storage.session, rotating refresh in storage.local; the 0.7.0 pasted token is still a fallback until a later release)",
- "the OpenVibe Dock in the side panel (Chrome side_panel, Firefox sidebar_action): the released OpenVibe.Shared Dock (v4.57.0) bundled under browser/shared/, reading and writing the same Network state as the Dock on a site, with the same tabs (Agents, Chats, Activity); approvals/sends are hidden with the read-only scopes",
+ "the OpenVibe Dock in the side panel (Chrome side_panel, Firefox sidebar_action): the released OpenVibe.Shared Dock (v4.57.0) bundled under browser/shared/, reading and writing the same Network state as the Dock on a site, with the same tabs (Agents, Chats, Activity), and re-reading it every 30 s so the page, the pop-out and the panel agree; approvals/sends are hidden with the read-only scopes",
"store-ready: reproducible Chrome and Firefox store zips (npm run pack:stores), listing and privacy answers in store/, Mozilla web-ext lint in CI",
"store-ready: reproducible Chrome and Firefox store zips (npm run pack:stores), listing and privacy answers in store/, Mozilla web-ext lint in CI",
"kiosk companion (Firefox new-tab page and history bridge for Live's /kiosk), moved from OpenVibe.Live",
"kiosk companion (Firefox new-tab page and history bridge for Live's /kiosk), moved from OpenVibe.Live",
"hardware/: the Raspberry Pi streamer and controller scripts, moved from OpenVibe.Live"
"hardware/: the Raspberry Pi streamer and controller scripts, moved from OpenVibe.Live"
],
],
- "updated": "2026-10-30"
+ "updated": "2026-11-06"
}
}
@@ -2,7 +2,7 @@
@@ -2,7 +2,7 @@
"manifest_version": 3,
"manifest_version": 3,
"name": "OpenVibe Companion",
"name": "OpenVibe Companion",
"short_name": "OpenVibe",
"short_name": "OpenVibe",
- "version": "0.8.0",
+ "version": "0.8.1",
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqVJohdlaysL2HgSsj2Fsd8PxPvXJkZHjEvNyGSYNSEpZBoIeZd7Cm7xznGVikDBbe7WseaNLt+HheX4WhN94jU/FrS2016AqQG9NTHoymJR8XSrPPDyhc85EUpICOFwqfFsSOykzaRyHZVcrvoepYwntwFjWdt1ul4VozwkqKovZoKUgCn398aNvZ6AH/J0xb0xWMiOiL9CvUWN/jK4wv0ShnJevpmyBBHL273CkQvfMS3VUBvBjq3seMU8BAHkE5RI3c2y/MRdjwYGsCoMU6MPYwt46PQZ0S8SPnFFozJndkpoqc/ue9oXhd77b12GKlJ1BPhHgn8BSx4IAvjeL1wIDAQAB",
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqVJohdlaysL2HgSsj2Fsd8PxPvXJkZHjEvNyGSYNSEpZBoIeZd7Cm7xznGVikDBbe7WseaNLt+HheX4WhN94jU/FrS2016AqQG9NTHoymJR8XSrPPDyhc85EUpICOFwqfFsSOykzaRyHZVcrvoepYwntwFjWdt1ul4VozwkqKovZoKUgCn398aNvZ6AH/J0xb0xWMiOiL9CvUWN/jK4wv0ShnJevpmyBBHL273CkQvfMS3VUBvBjq3seMU8BAHkE5RI3c2y/MRdjwYGsCoMU6MPYwt46PQZ0S8SPnFFozJndkpoqc/ue9oXhd77b12GKlJ1BPhHgn8BSx4IAvjeL1wIDAQAB",
"description": "Coupon codes for the shop you are on, tried in its coupon box when you ask; the same restaurant on every delivery app; a review site's rating saved to OpenVibe.Reviews; and one click to keep a rental listing on your OpenVibe.Rent board or watch a page. The OpenVibe Dock sits in the side panel, on every site. Reads the tab only when you open it.",
"description": "Coupon codes for the shop you are on, tried in its coupon box when you ask; the same restaurant on every delivery app; a review site's rating saved to OpenVibe.Reviews; and one click to keep a rental listing on your OpenVibe.Rent board or watch a page. The OpenVibe Dock sits in the side panel, on every site. Reads the tab only when you open it.",
"homepage_url": "https://github.com/OpenVibers/OpenVibe.Extensions",
"homepage_url": "https://github.com/OpenVibers/OpenVibe.Extensions",
@@ -24,6 +24,7 @@
@@ -24,6 +24,7 @@
const gateTitle = doc.getElementById('gate-title');
const gateTitle = doc.getElementById('gate-title');
const gateText = doc.getElementById('gate-text');
const gateText = doc.getElementById('gate-text');
const gateButton = doc.getElementById('gate-button');
const gateButton = doc.getElementById('gate-button');
+ let current = null, mounted = false;
function showGate(title, text, button) {
function showGate(title, text, button) {
gateTitle.textContent = title;
gateTitle.textContent = title;
@@ -54,7 +55,7 @@
@@ -54,7 +55,7 @@
return { mounted: false, reason: 'no-token' };
return { mounted: false, reason: 'no-token' };
}
}
try {
try {
- await opts.net.dock();
+ current = await opts.net.dock();
} catch (err) {
} catch (err) {
showGate('OpenVibe.Network said no',
showGate('OpenVibe.Network said no',
'That sign-in was not accepted. Sign in to OpenVibe.Network again.',
'That sign-in was not accepted. Sign in to OpenVibe.Network again.',
@@ -71,11 +72,25 @@
@@ -71,11 +72,25 @@
chatBase: opts.config.CHAT_ORIGIN,
chatBase: opts.config.CHAT_ORIGIN,
sibling: opts.sibling,
sibling: opts.sibling,
});
});
- opts.dock.open();
+ // Hand the state we just read to the Dock as it opens, so its own first read cannot race it back to the
+ // default tab. The page and the side panel then open on the same tab, chat and agent.
+ opts.dock.open({ doc: current });
+ mounted = true;
gate.hidden = true;
gate.hidden = true;
return { mounted: true, reason: null, mode: status.mode };
return { mounted: true, reason: null, mode: status.mode };
}
}
+ /** Re-read the Network's Dock state and hand it to the open Dock in place (the panel follows the page). */
+ async function sync() {
+ if (!mounted) return null;
+ try {
+ const next = await opts.net.dock();
+ current = next;
+ if (opts.dock.update) opts.dock.update(next);
+ return next;
+ } catch { return null; }
+ }
+
async function signIn() {
async function signIn() {
if (!opts.auth) return;
if (!opts.auth) return;
gateButton.disabled = true;
gateButton.disabled = true;
@@ -95,7 +110,7 @@
@@ -95,7 +110,7 @@
if (gateButton && typeof gateButton.addEventListener === 'function') {
if (gateButton && typeof gateButton.addEventListener === 'function') {
gateButton.addEventListener('click', function () { signIn(); });
gateButton.addEventListener('click', function () { signIn(); });
}
}
- return { start: start, signIn: signIn, showGate: showGate, messageFor: messageFor };
+ return { start: start, sync: sync, signIn: signIn, showGate: showGate, messageFor: messageFor };
}
}
/** The page wiring: the real browser APIs and the real Dock. */
/** The page wiring: the real browser APIs and the real Dock. */
@@ -121,6 +136,13 @@
@@ -121,6 +136,13 @@
openSettings: function () { ext.runtime.openOptionsPage(); },
openSettings: function () { ext.runtime.openOptionsPage(); },
});
});
panel.start();
panel.start();
+ // The side panel stays open while the person moves between tabs and sites: re-read the Network's Dock state
+ // again every 30 s, and when the browser shows the panel again, so the page, the pop-out and the panel agree.
+ const syncT = setInterval(function () { panel.sync(); }, 30000);
+ if (syncT && typeof syncT.unref === 'function') syncT.unref();
+ root.document.addEventListener('visibilitychange', function () {
+ if (!root.document.hidden) panel.sync();
+ });
return panel;
return panel;
}
}
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-extensions",
"name": "openvibe-extensions",
- "version": "0.8.0",
+ "version": "0.8.1",
"private": true,
"private": true,
"description": "OpenVibe's first-party client surfaces (ADR-023). Today: the OpenVibe Companion, a Manifest V3 browser extension for Chromium and Firefox: coupon codes from OpenVibe.Coupons, and one click to keep a listing on the OpenVibe.Rent board or watch a page with OpenVibe.Watch.",
"description": "OpenVibe's first-party client surfaces (ADR-023). Today: the OpenVibe Companion, a Manifest V3 browser extension for Chromium and Firefox: coupon codes from OpenVibe.Coupons, and one click to keep a listing on the OpenVibe.Rent board or watch a page with OpenVibe.Watch.",
"scripts": {
"scripts": {
@@ -21,24 +21,31 @@ function fakeDoc() {
@@ -21,24 +21,31 @@ function fakeDoc() {
return { nodes, getElementById: (id) => nodes[id] || null };
return { nodes, getElementById: (id) => nodes[id] || null };
}
}
-/** A stand-in for the Network's GET /api/v1/me/dock. */
-async function startNetwork({ accept = TOKEN } = {}) {
+/** A stand-in for the Network's GET /api/v1/me/dock. `setState` changes what the next GET answers. */
+async function startNetwork({ accept = TOKEN, state, counts = { running: 1, waiting: 0, unread: 0 } } = {}) {
const requests = [];
const requests = [];
+ let answer = {
+ state: state || { open: false, tab: 'activity', pinned: false, agent: null, chat: null, place: { phone: { corner: 'bottom-right' }, desktop: { corner: 'bottom-right', width: null, height: null } }, updated_at: new Date().toISOString() },
+ counts,
+ };
const server = http.createServer((req, res) => {
const server = http.createServer((req, res) => {
requests.push({ url: req.url, auth: req.headers.authorization || null });
requests.push({ url: req.url, auth: req.headers.authorization || null });
const ok = req.headers.authorization === 'Bearer ' + accept;
const ok = req.headers.authorization === 'Bearer ' + accept;
res.writeHead(ok ? 200 : 401, { 'content-type': 'application/json' });
res.writeHead(ok ? 200 : 401, { 'content-type': 'application/json' });
- res.end(JSON.stringify(ok
- ? { state: { open: false, tab: 'activity', pinned: false, agent: null, chat: null, place: { phone: { corner: 'bottom-right' }, desktop: { corner: 'bottom-right', width: null, height: null } }, updated_at: new Date().toISOString() }, counts: { running: 1, waiting: 0, unread: 0 } }
- : { error: 'unauthorized' }));
+ res.end(JSON.stringify(ok ? answer : { error: 'unauthorized' }));
});
});
await new Promise((ok) => server.listen(0, '127.0.0.1', ok));
await new Promise((ok) => server.listen(0, '127.0.0.1', ok));
const origin = 'http://127.0.0.1:' + server.address().port;
const origin = 'http://127.0.0.1:' + server.address().port;
- return { origin, requests, close: () => server.close() };
+ return { origin, requests, state: () => answer, setState: (next) => { answer = next; }, close: () => server.close() };
}
}
function fakeDock() {
function fakeDock() {
- const dock = { mounted: null, opened: 0, mount(opts) { dock.mounted = opts; }, open() { dock.opened++; } };
+ const dock = {
+ mounted: null, opened: 0, openedWith: null, updated: [],
+ mount(opts) { dock.mounted = opts; },
+ open(opts) { dock.opened++; dock.openedWith = opts || null; },
+ update(next) { dock.updated.push(next); },
+ };
return dock;
return dock;
}
}
@@ -74,6 +81,32 @@ function fakeAuth({ mode = 'oauth', token = TOKEN } = {}) {
@@ -74,6 +81,32 @@ function fakeAuth({ mode = 'oauth', token = TOKEN } = {}) {
assert.strictEqual(dock.mounted.sibling('dock.js'), 'chrome-extension://abc/shared/dock.js', 'the bundled Dock, from this extension');
assert.strictEqual(dock.mounted.sibling('dock.js'), 'chrome-extension://abc/shared/dock.js', 'the bundled Dock, from this extension');
assert.strictEqual(doc.nodes.gate.hidden, true, 'the gate steps aside');
assert.strictEqual(doc.nodes.gate.hidden, true, 'the gate steps aside');
assert.ok(net1.requests.some((r) => r.url === '/api/v1/me/dock' && r.auth === 'Bearer ' + TOKEN), 'the Network is asked with that sign-in');
assert.ok(net1.requests.some((r) => r.url === '/api/v1/me/dock' && r.auth === 'Bearer ' + TOKEN), 'the Network is asked with that sign-in');
+ assert.deepStrictEqual(dock.openedWith.doc, net1.state(), 'the Dock opens on the state the Network keeps, before its own read could race it');
+ assert.ok(!('tab' in dock.openedWith), 'the Dock itself chooses the tab from that state and what needs the person');
+ });
+
+ await check('the panel follows the state the Network keeps', async () => {
+ const first = {
+ state: { open: true, tab: 'chats', pinned: true, agent: 'ags_01JAB2C3D4E5F6G7H8J9K0MNPQ', chat: '11', place: { phone: { corner: 'bottom-right' }, desktop: { corner: 'top-left', width: 500, height: 600 } }, updated_at: new Date().toISOString() },
+ counts: { running: 0, waiting: 0, unread: 3 },
+ };
+ const net2 = await startNetwork({ state: first.state, counts: first.counts });
+ try {
+ const doc = fakeDoc();
+ const dock = fakeDock();
+ const net = createNetworkClient({ origin: net2.origin, fetch: (...a) => fetch(...a), getToken: async () => TOKEN });
+ const panel = createPanel({ doc, config: { NETWORK_ORIGIN: 'https://openvibe.network', CHAT_ORIGIN: 'https://openvibe.chat' }, platform: { storage: { getNetworkToken: async () => null } }, auth: fakeAuth(), net, dock });
+ await panel.start();
+ assert.strictEqual(dock.openedWith.doc.state.tab, 'chats', 'the side panel opens on the page\'s tab');
+ assert.deepStrictEqual(dock.openedWith.doc.counts, first.counts, 'and the unread counts the page sees');
+ // The page changes the tab and an agent: the next read carries it to the open side panel.
+ const next = { ...first, state: { ...first.state, tab: 'agent' }, counts: { running: 1, waiting: 0, unread: 3 } };
+ net2.setState(next);
+ assert.deepStrictEqual(await panel.sync(), next, 'the panel reads the same Network state again');
+ assert.deepStrictEqual(dock.updated.at(-1), next, 'and hands it to the Dock in place, without a reload');
+ } finally {
+ net2.close();
+ }
});
});
await check('without a sign-in the panel says what is missing', async () => {
await check('without a sign-in the panel says what is missing', async () => {