openvibers/openvibe.shared Commits
Dock: show editable DMs live (#459)
Changes
@@@ -1,5 +1,9 @@# Changelog++## 4.203.0 — 2026-11-11++++- Dock conversations update edited messages and link cards live; authors can edit their own messages.++## 4.202.0 — 2026-11-11**One field dropdown, one window for small interfaces, one user card, a measured page end, eight site groups, and the reveal remade.**@@@ -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.202.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed++**Status:** alpha, v4.203.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployedconsumer installs a tagged release and none keeps a vendored copy([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` namesthe tag it pins.@@@ -4,13 +4,14 @@"code": true,"wave": "W2","package": "openvibe-shared",-- "version": "4.202.0",++ "version": "4.203.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.399.0 (devDependency: the release manifest contract)","sdk": "openvibe-sdk v0.147.0","features": [++ "the Dock updates edited DM text and link cards live and lets authors edit their own messages (4.203.0)","the shared field dropdown, one dialog/sheet/panel window, user cards, eight site groups, measured page ends and remade reveal (4.202.0)","the page-view beacon sends ?in=1 only for a signed-in browser and counts a guest at once, so a signed-in person is counted from presence and never also as a guest (4.191.0)","the mark at work is the O as eight dots with three bright ones going round, and it has a state of its own for an agent that is thinking or writing (data-state=think); a phone's Dock opens and closes exactly as the menu does and wears its close button in its place; a button, field, select or text box that nobody styled wears the theme instead of the browser's grey (4.186.0)",nnew file mode 100644@@@ -0,0 +1,41 @@++/* Edit events for an open Dock DM. Loaded when the conversation opens. */++(function (root) {++ 'use strict';++ root.OpenVibeDockChatEdits = function ({ id, list, messages, error, tokenOf, chatCall, chatBase, active, load, h }) {++ let socket = null, timer = null, stopped = false;++ function apply(m) {++ const old = messages.find((x) => Number(x.id) === Number(m && m.id));++ const li = old && list.querySelector(`[data-id="${Number(m.id)}"]`);++ if (!li || !m.edited_at || (old.edited_at && old.edited_at >= m.edited_at)) return;++ Object.assign(old, { message: m.message, card: m.card, edited_at: m.edited_at });++ li.querySelector('.ovdk-text').textContent = String(m.message || '');++ let mark = li.querySelector('.ovdk-edited');++ if (!mark) { mark = h('small', 'ovdk-edited'); li.append(mark); }++ mark.textContent = `edited · ${String(m.edited_at).slice(11, 16)}`;++ const card = li.querySelector('.ovdk-card'); if (card) card.remove();++ if (m.card) load('dock-card.js', () => !!root.OpenVibeDockCard).then((ok) => { if (ok && active() && li.isConnected) { const c = root.OpenVibeDockCard(m.card, h); if (c) li.append(c); } });++ }++ async function connect() {++ if (stopped || !active() || !root.WebSocket) return;++ const token = await tokenOf();++ if (!token || stopped || !active()) return;++ const ws = new root.WebSocket(chatBase().replace(/^http/, 'ws') + '/ws/chat');++ socket = ws;++ ws.onopen = () => ws.send(JSON.stringify({ type: 'join', token }));++ ws.onmessage = (event) => { let frame; try { frame = JSON.parse(event.data); } catch { return; } if (frame.type === 'dm_edited' && String(frame.conversation_id) === id) apply(frame.message); };++ ws.onclose = () => { if (socket === ws) socket = null; if (!stopped && active()) timer = setTimeout(connect, 3000); };++ }++ list.addEventListener('click', async (event) => {++ if (!event.target.classList.contains('ovdk-edit')) return;++ const li = event.target.closest('[data-id]');++ const old = li && messages.find((m) => Number(m.id) === Number(li.dataset.id));++ if (!old) return;++ const body = root.prompt('Edit message', old.message);++ if (body == null || body.trim() === old.message.trim()) return;++ try { const result = await chatCall(`/api/dm/conversations/${encodeURIComponent(id)}/messages/${encodeURIComponent(old.id)}`, { method: 'PATCH', body: JSON.stringify({ message: body }) }); apply(result.message); }++ catch (e) { error.textContent = e.message || 'Could not edit'; error.hidden = false; }++ });++ connect().catch(() => {});++ return { stop() { stopped = true; clearTimeout(timer); if (socket) { socket.onclose = null; socket.close(); socket = null; } } };++ };++})(typeof window === 'undefined' ? globalThis : window);@@@ -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': '1bfa19b01e8e', 'dock-code.js': '3dcbaca10bfb', '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': 'c1281ef69ea8', 'dock-timer.js': '460d68aa8321', 'dock-share.js': 'e83b2e27b3ef', 'dock-card.js': '2643c20b0016' };++ const DOCK_REV = { 'dock-vibe.js': '1bfa19b01e8e', 'dock-code.js': '3dcbaca10bfb', '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': 'c1281ef69ea8', '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);@@@ -616,7 +616,7 @@// The person's conversations and DMs, from OpenVibe.Chat's own API with their Network token (Chat checks they are// in each one). Everything people wrote is set as text, never markup; only https pictures are shown. A thread polls// for new messages every 5 s while it is open in the Dock, and is marked read as it is shown.-- const chats = { panel: null, list: null, open: null, msgs: [], last: 0, timer: 0, unread: 0, unreadT: 0, busy: false, draft: null, vibeSummary: null, filter: '' };++ const chats = { panel: null, list: null, open: null, msgs: [], last: 0, timer: 0, edits: null, unread: 0, unreadT: 0, busy: false, draft: null, vibeSummary: null, filter: '' };const chatBase = () => String(o.chatBase || 'https://openvibe.chat').replace(/\/+$/, '');const me = () => String(o.me || '');const scopes = () => Array.isArray(o.scopes) ? o.scopes : null;@@@ -664,6 +664,7 @@const titleOf = (c) => c.name || others(c).map((p) => p.display_name || p.username).join(', ') || 'Just you';async function chatList() {clearTimeout(chats.timer);++ if (chats.edits) { chats.edits.stop(); chats.edits = null; }chats.open = null;const panel = chats.panel; if (!panel) return;if (!chats.list) { panel.innerHTML = '<div class="ovdk-wait"><span></span></div>'; }@@@ -720,7 +721,10 @@const mine = m.username === me();const li = h('li', `ovdk-msg${mine ? ' is-mine' : ''}${prev && prev.username === m.username ? ' is-cont' : ''}`);if (!mine && !(prev && prev.username === m.username)) li.append(h('span', 'ovdk-who', m.display_name || m.username || ''));++ li.dataset.id = String(m.id);li.append(h('span', 'ovdk-text', String(m.message || '')));++ if (m.edited_at) li.append(h('small', 'ovdk-edited', `edited · ${String(m.edited_at).slice(11, 16)}`));++ if (mine && canWriteChat()) { const edit = h('button', 'ovk-btn ovk-btn--quiet ovdk-edit', 'Edit'); edit.type = 'button'; li.append(edit); }// A known OpenVibe link's card (Chat's link cards): dock-card.js draws it, loaded the first time one shows.if (m.card) load('dock-card.js', () => !!root.OpenVibeDockCard).then((ok) => { const c = ok && root.OpenVibeDockCard(m.card, h); if (c) li.append(c); });li.title = new Date(m.created_at).toLocaleString();@@@ -728,6 +732,7 @@}async function openChat(id, remember) {clearTimeout(chats.timer);++ if (chats.edits) { chats.edits.stop(); chats.edits = null; }const panel = chats.panel; if (!panel) return;chats.open = String(id); chats.msgs = []; chats.last = 0;const draft = chats.draft && canWriteChat() ? chats.draft.text : '';@@@ -776,6 +781,7 @@list.append(h('li', 'ovdk-wait', null));list.firstChild.innerHTML = '<span></span>';await pull(list, scroll, true);++ load('dock-chat-edits.js', () => !!root.OpenVibeDockChatEdits).then((ok) => { if (ok && chats.open === String(id)) chats.edits = root.OpenVibeDockChatEdits({ id: String(id), list, messages: chats.msgs, error: err, tokenOf, chatCall, chatBase, active: () => chats.open === String(id) && panelActive('chats'), load, h }); });if (canWriteChat() && !(typeof matchMedia === 'function' && matchMedia('(pointer: coarse)').matches)) setTimeout(() => { try { box.focus({ preventScroll: true }); if (draft) box.setSelectionRange(box.value.length, box.value.length); } catch { /* */ } }, 0);}/** New messages of the open thread (all of them the first time), drawn in place; read once shown. */@@@ -32,6 +32,7 @@ const BROWSER = Object.freeze(['dock-timer.js', // the Dock's timer (L48 R2); dock.js loads it while a timer runs or its menu opens'dock-share.js', // the Dock's Send to… (4.144.0); dock.js loads it the first time something is sent'dock-card.js', // a known OpenVibe link's card under a Dock message (4.147.0); loaded the first time one shows++ 'dock-chat-edits.js', // live edits in an open Dock DM; loaded with that conversation'ov-canvas.js', // an agent's live panel (actor.canvas@1, 4.145.0); a page that shows canvases loads it'dock-actor.js', // the Dock's Actor block (Actor v3 A3); dock.js loads it the first time the Actor tab opens'dock-agent.js', // the Dock's agent view (K-D3); dock.js loads it the first time the Agents tab opens@@@ -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': '8eec18660ff4', 'dock.js': '4031e67d411d', 'presence.js': '5f195d41f8c5', 'panels.js': 'd90bfe987d81', 'surfaces.js': 'e72f5a304f2d', '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': '336b30eae325', 'ov-home.js': 'ea69b8a08868', 'nav-slots.js': '7b11e6916732', 'ov-wallet.js': 'cca1c50e1cce', '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': '8eec18660ff4', 'dock.js': '1e63c27fc63c', 'presence.js': '5f195d41f8c5', 'panels.js': 'd90bfe987d81', 'surfaces.js': 'e72f5a304f2d', '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': '336b30eae325', 'ov-home.js': 'ea69b8a08868', 'nav-slots.js': '7b11e6916732', 'ov-wallet.js': 'cca1c50e1cce', '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.@@@ -1,12 +1,12 @@{"name": "openvibe-shared",-- "version": "4.202.0",++ "version": "4.203.0","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-shared",-- "version": "4.202.0",++ "version": "4.203.0","license": "MIT","dependencies": {"clean-css": "^5.3.3",@@@ -1,6 +1,6 @@{"name": "openvibe-shared",-- "version": "4.202.0",++ "version": "4.203.0","description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site","main": "index.js","bin": {@@@ -68,6 +68,7 @@"./dock-timer": "./dock-timer.js","./dock-share": "./dock-share.js","./dock-card": "./dock-card.js",++ "./dock-chat-edits": "./dock-chat-edits.js","./ov-canvas": "./ov-canvas.js","./dock-agent": "./dock-agent.js","./dock-actor": "./dock-actor.js",@@@ -25,7 +25,7 @@ const FILES = ['ov-mark.js', 'ov-glyphs.js', 'ov-brand.woff2', 'ov-look.js', 'ov// 2026-11-10: soft pages (intent prefetch, cross-document view transitions); not loaded by navbar.js yet.'soft.js'];// 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'] },++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'] }, 'boost.js': { name: 'BOOST_REV', files: ['route-transition.js'] } };const table = (name, files) => ` const ${name} = { ${files.map((f) => `'${f}': '${hashAsset(fs.readFileSync(path.join(ROOT, f)))}'`).join(', ')} };`;@@@ -168,6 +168,8 @@ const server = http.createServer((req, res) => {}const dm = /^\/api\/dm\/conversations\/(\d+)\/(messages|read)$/.exec(u.pathname);if (dm && dm[2] === 'read') { reads.push(dm[1]); return json(200, { ok: true }); }++ const dmEdit = /^\/api\/dm\/conversations\/(\d+)\/messages\/(\d+)$/.exec(u.pathname);++ if (dmEdit && req.method === 'PATCH') { const text = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}').message; return json(200, { message: { id: Number(dmEdit[2]), message: text, card: null, edited_at: new Date().toISOString() } }); }if (dm && req.method === 'POST') { said.push(JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}').message); return json(201, { message: { id: 900 + said.length } }); }if (dm) {const after = Number(u.searchParams.get('after')) || 0;@@@ -383,6 +385,10 @@ async function drag(p, from, to) {assert.deepStrictEqual(cards[0], ['A', 'The studio tour', 'https://openvibe.video/v/42', 'https://openvibe.media/t/abc?w=320', 'Watch together https://openvibe.party/new?media=https%3A%2F%2Fopenvibe.video%2Fv%2F42'], 'a card: its picture, its title as a link, its one move');assert.deepStrictEqual(cards[1], ['B', '<img src=x>', null, null, null], 'a card is data: no script link, no picture from elsewhere, no script button, the title as text');assert.strictEqual(await p.evaluate("document.querySelectorAll('script[src*=\"dock-card.js\"]').length"), 1, 'dock-card.js is loaded once, when a card first shows');++ await until(p, "!!document.querySelector('.ovdk .ovdk-edit')", 'the edit action');++ await p.evaluate("(() => { window.prompt = () => 'updated by me'; document.querySelector('.ovdk .ovdk-edit').click(); })()");++ await until(p, "document.querySelector('.ovdk .ovdk-msg.is-mine .ovdk-text').textContent === 'updated by me'", 'the edited message');++ assert.strictEqual(await p.evaluate("document.querySelector('.ovdk .ovdk-msg.is-mine .ovdk-edited').textContent.includes('edited')"), true);assert.strictEqual(await p.evaluate("!!document.querySelector('.ovdk:not([hidden])')"), true, 'opening a chat on a phone is not a tap outside the sheet');await p.evaluate("(() => { const b = document.querySelector('.ovdk-send textarea'); b.value = 'Looks good'; b.dispatchEvent(new Event('input')); b.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true })); })()");await until(p, "document.querySelectorAll('.ovdk .ovdk-msg').length === 3", 'the sent message in the thread');@@@ -21,8 +21,8 @@ const DOCK_PATH = path.join(ROOT, 'dock.js');// time something is sent, not on every page.// The timer itself (menu, countdown, chime, focus rounds) is dock-timer.js (3.7 KB brotli), loaded only while a timer// runs or its menu opens; the header's tab rail and narrow-width rules came with it.--const DOCK_BROTLI_BUDGET = 21100; // 20.61 KB with 4.202.0 page-end docking (#442, 2026-11-11); was 20.55 KB with the Code tab (ask 350, 2026-11-10: Chat · Code · Agents; the tab lives in dock-code.js, only its mount branch ships in dock.js); was 20940 with "Open in Chat" as an icon over your chats and in a thread (ask 336, 2026-11-10); 20.30 KB 20.30 KB with a robot for Agents, so the spark means one thing (Vibe) in the Dock as in the menu's row of places (4.194.0, 2026-11-10); 20.25 KB: 20.04 KB with a phone's sheet opening and closing as the menu's does (a circle from the orb, the menu's close button in the menu's place, a ring as it opens; 4.186.0, 2026-11-10); 20.00 KB: 19.80 KB with a phone's sheet opening out of the navbar's orb (4.185.0, 2026-11-10: the hand-off's styles and its two functions, about 0.95 KB; it runs as the sheet opens, so it cannot be a sibling loaded later); 19.00 KB: 18.63 KB with Dock v2's AI chats row, the conversation list, the group faces and the maker's marks (2026-11-09); 18.50 KB: 18.1 KB with the Actor tab, the sheets' shared surface and Dock chats from a site's session (4.162.0, 2026-11-09); 18.34 KB measured on 2026-11-09 with Dock v2's Chats summary and its lazy Agents rows; 18.00 KB: 17.9 KB with the orb's × as Close, dots drawn on the centre line and the pop from the pill (4.161.0, 2026-11-09); 17.50 KB: 17.28 KB with the full-screen phone Dock, the one-row header and its More menu (2026-11-09, owner: the pin unclear, pop-overs that did not close) + headroom; 16.80 KB: 16.55 KB with the phone header's two rows (4.150.0, 2026-11-08: the open tab read "Ac") + headroom; 16.40 KB: 16.30 KB with the link cards' loader (4.147.0, 2026-11-08; the card itself is dock-card.js, loaded the first time one shows) + headroom; 16.25 KB: 16.07 KB with OpenVibe Chat's hooks (share(), canSend(), the draft; 4.144.0, 2026-11-08), Send to… itself is dock-share.js (1.9 KB, loaded on first use); 16.0 KB: 15.74 KB with its siblings' ?v= content hashes (4.137.0, 2026-11-07) + headroom; 15.70 KB: 15.55 KB with Ask your agent (4.135.0, 2026-11-07) + headroom; was 15.50 KB (15435 after 4.120.0)--const SIBLINGS = ['action-center.js', 'notification-ui.js', 'notification-live.js', 'ov-mark.js', 'dock-vibe.js', 'dock-mount.js', 'dock-runs.js', 'ai-marks-client.js', 'dock-timer.js', 'dock-agent.js', 'dock-share.js', 'dock-card.js'];++const DOCK_BROTLI_BUDGET = 21300; // 4.203.0 adds 0.13 KB for the lazy DM edit loader and button; prior budget was 21100 bytes. // 20.61 KB with 4.202.0 page-end docking (#442, 2026-11-11); was 20.55 KB with the Code tab (ask 350, 2026-11-10: Chat · Code · Agents; the tab lives in dock-code.js, only its mount branch ships in dock.js); was 20940 with "Open in Chat" as an icon over your chats and in a thread (ask 336, 2026-11-10); 20.30 KB 20.30 KB with a robot for Agents, so the spark means one thing (Vibe) in the Dock as in the menu's row of places (4.194.0, 2026-11-10); 20.25 KB: 20.04 KB with a phone's sheet opening and closing as the menu's does (a circle from the orb, the menu's close button in the menu's place, a ring as it opens; 4.186.0, 2026-11-10); 20.00 KB: 19.80 KB with a phone's sheet opening out of the navbar's orb (4.185.0, 2026-11-10: the hand-off's styles and its two functions, about 0.95 KB; it runs as the sheet opens, so it cannot be a sibling loaded later); 19.00 KB: 18.63 KB with Dock v2's AI chats row, the conversation list, the group faces and the maker's marks (2026-11-09); 18.50 KB: 18.1 KB with the Actor tab, the sheets' shared surface and Dock chats from a site's session (4.162.0, 2026-11-09); 18.34 KB measured on 2026-11-09 with Dock v2's Chats summary and its lazy Agents rows; 18.00 KB: 17.9 KB with the orb's × as Close, dots drawn on the centre line and the pop from the pill (4.161.0, 2026-11-09); 17.50 KB: 17.28 KB with the full-screen phone Dock, the one-row header and its More menu (2026-11-09, owner: the pin unclear, pop-overs that did not close) + headroom; 16.80 KB: 16.55 KB with the phone header's two rows (4.150.0, 2026-11-08: the open tab read "Ac") + headroom; 16.40 KB: 16.30 KB with the link cards' loader (4.147.0, 2026-11-08; the card itself is dock-card.js, loaded the first time one shows) + headroom; 16.25 KB: 16.07 KB with OpenVibe Chat's hooks (share(), canSend(), the draft; 4.144.0, 2026-11-08), Send to… itself is dock-share.js (1.9 KB, loaded on first use); 16.0 KB: 15.74 KB with its siblings' ?v= content hashes (4.137.0, 2026-11-07) + headroom; 15.70 KB: 15.55 KB with Ask your agent (4.135.0, 2026-11-07) + headroom; was 15.50 KB (15435 after 4.120.0)++const SIBLINGS = ['action-center.js', 'notification-ui.js', 'notification-live.js', 'ov-mark.js', 'dock-vibe.js', 'dock-mount.js', 'dock-runs.js', 'ai-marks-client.js', 'dock-timer.js', 'dock-agent.js', 'dock-share.js', 'dock-card.js', 'dock-chat-edits.js'];// ── the budget ──────────────────────────────────────────────{
@@ -1,5 +1,9 @@
@@ -1,5 +1,9 @@
# Changelog
# Changelog
+## 4.203.0 — 2026-11-11
+
+- Dock conversations update edited messages and link cards live; authors can edit their own messages.
+
## 4.202.0 — 2026-11-11
## 4.202.0 — 2026-11-11
**One field dropdown, one window for small interfaces, one user card, a measured page end, eight site groups, and the reveal remade.**
**One field dropdown, one window for small interfaces, one user card, a measured page end, eight site groups, and the reveal remade.**
@@ -5,7 +5,7 @@
@@ -5,7 +5,7 @@
**Docs:** https://openvibe.services · **For AI:** https://openvibe.network/llms.txt
**Docs:** https://openvibe.services · **For AI:** https://openvibe.network/llms.txt
**Install and test:** `npm ci && npm test`
**Install and test:** `npm ci && npm test`
-**Status:** alpha, v4.202.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
+**Status:** alpha, v4.203.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
consumer installs a tagged release and none keeps a vendored copy
consumer installs a tagged release and none keeps a vendored copy
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
the tag it pins.
the tag it pins.
@@ -4,13 +4,14 @@
@@ -4,13 +4,14 @@
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
"package": "openvibe-shared",
"package": "openvibe-shared",
- "version": "4.202.0",
+ "version": "4.203.0",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"deployed": false,
"deployed": false,
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
"contracts": "openvibe-contracts v0.399.0 (devDependency: the release manifest contract)",
"contracts": "openvibe-contracts v0.399.0 (devDependency: the release manifest contract)",
"sdk": "openvibe-sdk v0.147.0",
"sdk": "openvibe-sdk v0.147.0",
"features": [
"features": [
+ "the Dock updates edited DM text and link cards live and lets authors edit their own messages (4.203.0)",
"the shared field dropdown, one dialog/sheet/panel window, user cards, eight site groups, measured page ends and remade reveal (4.202.0)",
"the shared field dropdown, one dialog/sheet/panel window, user cards, eight site groups, measured page ends and remade reveal (4.202.0)",
"the page-view beacon sends ?in=1 only for a signed-in browser and counts a guest at once, so a signed-in person is counted from presence and never also as a guest (4.191.0)",
"the page-view beacon sends ?in=1 only for a signed-in browser and counts a guest at once, so a signed-in person is counted from presence and never also as a guest (4.191.0)",
"the mark at work is the O as eight dots with three bright ones going round, and it has a state of its own for an agent that is thinking or writing (data-state=think); a phone's Dock opens and closes exactly as the menu does and wears its close button in its place; a button, field, select or text box that nobody styled wears the theme instead of the browser's grey (4.186.0)",
"the mark at work is the O as eight dots with three bright ones going round, and it has a state of its own for an agent that is thinking or writing (data-state=think); a phone's Dock opens and closes exactly as the menu does and wears its close button in its place; a button, field, select or text box that nobody styled wears the theme instead of the browser's grey (4.186.0)",
new file mode 100644
new file mode 100644
@@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
+/* Edit events for an open Dock DM. Loaded when the conversation opens. */
+(function (root) {
+ 'use strict';
+ root.OpenVibeDockChatEdits = function ({ id, list, messages, error, tokenOf, chatCall, chatBase, active, load, h }) {
+ let socket = null, timer = null, stopped = false;
+ function apply(m) {
+ const old = messages.find((x) => Number(x.id) === Number(m && m.id));
+ const li = old && list.querySelector(`[data-id="${Number(m.id)}"]`);
+ if (!li || !m.edited_at || (old.edited_at && old.edited_at >= m.edited_at)) return;
+ Object.assign(old, { message: m.message, card: m.card, edited_at: m.edited_at });
+ li.querySelector('.ovdk-text').textContent = String(m.message || '');
+ let mark = li.querySelector('.ovdk-edited');
+ if (!mark) { mark = h('small', 'ovdk-edited'); li.append(mark); }
+ mark.textContent = `edited · ${String(m.edited_at).slice(11, 16)}`;
+ const card = li.querySelector('.ovdk-card'); if (card) card.remove();
+ if (m.card) load('dock-card.js', () => !!root.OpenVibeDockCard).then((ok) => { if (ok && active() && li.isConnected) { const c = root.OpenVibeDockCard(m.card, h); if (c) li.append(c); } });
+ }
+ async function connect() {
+ if (stopped || !active() || !root.WebSocket) return;
+ const token = await tokenOf();
+ if (!token || stopped || !active()) return;
+ const ws = new root.WebSocket(chatBase().replace(/^http/, 'ws') + '/ws/chat');
+ socket = ws;
+ ws.onopen = () => ws.send(JSON.stringify({ type: 'join', token }));
+ ws.onmessage = (event) => { let frame; try { frame = JSON.parse(event.data); } catch { return; } if (frame.type === 'dm_edited' && String(frame.conversation_id) === id) apply(frame.message); };
+ ws.onclose = () => { if (socket === ws) socket = null; if (!stopped && active()) timer = setTimeout(connect, 3000); };
+ }
+ list.addEventListener('click', async (event) => {
+ if (!event.target.classList.contains('ovdk-edit')) return;
+ const li = event.target.closest('[data-id]');
+ const old = li && messages.find((m) => Number(m.id) === Number(li.dataset.id));
+ if (!old) return;
+ const body = root.prompt('Edit message', old.message);
+ if (body == null || body.trim() === old.message.trim()) return;
+ try { const result = await chatCall(`/api/dm/conversations/${encodeURIComponent(id)}/messages/${encodeURIComponent(old.id)}`, { method: 'PATCH', body: JSON.stringify({ message: body }) }); apply(result.message); }
+ catch (e) { error.textContent = e.message || 'Could not edit'; error.hidden = false; }
+ });
+ connect().catch(() => {});
+ return { stop() { stopped = true; clearTimeout(timer); if (socket) { socket.onclose = null; socket.close(); socket = null; } } };
+ };
+})(typeof window === 'undefined' ? globalThis : window);
@@ -89,7 +89,7 @@
@@ -89,7 +89,7 @@
}
}
// Each sibling's content hash (scripts/sibling-revs.js writes it), so a release gives each a new URL.
// Each sibling's content hash (scripts/sibling-revs.js writes it), so a release gives each a new URL.
- const DOCK_REV = { 'dock-vibe.js': '1bfa19b01e8e', 'dock-code.js': '3dcbaca10bfb', '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': 'c1281ef69ea8', 'dock-timer.js': '460d68aa8321', 'dock-share.js': 'e83b2e27b3ef', 'dock-card.js': '2643c20b0016' };
+ const DOCK_REV = { 'dock-vibe.js': '1bfa19b01e8e', 'dock-code.js': '3dcbaca10bfb', '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': 'c1281ef69ea8', 'dock-timer.js': '460d68aa8321', 'dock-share.js': 'e83b2e27b3ef', 'dock-card.js': '2643c20b0016', 'dock-chat-edits.js': '21e125e3f1e9' };
function load(src, test) {
function load(src, test) {
return new Promise((ok) => {
return new Promise((ok) => {
if (test()) return ok(true);
if (test()) return ok(true);
@@ -616,7 +616,7 @@
@@ -616,7 +616,7 @@
// The person's conversations and DMs, from OpenVibe.Chat's own API with their Network token (Chat checks they are
// The person's conversations and DMs, from OpenVibe.Chat's own API with their Network token (Chat checks they are
// in each one). Everything people wrote is set as text, never markup; only https pictures are shown. A thread polls
// in each one). Everything people wrote is set as text, never markup; only https pictures are shown. A thread polls
// for new messages every 5 s while it is open in the Dock, and is marked read as it is shown.
// for new messages every 5 s while it is open in the Dock, and is marked read as it is shown.
- const chats = { panel: null, list: null, open: null, msgs: [], last: 0, timer: 0, unread: 0, unreadT: 0, busy: false, draft: null, vibeSummary: null, filter: '' };
+ const chats = { panel: null, list: null, open: null, msgs: [], last: 0, timer: 0, edits: null, unread: 0, unreadT: 0, busy: false, draft: null, vibeSummary: null, filter: '' };
const chatBase = () => String(o.chatBase || 'https://openvibe.chat').replace(/\/+$/, '');
const chatBase = () => String(o.chatBase || 'https://openvibe.chat').replace(/\/+$/, '');
const me = () => String(o.me || '');
const me = () => String(o.me || '');
const scopes = () => Array.isArray(o.scopes) ? o.scopes : null;
const scopes = () => Array.isArray(o.scopes) ? o.scopes : null;
@@ -664,6 +664,7 @@
@@ -664,6 +664,7 @@
const titleOf = (c) => c.name || others(c).map((p) => p.display_name || p.username).join(', ') || 'Just you';
const titleOf = (c) => c.name || others(c).map((p) => p.display_name || p.username).join(', ') || 'Just you';
async function chatList() {
async function chatList() {
clearTimeout(chats.timer);
clearTimeout(chats.timer);
+ if (chats.edits) { chats.edits.stop(); chats.edits = null; }
chats.open = null;
chats.open = null;
const panel = chats.panel; if (!panel) return;
const panel = chats.panel; if (!panel) return;
if (!chats.list) { panel.innerHTML = '<div class="ovdk-wait"><span></span></div>'; }
if (!chats.list) { panel.innerHTML = '<div class="ovdk-wait"><span></span></div>'; }
@@ -720,7 +721,10 @@
@@ -720,7 +721,10 @@
const mine = m.username === me();
const mine = m.username === me();
const li = h('li', `ovdk-msg${mine ? ' is-mine' : ''}${prev && prev.username === m.username ? ' is-cont' : ''}`);
const li = h('li', `ovdk-msg${mine ? ' is-mine' : ''}${prev && prev.username === m.username ? ' is-cont' : ''}`);
if (!mine && !(prev && prev.username === m.username)) li.append(h('span', 'ovdk-who', m.display_name || m.username || ''));
if (!mine && !(prev && prev.username === m.username)) li.append(h('span', 'ovdk-who', m.display_name || m.username || ''));
+ li.dataset.id = String(m.id);
li.append(h('span', 'ovdk-text', String(m.message || '')));
li.append(h('span', 'ovdk-text', String(m.message || '')));
+ if (m.edited_at) li.append(h('small', 'ovdk-edited', `edited · ${String(m.edited_at).slice(11, 16)}`));
+ if (mine && canWriteChat()) { const edit = h('button', 'ovk-btn ovk-btn--quiet ovdk-edit', 'Edit'); edit.type = 'button'; li.append(edit); }
// A known OpenVibe link's card (Chat's link cards): dock-card.js draws it, loaded the first time one shows.
// A known OpenVibe link's card (Chat's link cards): dock-card.js draws it, loaded the first time one shows.
if (m.card) load('dock-card.js', () => !!root.OpenVibeDockCard).then((ok) => { const c = ok && root.OpenVibeDockCard(m.card, h); if (c) li.append(c); });
if (m.card) load('dock-card.js', () => !!root.OpenVibeDockCard).then((ok) => { const c = ok && root.OpenVibeDockCard(m.card, h); if (c) li.append(c); });
li.title = new Date(m.created_at).toLocaleString();
li.title = new Date(m.created_at).toLocaleString();
@@ -728,6 +732,7 @@
@@ -728,6 +732,7 @@
}
}
async function openChat(id, remember) {
async function openChat(id, remember) {
clearTimeout(chats.timer);
clearTimeout(chats.timer);
+ if (chats.edits) { chats.edits.stop(); chats.edits = null; }
const panel = chats.panel; if (!panel) return;
const panel = chats.panel; if (!panel) return;
chats.open = String(id); chats.msgs = []; chats.last = 0;
chats.open = String(id); chats.msgs = []; chats.last = 0;
const draft = chats.draft && canWriteChat() ? chats.draft.text : '';
const draft = chats.draft && canWriteChat() ? chats.draft.text : '';
@@ -776,6 +781,7 @@
@@ -776,6 +781,7 @@
list.append(h('li', 'ovdk-wait', null));
list.append(h('li', 'ovdk-wait', null));
list.firstChild.innerHTML = '<span></span>';
list.firstChild.innerHTML = '<span></span>';
await pull(list, scroll, true);
await pull(list, scroll, true);
+ load('dock-chat-edits.js', () => !!root.OpenVibeDockChatEdits).then((ok) => { if (ok && chats.open === String(id)) chats.edits = root.OpenVibeDockChatEdits({ id: String(id), list, messages: chats.msgs, error: err, tokenOf, chatCall, chatBase, active: () => chats.open === String(id) && panelActive('chats'), load, h }); });
if (canWriteChat() && !(typeof matchMedia === 'function' && matchMedia('(pointer: coarse)').matches)) setTimeout(() => { try { box.focus({ preventScroll: true }); if (draft) box.setSelectionRange(box.value.length, box.value.length); } catch { /* */ } }, 0);
if (canWriteChat() && !(typeof matchMedia === 'function' && matchMedia('(pointer: coarse)').matches)) setTimeout(() => { try { box.focus({ preventScroll: true }); if (draft) box.setSelectionRange(box.value.length, box.value.length); } catch { /* */ } }, 0);
}
}
/** New messages of the open thread (all of them the first time), drawn in place; read once shown. */
/** New messages of the open thread (all of them the first time), drawn in place; read once shown. */
@@ -32,6 +32,7 @@ const BROWSER = Object.freeze([
@@ -32,6 +32,7 @@ const BROWSER = Object.freeze([
'dock-timer.js', // the Dock's timer (L48 R2); dock.js loads it while a timer runs or its menu opens
'dock-timer.js', // the Dock's timer (L48 R2); dock.js loads it while a timer runs or its menu opens
'dock-share.js', // the Dock's Send to… (4.144.0); dock.js loads it the first time something is sent
'dock-share.js', // the Dock's Send to… (4.144.0); dock.js loads it the first time something is sent
'dock-card.js', // a known OpenVibe link's card under a Dock message (4.147.0); loaded the first time one shows
'dock-card.js', // a known OpenVibe link's card under a Dock message (4.147.0); loaded the first time one shows
+ 'dock-chat-edits.js', // live edits in an open Dock DM; loaded with that conversation
'ov-canvas.js', // an agent's live panel (actor.canvas@1, 4.145.0); a page that shows canvases loads it
'ov-canvas.js', // an agent's live panel (actor.canvas@1, 4.145.0); a page that shows canvases loads it
'dock-actor.js', // the Dock's Actor block (Actor v3 A3); dock.js loads it the first time the Actor tab opens
'dock-actor.js', // the Dock's Actor block (Actor v3 A3); dock.js loads it the first time the Actor tab opens
'dock-agent.js', // the Dock's agent view (K-D3); dock.js loads it the first time the Agents tab opens
'dock-agent.js', // the Dock's agent view (K-D3); dock.js loads it the first time the Agents tab opens
@@ -607,7 +607,7 @@
@@ -607,7 +607,7 @@
* serves its own pinned copy at /shared/ gets its own), else the network's copy.
* serves its own pinned copy at /shared/ gets its own), else the network's copy.
*/
*/
// The files loaded from beside this one, by content (scripts/sibling-revs.js): a release changes their URLs.
// The files loaded from beside this one, by content (scripts/sibling-revs.js): a release changes their URLs.
- const SIBLING_REV = { 'ov-mark.js': 'f34f731c3a94', 'ov-glyphs.js': '771efd0e3963', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': 'dd84a30bb646', 'ov-fx.js': '947820d69097', 'ov-labels.js': 'd40f7ca14bc4', 'ov-forms.js': '2eb9e2604c3e', 'ov-forms.css': '06208fe89eac', 'ov-share.js': 'd419508a797e', 'ov-share.css': 'd609a71f611a', 'search.js': '8eec18660ff4', 'dock.js': '4031e67d411d', 'presence.js': '5f195d41f8c5', 'panels.js': 'd90bfe987d81', 'surfaces.js': 'e72f5a304f2d', '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': '336b30eae325', 'ov-home.js': 'ea69b8a08868', 'nav-slots.js': '7b11e6916732', 'ov-wallet.js': 'cca1c50e1cce', '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': '8eec18660ff4', 'dock.js': '1e63c27fc63c', 'presence.js': '5f195d41f8c5', 'panels.js': 'd90bfe987d81', 'surfaces.js': 'e72f5a304f2d', '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': '336b30eae325', 'ov-home.js': 'ea69b8a08868', 'nav-slots.js': '7b11e6916732', 'ov-wallet.js': 'cca1c50e1cce', 'ov-wallet.css': 'be8daa24c3a8', 'purse.js': '749120bfd24b', 'ov-drop.js': '445689a5eb80', 'ov-tip.js': '1d918e7de69d', 'soft.js': '98ab9b8080b1' };
const siblingV = (name) => `${sibling(name)}${SIBLING_REV[name] ? `?v=${SIBLING_REV[name]}` : ''}`;
const siblingV = (name) => `${sibling(name)}${SIBLING_REV[name] ? `?v=${SIBLING_REV[name]}` : ''}`;
// ov-tip.js loads at the first hover over a tooltip's owner.
// ov-tip.js loads at the first hover over a tooltip's owner.
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.202.0",
+ "version": "4.203.0",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.202.0",
+ "version": "4.203.0",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"clean-css": "^5.3.3",
"clean-css": "^5.3.3",
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.202.0",
+ "version": "4.203.0",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"main": "index.js",
"bin": {
"bin": {
@@ -68,6 +68,7 @@
@@ -68,6 +68,7 @@
"./dock-timer": "./dock-timer.js",
"./dock-timer": "./dock-timer.js",
"./dock-share": "./dock-share.js",
"./dock-share": "./dock-share.js",
"./dock-card": "./dock-card.js",
"./dock-card": "./dock-card.js",
+ "./dock-chat-edits": "./dock-chat-edits.js",
"./ov-canvas": "./ov-canvas.js",
"./ov-canvas": "./ov-canvas.js",
"./dock-agent": "./dock-agent.js",
"./dock-agent": "./dock-agent.js",
"./dock-actor": "./dock-actor.js",
"./dock-actor": "./dock-actor.js",
@@ -25,7 +25,7 @@ const FILES = ['ov-mark.js', 'ov-glyphs.js', 'ov-brand.woff2', 'ov-look.js', 'ov
@@ -25,7 +25,7 @@ const FILES = ['ov-mark.js', 'ov-glyphs.js', 'ov-brand.woff2', 'ov-look.js', 'ov
// 2026-11-10: soft pages (intent prefetch, cross-document view transitions); not loaded by navbar.js yet.
// 2026-11-10: soft pages (intent prefetch, cross-document view transitions); not loaded by navbar.js yet.
'soft.js'];
'soft.js'];
// The files dock.js and notification-ui.js load from beside themselves: their own tables, in their own files.
// The files dock.js and notification-ui.js load from beside themselves: their own tables, in their own files.
-const OWN = { 'dock.js': { name: 'DOCK_REV', files: ['dock-vibe.js', 'dock-code.js', 'dock-mount.js', 'dock-runs.js', 'ai-marks-client.js', 'action-center.js', 'dock-timeline.js', 'dock-actor.js', 'dock-agent.js', 'dock-timer.js', 'dock-share.js', 'dock-card.js'] },
+const OWN = { 'dock.js': { name: 'DOCK_REV', files: ['dock-vibe.js', 'dock-code.js', 'dock-mount.js', 'dock-runs.js', 'ai-marks-client.js', 'action-center.js', 'dock-timeline.js', 'dock-actor.js', 'dock-agent.js', 'dock-timer.js', 'dock-share.js', 'dock-card.js', 'dock-chat-edits.js'] },
'notification-ui.js': { name: 'NOTIFY_REV', files: ['notification-live.js'] },
'notification-ui.js': { name: 'NOTIFY_REV', files: ['notification-live.js'] },
'footer.js': { name: 'FOOTER_REV', files: ['shipped.js'] }, 'ov-thread.js': { name: 'THREAD_REV', files: ['ov-composer.js', 'ov-combo.js'] }, 'ov-composer.js': { name: 'COMPOSER_REV', files: ['ov-window.js'] }, 'ov-user.js': { name: 'USER_REV', files: ['ov-window.js'] }, 'ov-wallet.js': { name: 'WALLET_REV', files: ['ov-window.js'] }, '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'] }, 'boost.js': { name: 'BOOST_REV', files: ['route-transition.js'] } };
const table = (name, files) => ` const ${name} = { ${files.map((f) => `'${f}': '${hashAsset(fs.readFileSync(path.join(ROOT, f)))}'`).join(', ')} };`;
const table = (name, files) => ` const ${name} = { ${files.map((f) => `'${f}': '${hashAsset(fs.readFileSync(path.join(ROOT, f)))}'`).join(', ')} };`;
@@ -168,6 +168,8 @@ const server = http.createServer((req, res) => {
@@ -168,6 +168,8 @@ const server = http.createServer((req, res) => {
}
}
const dm = /^\/api\/dm\/conversations\/(\d+)\/(messages|read)$/.exec(u.pathname);
const dm = /^\/api\/dm\/conversations\/(\d+)\/(messages|read)$/.exec(u.pathname);
if (dm && dm[2] === 'read') { reads.push(dm[1]); return json(200, { ok: true }); }
if (dm && dm[2] === 'read') { reads.push(dm[1]); return json(200, { ok: true }); }
+ const dmEdit = /^\/api\/dm\/conversations\/(\d+)\/messages\/(\d+)$/.exec(u.pathname);
+ if (dmEdit && req.method === 'PATCH') { const text = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}').message; return json(200, { message: { id: Number(dmEdit[2]), message: text, card: null, edited_at: new Date().toISOString() } }); }
if (dm && req.method === 'POST') { said.push(JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}').message); return json(201, { message: { id: 900 + said.length } }); }
if (dm && req.method === 'POST') { said.push(JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}').message); return json(201, { message: { id: 900 + said.length } }); }
if (dm) {
if (dm) {
const after = Number(u.searchParams.get('after')) || 0;
const after = Number(u.searchParams.get('after')) || 0;
@@ -383,6 +385,10 @@ async function drag(p, from, to) {
@@ -383,6 +385,10 @@ async function drag(p, from, to) {
assert.deepStrictEqual(cards[0], ['A', 'The studio tour', 'https://openvibe.video/v/42', 'https://openvibe.media/t/abc?w=320', 'Watch together https://openvibe.party/new?media=https%3A%2F%2Fopenvibe.video%2Fv%2F42'], 'a card: its picture, its title as a link, its one move');
assert.deepStrictEqual(cards[0], ['A', 'The studio tour', 'https://openvibe.video/v/42', 'https://openvibe.media/t/abc?w=320', 'Watch together https://openvibe.party/new?media=https%3A%2F%2Fopenvibe.video%2Fv%2F42'], 'a card: its picture, its title as a link, its one move');
assert.deepStrictEqual(cards[1], ['B', '<img src=x>', null, null, null], 'a card is data: no script link, no picture from elsewhere, no script button, the title as text');
assert.deepStrictEqual(cards[1], ['B', '<img src=x>', null, null, null], 'a card is data: no script link, no picture from elsewhere, no script button, the title as text');
assert.strictEqual(await p.evaluate("document.querySelectorAll('script[src*=\"dock-card.js\"]').length"), 1, 'dock-card.js is loaded once, when a card first shows');
assert.strictEqual(await p.evaluate("document.querySelectorAll('script[src*=\"dock-card.js\"]').length"), 1, 'dock-card.js is loaded once, when a card first shows');
+ await until(p, "!!document.querySelector('.ovdk .ovdk-edit')", 'the edit action');
+ await p.evaluate("(() => { window.prompt = () => 'updated by me'; document.querySelector('.ovdk .ovdk-edit').click(); })()");
+ await until(p, "document.querySelector('.ovdk .ovdk-msg.is-mine .ovdk-text').textContent === 'updated by me'", 'the edited message');
+ assert.strictEqual(await p.evaluate("document.querySelector('.ovdk .ovdk-msg.is-mine .ovdk-edited').textContent.includes('edited')"), true);
assert.strictEqual(await p.evaluate("!!document.querySelector('.ovdk:not([hidden])')"), true, 'opening a chat on a phone is not a tap outside the sheet');
assert.strictEqual(await p.evaluate("!!document.querySelector('.ovdk:not([hidden])')"), true, 'opening a chat on a phone is not a tap outside the sheet');
await p.evaluate("(() => { const b = document.querySelector('.ovdk-send textarea'); b.value = 'Looks good'; b.dispatchEvent(new Event('input')); b.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true })); })()");
await p.evaluate("(() => { const b = document.querySelector('.ovdk-send textarea'); b.value = 'Looks good'; b.dispatchEvent(new Event('input')); b.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true })); })()");
await until(p, "document.querySelectorAll('.ovdk .ovdk-msg').length === 3", 'the sent message in the thread');
await until(p, "document.querySelectorAll('.ovdk .ovdk-msg').length === 3", 'the sent message in the thread');
@@ -21,8 +21,8 @@ const DOCK_PATH = path.join(ROOT, 'dock.js');
@@ -21,8 +21,8 @@ const DOCK_PATH = path.join(ROOT, 'dock.js');
// time something is sent, not on every page.
// time something is sent, not on every page.
// The timer itself (menu, countdown, chime, focus rounds) is dock-timer.js (3.7 KB brotli), loaded only while a timer
// The timer itself (menu, countdown, chime, focus rounds) is dock-timer.js (3.7 KB brotli), loaded only while a timer
// runs or its menu opens; the header's tab rail and narrow-width rules came with it.
// runs or its menu opens; the header's tab rail and narrow-width rules came with it.
-const DOCK_BROTLI_BUDGET = 21100; // 20.61 KB with 4.202.0 page-end docking (#442, 2026-11-11); was 20.55 KB with the Code tab (ask 350, 2026-11-10: Chat · Code · Agents; the tab lives in dock-code.js, only its mount branch ships in dock.js); was 20940 with "Open in Chat" as an icon over your chats and in a thread (ask 336, 2026-11-10); 20.30 KB 20.30 KB with a robot for Agents, so the spark means one thing (Vibe) in the Dock as in the menu's row of places (4.194.0, 2026-11-10); 20.25 KB: 20.04 KB with a phone's sheet opening and closing as the menu's does (a circle from the orb, the menu's close button in the menu's place, a ring as it opens; 4.186.0, 2026-11-10); 20.00 KB: 19.80 KB with a phone's sheet opening out of the navbar's orb (4.185.0, 2026-11-10: the hand-off's styles and its two functions, about 0.95 KB; it runs as the sheet opens, so it cannot be a sibling loaded later); 19.00 KB: 18.63 KB with Dock v2's AI chats row, the conversation list, the group faces and the maker's marks (2026-11-09); 18.50 KB: 18.1 KB with the Actor tab, the sheets' shared surface and Dock chats from a site's session (4.162.0, 2026-11-09); 18.34 KB measured on 2026-11-09 with Dock v2's Chats summary and its lazy Agents rows; 18.00 KB: 17.9 KB with the orb's × as Close, dots drawn on the centre line and the pop from the pill (4.161.0, 2026-11-09); 17.50 KB: 17.28 KB with the full-screen phone Dock, the one-row header and its More menu (2026-11-09, owner: the pin unclear, pop-overs that did not close) + headroom; 16.80 KB: 16.55 KB with the phone header's two rows (4.150.0, 2026-11-08: the open tab read "Ac") + headroom; 16.40 KB: 16.30 KB with the link cards' loader (4.147.0, 2026-11-08; the card itself is dock-card.js, loaded the first time one shows) + headroom; 16.25 KB: 16.07 KB with OpenVibe Chat's hooks (share(), canSend(), the draft; 4.144.0, 2026-11-08), Send to… itself is dock-share.js (1.9 KB, loaded on first use); 16.0 KB: 15.74 KB with its siblings' ?v= content hashes (4.137.0, 2026-11-07) + headroom; 15.70 KB: 15.55 KB with Ask your agent (4.135.0, 2026-11-07) + headroom; was 15.50 KB (15435 after 4.120.0)
-const SIBLINGS = ['action-center.js', 'notification-ui.js', 'notification-live.js', 'ov-mark.js', 'dock-vibe.js', 'dock-mount.js', 'dock-runs.js', 'ai-marks-client.js', 'dock-timer.js', 'dock-agent.js', 'dock-share.js', 'dock-card.js'];
+const DOCK_BROTLI_BUDGET = 21300; // 4.203.0 adds 0.13 KB for the lazy DM edit loader and button; prior budget was 21100 bytes. // 20.61 KB with 4.202.0 page-end docking (#442, 2026-11-11); was 20.55 KB with the Code tab (ask 350, 2026-11-10: Chat · Code · Agents; the tab lives in dock-code.js, only its mount branch ships in dock.js); was 20940 with "Open in Chat" as an icon over your chats and in a thread (ask 336, 2026-11-10); 20.30 KB 20.30 KB with a robot for Agents, so the spark means one thing (Vibe) in the Dock as in the menu's row of places (4.194.0, 2026-11-10); 20.25 KB: 20.04 KB with a phone's sheet opening and closing as the menu's does (a circle from the orb, the menu's close button in the menu's place, a ring as it opens; 4.186.0, 2026-11-10); 20.00 KB: 19.80 KB with a phone's sheet opening out of the navbar's orb (4.185.0, 2026-11-10: the hand-off's styles and its two functions, about 0.95 KB; it runs as the sheet opens, so it cannot be a sibling loaded later); 19.00 KB: 18.63 KB with Dock v2's AI chats row, the conversation list, the group faces and the maker's marks (2026-11-09); 18.50 KB: 18.1 KB with the Actor tab, the sheets' shared surface and Dock chats from a site's session (4.162.0, 2026-11-09); 18.34 KB measured on 2026-11-09 with Dock v2's Chats summary and its lazy Agents rows; 18.00 KB: 17.9 KB with the orb's × as Close, dots drawn on the centre line and the pop from the pill (4.161.0, 2026-11-09); 17.50 KB: 17.28 KB with the full-screen phone Dock, the one-row header and its More menu (2026-11-09, owner: the pin unclear, pop-overs that did not close) + headroom; 16.80 KB: 16.55 KB with the phone header's two rows (4.150.0, 2026-11-08: the open tab read "Ac") + headroom; 16.40 KB: 16.30 KB with the link cards' loader (4.147.0, 2026-11-08; the card itself is dock-card.js, loaded the first time one shows) + headroom; 16.25 KB: 16.07 KB with OpenVibe Chat's hooks (share(), canSend(), the draft; 4.144.0, 2026-11-08), Send to… itself is dock-share.js (1.9 KB, loaded on first use); 16.0 KB: 15.74 KB with its siblings' ?v= content hashes (4.137.0, 2026-11-07) + headroom; 15.70 KB: 15.55 KB with Ask your agent (4.135.0, 2026-11-07) + headroom; was 15.50 KB (15435 after 4.120.0)
+const SIBLINGS = ['action-center.js', 'notification-ui.js', 'notification-live.js', 'ov-mark.js', 'dock-vibe.js', 'dock-mount.js', 'dock-runs.js', 'ai-marks-client.js', 'dock-timer.js', 'dock-agent.js', 'dock-share.js', 'dock-card.js', 'dock-chat-edits.js'];
// ── the budget ──────────────────────────────────────────────
// ── the budget ──────────────────────────────────────────────
{
{