openvibers/openvibe.shared Commits
4.240.0: one agent in the menu's Vibe, and a menu button drawn from the menu (#523)
Changes
@@@ -1,5 +1,32 @@# Changelog++## 4.240.0 — 2026-11-13++++**One agent in the menu's Vibe, and a menu button drawn from the menu** (the owner, 11-13: "all this separation of++tasks and codes and chats … we shouldn't even have that. It's just like filters … your actor is just your agent … it++can orchestrate to openvibe.codes, or timer, task, desktop, browser"; "I tried typing test in and just like nothing++happens … not seamless and sleek"; "regenerate this icon … based on what the menu actually looks like and what the++menu has to offer … good animations and effects … highlight differently and glow").++++- **The menu's Vibe is one agent (ov-run.js, search.js).**++ - Your agent heads the list (`o.agent`): its face with a state dot (ready, working, asks you, paused), its name,++ what it is doing and today's spend, and Pause or Resume. A tap shows its work. It reads the Frame's `/actor`.++ - All · Chats · Tasks · Code are filter chips over one list, never modes: the Dock's Chat, Agents and Code panels++ and its Actor card are no longer lent into the menu (only Now and the live view of an agent at work, the Dock's++ new `part: 'live'`).++ - One box starts everything ("Ask, or say what to do"); the Frame places the words (Network #911: Codes for a change++ to code, your computer for "on my computer", Vibe for the rest). The thread opens at once with your words and++ your agent typing until it answers; a run your agent handed on says where it went ("Sent to Codes"). A start that++ fails goes back to the list with the words in the box and why.++ - Now (timers, what is on) shows above the list on every filter; Ask your agent (`[data-ov-ask]`, the Dock's++ `ask()`) puts the words in the one box, unsent; words put in the box before the shared composer loads carry over.++- **The menu button (navbar.js)** is the menu, small: the ask bar you lead (your face, the words, Vibe's spark) over++ the place tabs, one lit. Now and then the light walks across the tabs; under the pointer the tabs lift and light,++ the spark turns and the mark glows; while an agent works the spark turns slowly (`.is-working`); open, the bar and++ the tabs fold into the ×. Calm and Off keep it still. Class names are M57's, so callers and tests are unchanged.++- Tests: `run-thread-chrome` (a new run opens with your words and the typing), `dock-chrome` (the menu's Vibe is one++ list with your agent; Tasks is a filter; the live view above it).++## 4.239.0 — 2026-11-13**One floating action, a composer that fits its column, a close button with depth** (the owner, 11-13: "this button@@@ -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.239.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed++**Status:** alpha, v4.240.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployedconsumer installs a tagged release and none keeps a vendored copy([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` namesthe tag it pins.@@@ -4,7 +4,7 @@"code": true,"wave": "W2","package": "openvibe-shared",-- "version": "4.239.0",++ "version": "4.240.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)",@@@ -271,9 +271,11 @@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' && part === 'top') {-- // 4.225.0: only the top is lent (the menu's Vibe draws the runs and the Action Center's work itself).-- panel.innerHTML = '<div class="ovdk-actor"></div><div class="ovdk-timeline"></div><div class="ovdk-agent"></div>';++ if (id === 'agent' && (part === 'top' || part === 'live')) {++ // 4.225.0: only the top is lent (the menu's Vibe draws the runs and the Action Center's work itself). 4.240.0,++ // part 'live': Now (timers, what is on) and the live view of an agent at work, without the Actor card (the++ // menu's Vibe draws your agent itself).++ panel.innerHTML = part === 'live' ? '<div class="ovdk-timeline"></div><div class="ovdk-agent"></div>' : '<div class="ovdk-actor"></div><div class="ovdk-timeline"></div><div class="ovdk-agent"></div>';agentTop(id, panel, epoch);mounts[id] = { refresh() { ['Timeline', 'Actor', 'View'].forEach((k) => { const m = mounts[id + k]; if (m && m.refresh) m.refresh(); }); } };return;@@@ -310,7 +312,7 @@load('dock-timeline.js', () => !!(root.OpenVibeDockTimeline && root.OpenVibeDockTimeline.mount)).then((on) => {if (on && epoch === panelEpoch && !mounts[id + 'Timeline'] && panel.isConnected && panelActive(id)) mounts[id + 'Timeline'] = root.OpenVibeDockTimeline.mount(panel.querySelector('.ovdk-timeline'), { token: o.token, base: directBase(o.actorBase), frameBase: frameBase(), active: () => panelActive('agent'), focus: () => o.task || null });});-- load('dock-actor.js', () => !!(root.OpenVibeDockActor && root.OpenVibeDockActor.mount)).then((on) => {++ if (panel.querySelector('.ovdk-actor')) load('dock-actor.js', () => !!(root.OpenVibeDockActor && root.OpenVibeDockActor.mount)).then((on) => {if (on && epoch === panelEpoch && !mounts[id + 'Actor'] && panel.isConnected && panelActive(id)) mounts[id + 'Actor'] = root.OpenVibeDockActor.mount(panel.querySelector('.ovdk-actor'), { token: o.token, base: directBase(o.actorBase), frameBase: frameBase(), networkBase: o.apiBase || 'https://openvibe.network', active: () => panelActive('agent') });});load('dock-agent.js', () => !!(root.OpenVibeDockAgent && root.OpenVibeDockAgent.mount)).then((on) => {@@@ -689,6 +691,14 @@// ── Open: the menu, on one of its places ──// Ask your agent (4.135.0): Vibe opens with the question in its box, unsent.function ask({ text = '' } = {}, n = 0) {++ // 4.240.0: the menu's Vibe is one agent with one box (ov-run.js): the words wait there, unsent.++ const S = root.OpenVibeSearch;++ if (S && typeof S.prefill === 'function') {++ if (!n) open({ tab: 'vibe' });++ if (S.prefill(text)) return;++ if (n < 40) setTimeout(() => ask({ text }, n + 1), 100);++ return;++ }if (!n && !panelActive('vibe')) open({ tab: 'vibe' }); if (vibe.api && vibe.api.ask && panelActive('vibe')) vibe.api.ask(text); else if (n < 40) setTimeout(() => ask({ text }, n + 1), 100); }function isOpen() { return !!inline; }function share({ text = '', title = '' } = {}) {@@@ -735,7 +745,7 @@mountHandle = handle;load('dock-mount.js', () => !!root.OpenVibeDockMount).then((ok) => {if (ok && !gone) api = root.OpenVibeDockMount.mount(host, wanted, {-- start(wrap) { if (!bodyEl) build(); part = lend === 'top' ? 'top' : null; wrap.classList.toggle('ovdk-part-top', part === 'top'); inline = wrap; wrap.append(bodyEl); },++ start(wrap) { if (!bodyEl) build(); part = lend === 'top' || lend === 'live' ? lend : null; wrap.classList.toggle('ovdk-part-top', !!part); inline = wrap; wrap.append(bodyEl); },// A lent top is not a place the person chose: the menu's Vibe remembers its own (remember()).show(id) { setTab(id, part !== 'top'); },end() { disposePanel(); inline = null; part = null; if (bodyEl) bodyEl.remove(); paintStatus(); },@@@ -318,35 +318,50 @@.openvibe-navbar .ovnav-orb::before { content: ""; position: absolute; inset: -3px; border-radius: 50%; background: radial-gradient(closest-side, color-mix(in srgb, var(--accent, #3b82f6) 38%, transparent), transparent); opacity: 0; transform: scale(.5); transition: opacity .25s, transform .4s cubic-bezier(.2,1.4,.3,1); pointer-events: none; }.openvibe-navbar .ovnav-orb:hover::before, .openvibe-navbar .ovnav-orb:focus-visible::before, .openvibe-navbar .ovnav-orb[aria-expanded="true"]::before { opacity: 1; transform: none; }.openvibe-navbar .ovnav-orb svg { position: relative; display: block; overflow: visible; }-- .ovmb { display: block; overflow: visible; }-- .ovmb-bar { fill: transparent; stroke: currentColor; stroke-width: 1.9; }-- .ovmb-app { fill: currentColor; }-- .ovmb-dot, .ovmb-a1 { fill: var(--ovo-a, var(--accent-light, #60a5fa)); }-- .ovmb :is(.ovmb-bar, .ovmb-dot, .ovmb-app) { transform-box: fill-box; transform-origin: center; transition: transform .5s cubic-bezier(.2,1.3,.3,1), fill .3s, stroke-width .3s, opacity .25s; }++ .ovmb { display: block; overflow: visible; transition: filter .3s; }++ .ovmb-bar { fill: transparent; stroke: currentColor; stroke-width: 1.7; }++ .ovmb-dot { fill: currentColor; }++ .ovmb-spark { fill: var(--ovo-b, #e879f9); }++ .ovmb-words { fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; opacity: .55; }++ .ovmb-app { fill: currentColor; opacity: .42; }++ .ovmb-lit { fill: var(--ovo-a, var(--accent-light, #60a5fa)); }++ .ovmb :is(.ovmb-bar, .ovmb-dot, .ovmb-words, .ovmb-spark, .ovmb-app, .ovmb-lit) { transform-box: fill-box; transform-origin: center; transition: transform .5s cubic-bezier(.2,1.3,.3,1), fill .3s, stroke-width .3s, opacity .25s; }.ovmb-a0 { transition-delay: .03s; } .ovmb-a1 { transition-delay: .06s; } .ovmb-a2 { transition-delay: .09s; }-- /* Under the pointer (and once, the first time it is seen): the bar and the three apps lift in turn. */-- :is(.ovnav-orb, .ovs-orb):is(:hover, .ovo-intro) .ovmb:not(.is-x) :is(.ovmb-bar, .ovmb-dot, .ovmb-app) { animation: ovmb-wave .7s ease-in-out both; }-- .ovmb .ovmb-a0 { animation-delay: .08s; } .ovmb .ovmb-a1 { animation-delay: .15s; } .ovmb .ovmb-a2 { animation-delay: .22s; }-- :is(.ovnav-orb, .ovs-orb):is(:hover, :focus-visible) .ovmb-app { fill: var(--ovo-a, var(--accent-light, #60a5fa)); }-- :is(.ovnav-orb, .ovs-orb):is(:hover, :focus-visible) .ovmb-a1 { fill: var(--ovo-b, #e879f9); }-- /* While the menu loads: the apps breathe in turn. */++ /* Under the pointer (and once, the first time it is seen): the tabs lift in turn and light up, the spark++ turns, your face brightens, and the mark glows in the accent. */++ :is(.ovnav-orb, .ovs-orb):is(:hover, .ovo-intro) .ovmb:not(.is-x) :is(.ovmb-bar, .ovmb-app, .ovmb-lit) { animation: ovmb-wave .7s ease-in-out both; }++ .ovmb .ovmb-a0, .ovmb .ovmb-lit { animation-delay: .08s; } .ovmb .ovmb-a1 { animation-delay: .15s; } .ovmb .ovmb-a2 { animation-delay: .22s; }++ :is(.ovnav-orb, .ovs-orb):is(:hover, .ovo-intro) .ovmb:not(.is-x) .ovmb-spark { animation: ovmb-twinkle .8s cubic-bezier(.3,1.4,.4,1) both; }++ :is(.ovnav-orb, .ovs-orb):is(:hover, :focus-visible) .ovmb:not(.is-x) { filter: drop-shadow(0 0 3px color-mix(in srgb, var(--ovo-a, #60a5fa) 70%, transparent)); }++ :is(.ovnav-orb, .ovs-orb):is(:hover, :focus-visible) .ovmb-app { fill: var(--ovo-a, var(--accent-light, #60a5fa)); opacity: .75; }++ :is(.ovnav-orb, .ovs-orb):is(:hover, :focus-visible) .ovmb-dot { fill: var(--text-primary, #e6edf7); }++ :is(.ovnav-orb, .ovs-orb):is(:hover, :focus-visible) .ovmb-words { opacity: .9; transform: scaleX(1.25); }++ /* Something runs for you (an agent working, set by the menu's stack): the spark turns slowly. */++ .ovnav-orb.is-working .ovmb:not(.is-x) .ovmb-spark { animation: ovmb-spin 2.4s linear infinite; }++ /* While the menu loads: the tabs breathe in turn. */.ovnav-orb.is-busy .ovmb-app { animation: ovmb-busy .9s ease-in-out infinite; }-- /* Open: the bar is one stroke of the close mark, the three apps the other. */-- :is(.ovnav-orb[aria-expanded="true"], .ovs-orb.is-x) .ovmb :is(.ovmb-bar, .ovmb-dot, .ovmb-app), .ovmb.is-x :is(.ovmb-bar, .ovmb-dot, .ovmb-app) { animation: none; }-- :is(.ovnav-orb[aria-expanded="true"], .ovs-orb.is-x) .ovmb .ovmb-bar, .ovmb.is-x .ovmb-bar { transform: translateY(4px) rotate(45deg) scale(.9, .43); fill: var(--ovo-a, var(--accent-light, #60a5fa)); stroke-width: 0; }-- :is(.ovnav-orb[aria-expanded="true"], .ovs-orb.is-x) .ovmb .ovmb-dot, .ovmb.is-x .ovmb-dot { transform: translate(4.6px, 4px) scale(0); opacity: 0; }-- :is(.ovnav-orb[aria-expanded="true"], .ovs-orb.is-x) .ovmb .ovmb-app, .ovmb.is-x .ovmb-app { fill: var(--ovo-a, var(--accent-light, #60a5fa)); }-- :is(.ovnav-orb[aria-expanded="true"], .ovs-orb.is-x) .ovmb .ovmb-a0, .ovmb.is-x .ovmb-a0 { transform: translate(2.4px, -1.2px) rotate(-45deg) scale(1.3, .6); }-- :is(.ovnav-orb[aria-expanded="true"], .ovs-orb.is-x) .ovmb .ovmb-a1, .ovmb.is-x .ovmb-a1 { transform: translate(0, -4.6px) rotate(-45deg) scale(1.3, .6); }-- :is(.ovnav-orb[aria-expanded="true"], .ovs-orb.is-x) .ovmb .ovmb-a2, .ovmb.is-x .ovmb-a2 { transform: translate(-2.4px, -8px) rotate(-45deg) scale(1.3, .6); }++ /* Open: the bar is one stroke of the close mark, the three tabs the other; your face and the spark go. */++ :is(.ovnav-orb[aria-expanded="true"], .ovs-orb.is-x) .ovmb :is(.ovmb-bar, .ovmb-dot, .ovmb-words, .ovmb-spark, .ovmb-app, .ovmb-lit), .ovmb.is-x :is(.ovmb-bar, .ovmb-dot, .ovmb-words, .ovmb-spark, .ovmb-app, .ovmb-lit) { animation: none; }++ :is(.ovnav-orb[aria-expanded="true"], .ovs-orb.is-x) .ovmb .ovmb-bar, .ovmb.is-x .ovmb-bar { transform: translateY(4.1px) rotate(45deg) scale(.92, .38); fill: var(--ovo-a, var(--accent-light, #60a5fa)); stroke-width: 0; }++ :is(.ovnav-orb[aria-expanded="true"], .ovs-orb.is-x) .ovmb :is(.ovmb-dot, .ovmb-words, .ovmb-spark), .ovmb.is-x :is(.ovmb-dot, .ovmb-words, .ovmb-spark) { transform: scale(0); opacity: 0; }++ :is(.ovnav-orb[aria-expanded="true"], .ovs-orb.is-x) .ovmb .ovmb-app, .ovmb.is-x .ovmb-app { fill: var(--ovo-a, var(--accent-light, #60a5fa)); opacity: 1; }++ :is(.ovnav-orb[aria-expanded="true"], .ovs-orb.is-x) .ovmb .ovmb-a0, .ovmb.is-x .ovmb-a0 { transform: translate(2.4px, -1.2px) rotate(-45deg) scale(1.12, .55); }++ :is(.ovnav-orb[aria-expanded="true"], .ovs-orb.is-x) .ovmb .ovmb-a1, .ovmb.is-x .ovmb-a1 { transform: translate(0, -5.2px) rotate(-45deg) scale(1.12, .55); }++ :is(.ovnav-orb[aria-expanded="true"], .ovs-orb.is-x) .ovmb .ovmb-a2, .ovmb.is-x .ovmb-a2 { transform: translate(-2.4px, -9.2px) rotate(-45deg) scale(1.12, .55); }++ :is(.ovnav-orb[aria-expanded="true"], .ovs-orb.is-x) .ovmb .ovmb-lit, .ovmb.is-x .ovmb-lit { transform: translate(2.4px, -1.2px) rotate(-45deg) scale(1.12, .55); opacity: 0; }/* A close mark that arrives already made (the Dock's sheet) forms from the button it came from. */.ovmb.is-forming :is(.ovmb-bar, .ovmb-app) { animation: ovmb-form .5s cubic-bezier(.2,1.3,.3,1) both; }@keyframes ovmb-form { from { transform: none; } }-- .ovnav-orb:not([aria-expanded="true"]) .ovmb:not(.is-x) .ovmb-app { animation: ovmb-idle 9s ease-in-out 3s infinite; }-- .ovnav-orb .ovmb .ovmb-a1 { animation-delay: 3.09s !important; } .ovnav-orb .ovmb .ovmb-a2 { animation-delay: 3.18s !important; }-- @keyframes ovmb-idle { 0%, 4%, 100% { transform: none; } 2% { transform: translateY(calc(-2.2px * var(--ov-move, 1))); } }-- @keyframes ovmb-wave { 0%, 100% { transform: translateY(0); } 40% { transform: translateY(calc(-2.6px * var(--ov-move, 1))); } }-- @keyframes ovmb-busy { 0%, 100% { transform: scale(.72); opacity: .5; } 50% { transform: scale(1.18); opacity: 1; } }++ /* At rest, every 9 s: the light walks across the places and comes home (Calm and Off leave it still). */++ .ovnav-orb:not([aria-expanded="true"]):not(:hover) .ovmb:not(.is-x) .ovmb-lit { animation: ovmb-walk 9s cubic-bezier(.5,0,.3,1) 3s infinite; }++ @keyframes ovmb-walk { 0%, 62%, 100% { transform: none; } 70%, 74% { transform: translateX(6.4px); } 82%, 86% { transform: translateX(12.8px); } 94% { transform: translateX(0); } }++ @keyframes ovmb-wave { 0%, 100% { transform: translateY(0); } 40% { transform: translateY(calc(-2.4px * var(--ov-move, 1))); } }++ @keyframes ovmb-twinkle { 0% { transform: none; } 50% { transform: rotate(45deg) scale(1.45); } 100% { transform: rotate(90deg); } }++ @keyframes ovmb-spin { to { transform: rotate(360deg); } }++ @keyframes ovmb-busy { 0%, 100% { transform: scale(.72); opacity: .35; } 50% { transform: scale(1.12); opacity: .9; } }++ html[data-ov-motion=reduced] .ovmb :is(.ovmb-lit, .ovmb-spark, .ovmb-app) { animation-duration: 18s; }++ html[data-ov-motion=reduced] :is(.ovnav-orb, .ovs-orb):is(:hover, .ovo-intro) .ovmb :is(.ovmb-bar, .ovmb-app, .ovmb-lit, .ovmb-spark) { animation-duration: 1.4s; }++ html[data-ov-motion=off] .ovmb :is(.ovmb-bar, .ovmb-dot, .ovmb-words, .ovmb-spark, .ovmb-app, .ovmb-lit) { animation: none !important; transition: none; }.openvibe-navbar .ovnav-orb[aria-expanded="true"] { color: var(--text-primary, #e6edf7); }.openvibe-navbar :is(.ovnav-orb, .openvibe-bell, .openvibe-navbar-avatar) { transition: transform .18s cubic-bezier(.2, 1.4, .3, 1), background-color .15s, color .15s; }.openvibe-navbar :is(.ovnav-orb, .openvibe-bell, .openvibe-navbar-avatar):active { transform: scale(.9); }@@@ -609,7 +624,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': 'f490466f57df', 'ov-glyphs.js': '5fa4615e0c7a', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': '2f6cab5e0778', 'ov-fx.js': '947820d69097', 'ov-labels.js': 'd40f7ca14bc4', 'ov-forms.js': '2eb9e2604c3e', 'ov-forms.css': '07e8f59d9e6d', 'ov-share.js': 'd419508a797e', 'ov-share.css': 'd609a71f611a', 'search.js': '228a974d7922', 'dock.js': '6adcf85f7bd8', 'presence.js': '5f195d41f8c5', 'panels.js': 'd90bfe987d81', 'surfaces.js': '3e6f2784c85f', 'notification-ui.js': 'c993615033ae', 'history.js': 'f9fb9e096c65', 'bottom.js': '1671b6949137', 'appbar.js': 'b2a16f53eae0', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': '8701ebc6f8a3', 'sso-client.js': 'f574ac3331a5', 'overlay.js': '2bc56b3cf726', 'ov-home.js': '4e7d405117de', 'nav-slots.js': '7b11e6916732', 'ov-wallet.js': 'f1a2d6774087', 'ov-wallet.css': '4923c808c9ad', 'purse.js': '9d3fea0dae02', 'ov-drop.js': '445689a5eb80', 'ov-tip.js': '1d918e7de69d', 'ov-arrange.js': 'ae24086bd2be', 'soft.js': '98ab9b8080b1', 'ov-run.js': '504bfeabcd49', 'ov-composer.js': '79b34aff4452' };++ const SIBLING_REV = { 'ov-mark.js': 'f490466f57df', 'ov-glyphs.js': '5fa4615e0c7a', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': '2f6cab5e0778', 'ov-fx.js': '947820d69097', 'ov-labels.js': 'd40f7ca14bc4', 'ov-forms.js': '2eb9e2604c3e', 'ov-forms.css': '07e8f59d9e6d', 'ov-share.js': 'd419508a797e', 'ov-share.css': 'd609a71f611a', 'search.js': '73c7387a6ec5', 'dock.js': '8de9086a2cfd', 'presence.js': '5f195d41f8c5', 'panels.js': 'd90bfe987d81', 'surfaces.js': '3e6f2784c85f', 'notification-ui.js': 'c993615033ae', 'history.js': 'f9fb9e096c65', 'bottom.js': '1671b6949137', 'appbar.js': 'b2a16f53eae0', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': '8701ebc6f8a3', 'sso-client.js': 'f574ac3331a5', 'overlay.js': '2bc56b3cf726', 'ov-home.js': '4e7d405117de', 'nav-slots.js': '7b11e6916732', 'ov-wallet.js': 'f1a2d6774087', 'ov-wallet.css': '4923c808c9ad', 'purse.js': '9d3fea0dae02', 'ov-drop.js': '445689a5eb80', 'ov-tip.js': '1d918e7de69d', 'ov-arrange.js': 'ae24086bd2be', 'soft.js': '98ab9b8080b1', 'ov-run.js': '34f19495bc12', 'ov-composer.js': '79b34aff4452' };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.@@@ -1015,12 +1030,19 @@try { const sc = document.createElement('script'); sc.id = 'ov-slots-loader'; sc.src = siblingV('nav-slots.js'); sc.async = true; sc.onload = () => paintSlots(); document.head.appendChild(sc); } catch { /* */ }}-- // The OpenVibe menu's button (the owner's pick, 2026-11-10: the Arena's M57, "Search and apps"): the menu itself,-- // small: a search bar over three apps. Open, the bar turns into one stroke of the close mark and the three apps-- // line up as the other, so the button that opens the menu is, in the same place, the one that closes it. One-- // drawing for the bar, the menu's sheet and the Dock's (root.OpenVibeOrb); its styles are below (.ovmb).-- const ORB_PARTS = '<rect class="ovmb-bar" x="4" y="5.2" width="16" height="5.6" rx="2.8"/><circle class="ovmb-dot" cx="7.4" cy="8" r="1.15"/>'-- + '<rect class="ovmb-app ovmb-a0" x="4.2" y="14.6" width="4" height="4" rx="1.3"/><rect class="ovmb-app ovmb-a1" x="10" y="14.6" width="4" height="4" rx="1.3"/><rect class="ovmb-app ovmb-a2" x="15.8" y="14.6" width="4" height="4" rx="1.3"/>';++ // The OpenVibe menu's button, 4.240.0 (the owner, 11-13: "regenerate based on what the menu actually looks like and++ // what the menu has to offer"): the menu itself, small. On top, the ask bar you lead: your face at its left, Vibe's++ // spark at its right. Under it, the place tabs, the one you are on lit. Now and then the light walks across the tabs++ // (the menu's places); under the pointer the tabs lift in turn, the spark turns and the whole mark glows. Open, the++ // bar is one stroke of the close mark and the tabs line up as the other, so the button that opens the menu is, in++ // the same place, the one that closes it (rule opens-it-closes-it). One drawing for the bar, the menu's sheet and++ // the Dock's (root.OpenVibeOrb); its styles are above (.ovmb). Class names kept from M57 (.ovmb-bar, .ovmb-dot for++ // the face, .ovmb-app .ovmb-a0..2 for the tabs) so every caller and test still finds its parts.++ const ORB_PARTS = '<rect class="ovmb-bar" x="3.2" y="4.3" width="17.6" height="7.2" rx="3.6"/>'++ + '<circle class="ovmb-dot" cx="6.9" cy="7.9" r="1.5"/><path class="ovmb-words" d="M10 7.9h3.4"/>'++ + '<path class="ovmb-spark" d="M16.6 5.75l.62 1.53 1.53.62-1.53.62-.62 1.53-.62-1.53-1.53-.62 1.53-.62z"/>'++ + '<rect class="ovmb-app ovmb-a0" x="3" y="14.7" width="5.2" height="5" rx="1.6"/><rect class="ovmb-app ovmb-a1" x="9.4" y="14.7" width="5.2" height="5" rx="1.6"/><rect class="ovmb-app ovmb-a2" x="15.8" y="14.7" width="5.2" height="5" rx="1.6"/>'++ + '<rect class="ovmb-lit" x="3" y="14.7" width="5.2" height="5" rx="1.6"/>';function orbSVG(size = 22, open = false) {return `<svg class="ovmb${open ? ' is-x' : ''}" viewBox="0 0 24 24" width="${size}" height="${size}" aria-hidden="true">${ORB_PARTS}</svg>`;}@@@ -33,6 +33,15 @@* OpenVibeRun.card(type, (data, step, ctx) => Node) a rich card for a step's `ui.type` (a site or a mod adds its* own); an unknown type shows the step's words.*++ * 4.240.0, one agent (the owner, 11-13: "all this separation of tasks and codes and chats … we shouldn't even have++ * that. It's just like filters … your actor is just your agent … it can orchestrate to openvibe.codes, or timer, task,++ * desktop, browser"):++ * o.agent: true | { name } your agent heads the list: its face, its state and today's spend (the Frame's++ * /actor, Actor's GET /api/v1/me/actor), Pause or Resume; a tap on it shows its work++ * the filters are chips over one list, never modes; one box starts everything (no kind: the Frame hands the words to++ * chat, Codes, a task or a machine), and the thread opens at once with your words and your agent typing. A run your++ * agent handed on says where it went ("Sent to Codes").++ ** The list: needs-you first, then newest, filtered All · Chats · Tasks · Code; each run is its agent's face (glyph and* hue, or its picture, with what it is doing), its title, its last line and when. The thread: your words on the right,* the agent's on the left (paragraphs, lists, links, code, numbered sources), thinking folded, each tool one quiet row,@@@ -268,6 +277,61 @@} catch { return []; }}const state = { view: 'list', filter: FILTER_IDS.includes(o.filter) ? o.filter : 'all', runs: [], run: null, steps: new Map(), stopStream: null, abort: null, gen: 0 };++ // Your agent (4.240.0): one, whatever it is doing. The Frame relays Actor's /api/v1/me/actor (every site's++ // connect-src allows openvibe.network); its name and picture are the person's choice, "Vibe" until they choose.++ let agent = null, agentAt = 0;++ const agentName = () => String((agent && agent.name) || (o.agent && o.agent.name) || 'Vibe').slice(0, 40);++ const agentState = () => { const a = agent; if (!a) return 'resting'; if (a.status === 'paused' || a.enabled === false) return 'paused'; if (Array.isArray(a.asks) && a.asks.length) return 'asks'; return a.status === 'working' ? 'working' : 'resting'; };++ function agentFace(size, st) {++ const f = el('span', `ovr-face is-${size} ovr-me is-${st || agentState()}`);++ f.style.setProperty('--ovr-h', String(hueOf(agentName())));++ const pic = agent && agent.avatar && httpsUrl(agent.avatar.url);++ if (pic) { const i = el('img'); i.src = pic; i.alt = ''; f.append(i); } else f.append(svg('spark'));++ return f;++ }++ async function loadAgent() {++ if (!o.agent || !tokenOf()) return null;++ if (agentAt && Date.now() - agentAt < 20000) return agent;++ try { const r = await fetch(`${base}/actor`, { credentials: 'omit', headers: { Accept: 'application/json', ...headers() } }); agent = r.ok ? await r.json() : null; }++ catch { agent = null; }++ agentAt = Date.now();++ return agent;++ }++ const money = (v) => `$${(Number(v) || 0).toFixed(2)}`;++ const AGENT_WORD = { working: 'Working', asks: 'Asks you something', resting: 'Ready', paused: 'Paused' };++ /** The list's head: your agent, what it is doing, and the one action that fits. */++ function paintAgent(slot) {++ slot.textContent = '';++ if (!o.agent || !tokenOf()) { slot.hidden = true; return; }++ slot.hidden = false;++ const st = agentState();++ const card = el('div', `ovr-agent is-${st}`);++ const who = el('button', 'ovr-agent-who'); who.type = 'button';++ who.setAttribute('aria-label', `${agentName()}, your agent: ${AGENT_WORD[st]}. Show its work`);++ const now = (agent && agent.now) || {};++ const step = st === 'working' ? String(now.current_step || '').slice(0, 120) : st === 'asks' ? String((agent.asks[0] && agent.asks[0].text) || '').slice(0, 120) : '';++ const cap = agent && agent.caps && Number(agent.caps.usd) > 0 ? ` of ${money(agent.caps.usd)}` : '';++ const t = el('span', 'ovr-agent-t');++ const top = el('span', 'ovr-agent-top'); top.append(el('b', '', agentName()), el('i', 'ovr-agent-tag', 'your agent'));++ t.append(top, el('span', 'ovr-agent-line', [AGENT_WORD[st], step, agent ? `${money(now.cost_today)}${cap} today` : ''].filter(Boolean).join(' · ')));++ who.append(agentFace('m', st), t);++ who.addEventListener('click', () => list('task'));++ card.append(who);++ if (agent && agent.id) {++ const paused = st === 'paused';++ const go = el('button', `ovr-agent-go${paused ? ' is-main' : ''}`); go.type = 'button';++ go.setAttribute('aria-label', paused ? `Resume ${agentName()}` : `Pause ${agentName()}`); go.title = paused ? 'Resume' : 'Pause';++ go.innerHTML = paused ? '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5.5v13l10.5-6.5z" fill="currentColor"/></svg>' : '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8.5 5.5v13M15.5 5.5v13"/></svg>';++ go.addEventListener('click', async () => {++ go.disabled = true;++ try { const r = await fetch(`${base}/actor/${paused ? 'resume' : 'pause'}`, { method: 'POST', credentials: 'omit', headers: { Accept: 'application/json', 'Content-Type': 'application/json', ...headers() }, body: '{}' }); if (r.ok) { agentAt = 0; await loadAgent(); } }++ catch { /* the row stays as it was */ }++ if (slot.isConnected) paintAgent(slot);++ });++ card.append(go);++ }++ slot.append(card);++ }function clearStream() { if (state.stopStream) state.stopStream(); state.stopStream = null; if (state.abort) state.abort.abort(); state.abort = null; }@@@ -302,16 +366,47 @@let sc = document.getElementById('ov-composer-script');if (!sc) { sc = document.createElement('script'); sc.id = 'ov-composer-script'; sc.async = true; sc.src = o.sibling('ov-composer.js'); document.head.appendChild(sc); }plain();-- sc.addEventListener('load', () => { if (slot.isConnected && root.OpenVibeComposer) { slot.textContent = ''; real(); } }, { once: true });++ sc.addEventListener('load', () => { if (slot.isConnected && root.OpenVibeComposer) { const was = (slot.querySelector('textarea') || {}).value || ''; slot.textContent = ''; real(); if (was) { const ta = slot.querySelector('textarea'); if (ta) { ta.value = was; ta.dispatchEvent(new Event('input', { bubbles: true })); } } } }, { once: true }); // words put in the plain box (Ask your agent) carry over} else plain();}/** A new run with these words (Vibe answers, or hands the work on), then its thread. */async function start(text) {-- const run = await api('', { method: 'POST', headers: { 'Idempotency-Key': `ovr-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}` }, body: JSON.stringify({ ...(o.start || {}), text: String(text || '').slice(0, 16000) }) });-- open(run.ref || run.id, run);++ const words = String(text || '').slice(0, 16000);++ const gen0 = pending(words);++ let run;++ try { run = await api('', { method: 'POST', headers: { 'Idempotency-Key': `ovr-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}` }, body: JSON.stringify({ ...(o.start || {}), text: words }) }); }++ catch (err) {++ // It did not start: back to the list, the words back in the box, and why, in plain words.++ if (gen0 !== state.gen) return null;++ await list();++ const foot = box.querySelector('.ovr-list-note'); if (foot) foot.prepend(el('p', 'ovr-note is-err', err.status === 401 ? 'Sign in to talk to your agent.' : (err && err.message) || 'That did not start. Try again.'));++ const ta = box.querySelector('.ovr-compose textarea'); if (ta && !ta.value) { ta.value = words; ta.dispatchEvent(new Event('input', { bubbles: true })); ta.focus(); }++ return null;++ }++ if (gen0 !== state.gen) return run; // the person went elsewhere while it started++ open(run.ref || run.id, run, { said: words });return run;}++ /** The thread before the run exists: your words on the right, your agent typing on the left. */++ function pending(words) {++ clearStream(); dropTop(); dropPane();++ const gen = ++state.gen;++ state.view = 'thread'; state.run = null; state.steps = new Map();++ box.textContent = ''; box.className = 'ovr is-thread is-pending';++ const head = el('div', 'ovr-thead');++ const back = el('button', 'ovr-ib'); back.type = 'button'; back.setAttribute('aria-label', 'Back to your runs'); back.append(svg('back'));++ back.addEventListener('click', () => list());++ const who = el('div', 'ovr-who'); who.append(el('b', '', agentName()), el('small', '', 'Starting'));++ head.append(back, agentFace('s', 'working'), who);++ const thread = el('div', 'ovr-thread'); thread.setAttribute('role', 'log'); thread.setAttribute('aria-live', 'polite');++ const me = el('div', 'ovr-step is-you'); const b = el('div', 'ovr-bubble'); b.append(document.createTextNode(words)); me.append(b);++ thread.append(me, typing());++ box.append(head, thread);++ return gen;++ }++ function typing() { const t = el('div', 'ovr-typing'); t.setAttribute('aria-label', `${agentName()} is writing`); t.append(el('i'), el('i'), el('i')); return t; }++// ── the list ──async function list(filter) {@@@ -337,6 +432,8 @@e.preventDefault(); next.focus(); next.click();});head.append(tabs);++ const me = el('div', 'ovr-agent-slot'); me.hidden = true;++ if (o.agent) { box.append(me); paintAgent(me); loadAgent().then(() => { if (me.isConnected) paintAgent(me); }); }const rows = el('ul', 'ovr-rows'); rows.setAttribute('aria-label', 'Your runs');const foot = el('div', 'ovr-list-note');const top = el('div', 'ovr-top');@@@ -353,7 +450,7 @@});box.append(st);}-- composer(box, { placeholder: o.placeholder || 'Ask, or say what to make', onSend: (v) => start(v.text) });++ composer(box, { placeholder: o.placeholder || (o.agent ? 'Ask, or say what to do' : 'Ask, or say what to make'), onSend: (v) => start(v.text) });}place({ filter: state.filter });box.querySelectorAll('.ovr-filter').forEach((b) => { const on = b.dataset.filter === state.filter; b.setAttribute('aria-selected', String(on)); b.tabIndex = on ? 0 : -1; });@@@ -361,7 +458,7 @@// pieces may share one owner (the Dock lends one panel at a time), so the old one goes before the new one comes.const pf = paneFor(state.filter);const topEl = box.querySelector('.ovr-top'), paneEl = box.querySelector('.ovr-pane');-- const wantTop = !pf && ['all', 'task'].includes(state.filter) && typeof o.top === 'function';++ const wantTop = !pf && typeof o.top === 'function'; // 4.240.0: Now (timers) and an agent at work show on every filterif (!wantTop) { dropTop(); topEl.textContent = ''; }if (paneId !== (pf ? state.filter : null)) { dropPane(); paneEl.textContent = ''; }box.classList.toggle('has-pane', !!pf);@@@ -394,7 +491,7 @@state.runs = state.runs.concat(extra).sort((x, y) => needs(y) - needs(x) || String(y.updated_at || '').localeCompare(String(x.updated_at || '')));}rows.textContent = ''; rows.removeAttribute('aria-busy');-- if (!state.runs.length) foot.append(empty('Nothing here yet', state.filter === 'code' ? 'Say what to build below: a page, a fix, controls for your stream.' : 'Ask anything below. Chats, tasks and code you start show up here.'));++ if (!state.runs.length) foot.append(empty('Nothing here yet', state.filter === 'code' ? 'Say what to build below: a page, a fix, controls for your stream. Your agent hands it to Codes.' : o.agent ? `Ask ${agentName()} anything below. It answers, sets timers and reminders, and hands coding and computer work on.` : 'Ask anything below. Chats, tasks and code you start show up here.'));state.runs.forEach((r, i) => { const li = el('li'); li.append(row(r, i)); const acts = answerRow(r); if (acts) { li.classList.add('has-acts'); li.append(acts); } rows.append(li); });if (Array.isArray(out.partial) && out.partial.length) foot.append(el('p', 'ovr-note', `Some of your runs did not load (${out.partial.join(', ')}).`));}@@@ -444,7 +541,7 @@function empty(title, sub) { const e = el('div', 'ovr-empty'); e.append(svg('spark')); const w = el('div'); w.append(el('b', '', title), el('span', '', sub)); e.append(w); return e; }// ── the thread ──-- async function open(ref, known) {++ async function open(ref, known, how = {}) {clearStream();dropTop(); dropPane();const gen = ++state.gen;@@@ -470,9 +567,17 @@box.classList.toggle('is-needs', needsPerson(r));};if (known) paintHead(known);++ // A run your agent handed on (it started from the one box and is not a chat) says where it went.++ const WENT = { code: ['code', 'Sent to Codes: it works on the code and shows you the change'], task: ['robot', `Sent to ${agentName()}'s tasks: it works on it and asks before anything that matters`], machine: ['tool', 'Sent to your computer: it runs there and shows you each step'], goal: ['spark', 'A goal: your agent plans the steps'] };++ if (how.said && known && WENT[known.kind]) { const w = el('p', 'ovr-went'); w.append(svg(WENT[known.kind][0]), el('span', '', WENT[known.kind][1])); thread.append(w); }++ let wait = null;++ if (how.said) { const me = el('div', 'ovr-step is-you is-said'); const b = el('div', 'ovr-bubble'); b.append(document.createTextNode(how.said)); me.append(b); thread.append(me); wait = typing(); thread.append(wait); }composer(box, { placeholder: 'Say the next thing', onSend: async (v) => { const step = await api(`/${encodeURIComponent(ref)}/messages`, { method: 'POST', body: JSON.stringify({ text: v.text }) }); add(step); } });const add = (s) => {if (!s || !Number.isInteger(s.seq)) return;++ // Your words came back as a step: the stand-in goes. Your agent's first words: the typing goes.++ if (s.kind === 'message' && (s.from === 'person') && how.said) { const said = thread.querySelector(':scope > .is-said'); if (said) said.remove(); }++ if (wait && !(s.kind === 'message' && s.from === 'person')) { wait.remove(); wait = null; }if (state.steps.has(s.seq)) { const old = state.steps.get(s.seq); const n = stepNode(s); old.replaceWith(n); state.steps.set(s.seq, n); return; }const wasNear = thread.scrollHeight - thread.scrollTop - thread.clientHeight < 160;const none = thread.querySelector(':scope > .ovr-none'); if (none) none.remove();@@@ -488,7 +593,8 @@paintHead(r);(page.steps || []).forEach(add);requestAnimationFrame(() => { thread.scrollTop = thread.scrollHeight; });-- if (!(page.steps || []).length) thread.append(el('p', 'ovr-note ovr-none', 'Nothing here yet.'));++ if (!(page.steps || []).length && !how.said) thread.append(el('p', 'ovr-note ovr-none', 'Nothing here yet.'));++ if (wait) thread.append(wait); // still writing: the dots stay last} catch (err) { if (gen !== state.gen) return; thread.append(empty('This run did not load', err.message)); return; }if (isEnd(state.run)) return;const last = Math.max(0, ...state.steps.keys());@@@ -598,7 +704,8 @@.ovr { --ovr-gap: 12px; display: flex; flex-direction: column; min-height: 0; height: 100%; font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: var(--text-primary, #e6edf7); }.ovr svg { width: 18px; height: 18px; flex: none; fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }.ovr-head { padding: 4px 4px 8px; }--.ovr-filters { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 4px; padding: 4px; border-radius: 16px; background: color-mix(in srgb, var(--text-primary, #e6edf7) 5%, transparent); }++.ovr-filters { display: flex; gap: 6px; overflow-x: auto; padding: 2px 2px 4px; scrollbar-width: none; }++.ovr-filters::-webkit-scrollbar { display: none; }.ovr-filter { all: unset; box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; gap: 7px; min-height: 42px; border-radius: 12px; cursor: pointer; font-family: 'OV Brand', Nunito, Inter, sans-serif; font-weight: 800; font-size: 14px; color: var(--text-secondary, #a8b3c4); transition: background-color .18s, color .18s; }.ovr-filter[aria-selected="true"] { color: var(--text-primary, #e6edf7); background: var(--bg-secondary, #121826); box-shadow: 0 1px 2px rgba(0,0,0,.3), inset 0 0 0 1px color-mix(in srgb, var(--accent, #3b82f6) 35%, transparent); }.ovr-filter[aria-selected="true"] svg { color: var(--accent-light, var(--accent, #60a5fa)); }@@@ -723,6 +830,44 @@ a.ovr-row { color: inherit; text-decoration: none; }.ovr-btn.is-primary { color: var(--on-accent-strong, #fff); background: var(--accent-strong, var(--accent, #2563eb)); } .ovr-btn:disabled { opacity: .5; cursor: default; }.ovr-files { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; } .ovr-step.is-you .ovr-files { justify-content: flex-end; }.ovr-thumb { width: 120px; height: 90px; object-fit: cover; border-radius: 12px; }++/* 4.240.0: one agent. The filters are chips over one list; your agent heads it; the thread opens at once. */++.ovr-filter { flex: none; min-height: 36px !important; padding: 0 13px 0 11px; border-radius: 999px !important; font-size: 13.5px !important; gap: 6px !important; border: 1px solid color-mix(in srgb, var(--text-primary, #e6edf7) 12%, transparent); background: transparent !important; box-shadow: none !important; transition: background-color .2s, color .2s, border-color .2s, transform .18s cubic-bezier(.3,1.4,.4,1); }++.ovr-filter svg { width: 16px !important; height: 16px !important; }++.ovr-filter[aria-selected="true"] { color: var(--accent-light, #93c5fd) !important; border-color: color-mix(in srgb, var(--accent, #3b82f6) 55%, transparent); background: color-mix(in srgb, var(--accent, #3b82f6) 16%, transparent) !important; }++.ovr-filter:active { transform: scale(.94); }++.ovr-agent-slot { flex: none; width: 100%; max-width: 780px; margin-inline: auto; box-sizing: border-box; }++.ovr-agent-slot[hidden] { display: none; }++.ovr-agent { display: flex; align-items: center; gap: 8px; padding: 8px 8px 8px 6px; border-radius: 20px; background: linear-gradient(135deg, color-mix(in srgb, hsl(var(--ovr-ah, 220) 70% 55%) 14%, transparent), color-mix(in srgb, var(--text-primary, #e6edf7) 3%, transparent)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text-primary, #e6edf7) 9%, transparent); animation: ovr-rise .32s cubic-bezier(.2,.9,.3,1.1) both; }++.ovr-agent-who { all: unset; box-sizing: border-box; flex: 1; min-width: 0; display: flex; align-items: center; gap: 12px; min-height: 52px; padding: 2px 6px; border-radius: 14px; cursor: pointer; }++.ovr-agent-who:focus-visible, .ovr-agent-go:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 2px; }++.ovr-agent-t { min-width: 0; display: grid; gap: 2px; }++.ovr-agent-top { display: flex; align-items: baseline; gap: 8px; min-width: 0; }++.ovr-agent-top b { font: 800 16px/1.2 'OV Brand', Nunito, Inter, sans-serif; color: var(--text-primary, #e6edf7); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }++.ovr-agent-tag { font-style: normal; font-size: 12px; font-weight: 650; color: var(--text-muted, #8b98ad); white-space: nowrap; }++.ovr-agent-line { font-size: 13px; color: var(--text-secondary, #a8b3c4); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }++.ovr-agent.is-asks .ovr-agent-line { color: #fbbf24; }++.ovr-agent-go { all: unset; box-sizing: border-box; flex: none; display: grid; place-items: center; width: 44px; height: 44px; border-radius: 50%; cursor: pointer; color: var(--text-secondary, #a8b3c4); background: color-mix(in srgb, var(--text-primary, #e6edf7) 7%, transparent); transition: transform .18s cubic-bezier(.3,1.4,.4,1), background-color .2s; }++.ovr-agent-go svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 2.4; stroke-linecap: round; }++.ovr-agent-go.is-main { color: var(--on-accent, #fff); background: var(--accent, #3b82f6); box-shadow: 0 6px 18px -6px color-mix(in srgb, var(--accent, #3b82f6) 70%, transparent); }++.ovr-agent-go:active { transform: scale(.9); }++.ovr-agent-go:disabled { opacity: .5; }++.ovr-face.ovr-me { position: relative; }++.ovr-face.ovr-me::after { content: ""; position: absolute; right: -1px; bottom: -1px; width: 11px; height: 11px; border-radius: 50%; border: 2px solid var(--bg-secondary, #121826); background: #64748b; }++.ovr-face.ovr-me.is-resting::after { background: #22c55e; }++.ovr-face.ovr-me.is-working::after { background: var(--accent, #3b82f6); animation: ovr-pulse 1.2s ease-in-out infinite; }++.ovr-face.ovr-me.is-asks::after { background: #f59e0b; }++.ovr-face.ovr-me.is-working { animation: ovr-breathe 2.2s ease-in-out infinite; }++.ovr-face.ovr-me.is-paused { filter: saturate(.4); opacity: .8; }++.ovr-typing { align-self: flex-start; display: inline-flex; gap: 5px; padding: 12px 14px; margin: 2px 0 6px; border-radius: 18px 18px 18px 6px; background: color-mix(in srgb, var(--text-primary, #e6edf7) 7%, transparent); animation: ovr-rise .25s ease both; }++.ovr-typing i { width: 7px; height: 7px; border-radius: 50%; background: var(--text-secondary, #a8b3c4); animation: ovr-dot 1.1s ease-in-out infinite; }++.ovr-typing i:nth-child(2) { animation-delay: .15s; } .ovr-typing i:nth-child(3) { animation-delay: .3s; }++@keyframes ovr-dot { 0%, 60%, 100% { transform: translateY(0); opacity: .45; } 30% { transform: translateY(-4px); opacity: 1; } }++.ovr.is-pending .ovr-step.is-you, .ovr-step.is-said { animation: ovr-send .32s cubic-bezier(.2,.9,.3,1.1) both; }++@keyframes ovr-send { from { opacity: 0; transform: translateY(18px) scale(.96); } }++.ovr-went { display: flex; align-items: center; gap: 8px; align-self: center; max-width: 92%; margin: 4px auto 8px; padding: 7px 12px; border-radius: 999px; font-size: 12.5px; font-weight: 650; color: var(--accent-light, #93c5fd); background: color-mix(in srgb, var(--accent, #3b82f6) 12%, transparent); animation: ovr-rise .3s ease both; }++.ovr-went svg { flex: none; width: 15px; height: 15px; }++html[data-ov-motion=reduced] .ovr-typing i, html[data-ov-motion=reduced] .ovr-face.ovr-me.is-working { animation-duration: 3s; }++html[data-ov-motion=off] :is(.ovr-agent, .ovr-typing, .ovr-typing i, .ovr-went, .ovr-step.is-said, .ovr-face.ovr-me, .ovr-face.ovr-me::after) { animation: none !important; }@keyframes ovr-rise { from { opacity: 0; transform: translateY(6px); } }@keyframes ovr-shimmer { 50% { opacity: .55; } }@keyframes ovr-breathe { 50% { box-shadow: 0 0 0 5px color-mix(in srgb, var(--accent, #3b82f6) 22%, transparent); } }@@@ -1,12 +1,12 @@{"name": "openvibe-shared",-- "version": "4.239.0",++ "version": "4.240.0","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-shared",-- "version": "4.239.0",++ "version": "4.240.0","license": "MIT","dependencies": {"clean-css": "^5.3.3",@@@ -1,6 +1,6 @@{"name": "openvibe-shared",-- "version": "4.239.0",++ "version": "4.240.0","description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site","main": "index.js","bin": {@@@ -1651,12 +1651,12 @@ html[data-ov-motion=off] .ovs-youpanel { animation: none; }const D = () => root.OpenVibeDock;const opts = {base: `${net}/api/frame`, token: () => ctx.token, me, sibling: ctx.sibling, filter: runFilter,-- // All: every run, with the Action Center's work from every product, the Dock's Now, your Actor and the live-- // agent view above it, and Vibe's starters. Chats, Tasks and Code are still the Dock's own panels (its Vibe,-- // Agents and Code: their history, skills, voice, undo, repos), lent whole until the one view does all they do.-- actions: { base: net }, starters: VIBE_STARTERS,-- top: (slot) => lend(slot, { tab: 'agent', part: 'top' }),-- panes: { chat: (slot) => lend(slot, { tab: 'vibe' }), task: (slot) => lend(slot, { tab: 'agent' }), code: (slot) => lend(slot, { tab: 'code' }) },++ // 4.240.0 (the owner, 11-13: "it's just like filters … your actor is just your agent"): one view. Your agent++ // heads the list, Chats, Tasks and Code are filters over the same runs, and one box starts everything; the++ // Dock's Vibe, Agents and Code panels are no longer lent here.++ actions: { base: net }, starters: VIBE_STARTERS, agent: true,++ // Above the list: Now (timers, what is on) and the live view of an agent at work, from the Dock's agent top.++ top: (slot) => lend(slot, { tab: 'agent', part: 'live' }),onPlace: (p) => { if (p.filter) runFilter = p.filter; },};const go = (R) => {@@@ -126,6 +126,19 @@ const server = http.createServer((req, res) => {return json(200, { state: scenario.state || defaultState(), counts: scenario.counts || { running: 0, waiting: 0, unread: 0 }, sources_partial: false });}if (u.pathname === '/api/v1/me/actions') return json(200, scenario.actions || ACTIONS);++ // 4.240.0: one agent. A run starts through the Frame with the words alone; its thread and stream follow.++ if (u.pathname === '/api/frame/runs' && req.method === 'POST') {++ const b = JSON.parse(Buffer.concat(chunks).toString() || '{}');++ asked.push({ message: b.text, kind: b.kind || null });++ return json(201, { id: 'c_new', ref: 'chat:c_new', kind: 'chat', title: String(b.text || '').slice(0, 60), status: 'running', agent: { id: 'vibe', name: 'Vibe', glyph: 'spark' }, updated_at: new Date().toISOString() });++ }++ if (decodeURIComponent(u.pathname) === '/api/frame/runs/chat:c_new') return json(200, { id: 'c_new', ref: 'chat:c_new', kind: 'chat', title: (asked.at(-1) || {}).message || 'Chat', status: 'waiting', waiting: { for: 'message' }, agent: { id: 'vibe', name: 'Vibe', glyph: 'spark' }, updated_at: new Date().toISOString() });++ if (decodeURIComponent(u.pathname) === '/api/frame/runs/chat:c_new/steps') return json(200, { steps: [++ { seq: 1, kind: 'message', from: 'person', text: (asked.at(-1) || {}).message || '' },++ { seq: 2, kind: 'message', from: 'agent', text: 'Reminder set: Stretch, every 2 hours from 9 am to 5 pm.', ui: { type: 'results', data: { results: [{ kind: 'reminder', label: 'Stretch, every 2 hours' }] } } },++ ], next: null });++ if (decodeURIComponent(u.pathname) === '/api/frame/runs/chat:c_new/events') { res.writeHead(200, { 'content-type': 'text/event-stream' }); return res.end('event: end\ndata: {"status":"waiting"}\n\n'); }++ if (u.pathname === '/api/frame/actor') return json(200, { id: 'act_1', name: 'Juniper', status: 'resting', enabled: true, asks: [], now: { cost_today: 0.12 }, caps: { usd: 1 } });// 4.225.0: the menu's Vibe lists runs from the Frame (Network /api/frame/runs, agents.run@1).if (u.pathname === '/api/frame/runs' && req.method === 'GET') {assert.strictEqual(req.headers.authorization, 'Bearer tok_user', 'the runs are read with the person\'s own token');@@@ -320,22 +333,23 @@ const NO_FLOATING = "!document.querySelector('#ovdk-pill, .ovdk-pill') && [...doconst st = await until(p, STATUS, 'the status');assert.deepStrictEqual([st.tone, st.label], ['waiting', '1 needs you'], 'what needs you comes first');await p.evaluate(MENU_BUTTON);-- const place = await until(p, `(() => { const c = ${PLACE}; return c && c.items ? c : null; })()`, 'the menu, open on Agents');-- assert.deepStrictEqual([place.tab, place.groups, place.items], ['agent', ['waiting', 'running'], 3], 'what needs you first, then what runs');++ // 4.240.0 (one agent): the menu opens on Vibe with Tasks chosen, one list where what needs you comes first; no++ // Dock panel is lent into it.++ const place = await until(p, `(() => { const f = document.querySelector('.ovs.is-open .ovs-door-host .ovr-filter[aria-selected="true"]'); const rows = [...document.querySelectorAll('.ovs-door-host .ovr-rows .ovr-row:not(.is-ghost)')]; return f && rows.length >= 3 ? { filter: f.dataset.filter, needs: rows.map((r) => r.classList.contains('is-needs')), lent: !!document.querySelector('.ovs-door-host .ovdk .ovac-item, .ovs-door-host .ovr-pane .ovdk') } : null; })()`, 'the menu, open on your agent\'s tasks');++ assert.deepStrictEqual([place.filter, place.needs[0], place.lent], ['task', true, false], 'Tasks chosen, what needs you first, and no Dock panel lent');assert.strictEqual(await p.evaluate(NO_FLOATING), true, 'inside the menu, nothing floating');-- await p.evaluate("document.querySelector('.ovdk .ovac-item[data-bucket=waiting] button[data-do=approve]').click()");-- await until(p, "document.querySelector('.ovdk .ovac-item[data-bucket=waiting] .primary').textContent === 'Approved'", 'approved in place');++ await until(p, "!!document.querySelector('.ovs-door-host .ovr-rows li.has-acts .ovr-btn.is-primary')", 'its answer buttons');++ decided.length = 0;++ await p.evaluate("document.querySelector('.ovs-door-host .ovr-rows li.has-acts .ovr-btn.is-primary').click()");++ await until(p, "(document.querySelector('.ovs-door-host .ovr-rows li.has-acts .ovr-row-acts') || {}).textContent === 'Approved'", 'approved in place');assert.deepStrictEqual(decided, ['actor:apr_1:approve'], 'the approval goes to the Network for that item');await p.evaluate("document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true }))");await until(p, "!document.querySelector('.ovs.is-open')", 'Escape closes the menu');-- await eventually(async () => assert.ok(puts.some((b) => b.tab === 'agent'), `the Network heard the place (${JSON.stringify(puts)})`));-- assert.ok(puts.length <= 3, `changes go as debounced PUTs, not one per click (${puts.length})`);await p.close();}-- // ── 4.225.0: the menu's Vibe is one list (ov-run.js) under All · Chats · Tasks · Code: runs from the Frame and the-- // Action Center's work together, the Dock's Now, Actor and live view above; Chats, Tasks and Code are the Dock's-- // own panels, lent whole. No Chat · Code · Agents switch. ──++ // ── 4.240.0: the menu's Vibe is one agent: one list (ov-run.js) with All · Chats · Tasks · Code as filters over it,++ // runs from the Frame and the Action Center's work together, your agent at the top; no Dock panel lent. ──{scenario = { counts: { running: 0, waiting: 0, unread: 0 } };agentView = null;@@@ -348,19 +362,17 @@ const NO_FLOATING = "!document.querySelector('#ovdk-pill, .ovdk-pill') && [...doawait until(p, "document.querySelectorAll('.ovs-door-host .ovr-rows .ovr-row:not(.is-ghost)').length === 4", 'the run and the Action Center\'s three');const all = await p.evaluate(`({ filters: [...document.querySelectorAll('.ovs-door-host .ovr-filter')].map((b) => b.textContent + (b.getAttribute('aria-selected') === 'true' ? '*' : '')),titles: [...document.querySelectorAll('.ovs-door-host .ovr-row-top b')].map((b) => b.textContent), sw: !!document.querySelector('.ovs-door-sw, [data-part]'),-- top: !!document.querySelector('.ovs-door-host .ovr-top .ovdk.ovdk-part-top'), runsList: !!document.querySelector('.ovs-door-host .ovr-top .ovdk-scroll, .ovs-door-host .ovr-top .ovac-item'),++ top: !!document.querySelector('.ovs-door-host .ovr-agent'), runsList: !!document.querySelector('.ovs-door-host .ovdk .ovac-item, .ovs-door-host .ovdk .ovdk-actor, .ovs-door-host .ovr-pane .ovdk'),starters: document.querySelectorAll('.ovs-door-host .ovr-starter').length, wide: document.documentElement.scrollWidth > innerWidth })`);assert.deepStrictEqual(all.filters, ['All*', 'Chats', 'Tasks', 'Code'], 'All · Chats · Tasks · Code, on All');assert.deepStrictEqual(all.titles, ['Send the weekly digest?', 'Fix the flaky upload test', 'Compare three VPS offers', 'Patch notes for 4.162'], 'what needs you first, then newest (the work a minute ago, the chat four): a run and the Action Center\'s work in one list');-- assert.deepStrictEqual([all.sw, all.top, all.runsList, all.starters, all.wide], [false, true, false, 6, false], 'no switch; the Dock\'s top above (not its list); the six starters; nothing off the screen');++ assert.deepStrictEqual([all.sw, all.top, all.runsList, all.starters, all.wide], [false, true, false, 6, false], 'no switch; your agent above, no Dock list or Actor card; the six starters; nothing off the screen');if (process.env.OV_CAPTURE_VIBE) { await wait(500); const r = await p.send('Page.captureScreenshot', { format: 'png' }); fs.writeFileSync(path.join(process.env.OV_CAPTURE_VIBE, 'vibe-all-390.png'), Buffer.from(r.data, 'base64')); }-- // Tasks: the Dock's Agents panel, whole; the Network hears the place.++ // Tasks: a filter over the same list, never a panel of its own.await p.evaluate(`document.querySelector('.ovs-door-host .ovr-filter[data-filter="task"]').click()`);-- const place = await until(p, `(() => { const c = ${PLACE}; return c && c.tab === 'agent' && c.items ? c : null; })()`, 'Tasks: the Agents panel');-- assert.strictEqual(place.items, 3, 'its own list, as before');-- assert.strictEqual(await p.evaluate("!!document.querySelector('.ovs-door-host .ovr-pane .ovdk') && document.querySelector('.ovs-door-host .ovr-top').hidden"), true, 'in the pane, and the top handed back');++ await until(p, "document.querySelector('.ovs-door-host .ovr-filter[data-filter=task]').getAttribute('aria-selected') === 'true' && document.querySelectorAll('.ovs-door-host .ovr-rows .ovr-row:not(.is-ghost)').length >= 3", 'Tasks: the same list, filtered');++ assert.strictEqual(await p.evaluate("!document.querySelector('.ovs-door-host .ovr-pane .ovdk') && !document.querySelector('.ovs-door-host .ovdk .ovac-item')"), true, 'no Dock panel');if (process.env.OV_CAPTURE_VIBE) { await wait(500); const r = await p.send('Page.captureScreenshot', { format: 'png' }); fs.writeFileSync(path.join(process.env.OV_CAPTURE_VIBE, 'vibe-tasks-390.png'), Buffer.from(r.data, 'base64')); }-- await eventually(async () => assert.ok(puts.some((b) => b.tab === 'agent'), 'the Network heard Tasks'));// Approve in place from All, through the Action Center.await p.evaluate(`document.querySelector('.ovs-door-host .ovr-filter[data-filter="all"]').click()`);await until(p, "!!document.querySelector('.ovs-door-host .ovr-rows li.has-acts .ovr-btn.is-primary')", 'All again, with its answers');@@@ -481,7 +493,8 @@ const NO_FLOATING = "!document.querySelector('#ovdk-pill, .ovdk-pill') && [...doawait p.goto(`${base}/page`, { maxMs: 8000 });await until(p, "!!window.OpenVibeDock", 'the panels');await p.evaluate(OPEN('agent'));-- await until(p, `(() => { const c = ${PLACE}; return c && c.tab === 'agent' && c.items ? c : null; })()`, 'the Agents place');++ // 4.240.0: the agent at work shows above your one list in the menu's Vibe (Tasks chosen).++ await until(p, "!!document.querySelector('.ovs-door-host .ovr-top .ovdk-agent')", 'the Agents place');// The stream: Live's embed, the task, the numbers.const src = await until(p, "document.querySelector('.ovda iframe') && document.querySelector('.ovda iframe').getAttribute('src')", 'the embed');assert.strictEqual(src, view().embed_url, 'the embed Codes answered, with the ticket in its address');@@@ -507,16 +520,16 @@ const NO_FLOATING = "!document.querySelector('#ovdk-pill, .ovdk-pill') && [...doassert.deepStrictEqual(controls, ['curtain', 'pause', 'mute', 'end'], 'Pause, Mute and End reached Codes, which puts them to Live');await eventually(async () => assert.strictEqual(await p.evaluate("!!document.querySelector('.ovda iframe')"), false, 'no player once it has ended'));await eventually(async () => assert.strictEqual(await p.evaluate("document.querySelector('.ovda-meta').textContent"), 'Ended · worked 12 min'));-- await eventually(async () => assert.strictEqual(await p.evaluate("document.querySelectorAll('.ovac-item').length"), 3, 'the agents list is untouched under the stream'));++ await eventually(async () => assert.ok(await p.evaluate("document.querySelectorAll('.ovs-door-host .ovr-rows .ovr-row:not(.is-ghost)').length >= 3"), 'the one list is untouched under the stream'));// No stream at all: the view hides itself and the tab is just the list.agentView = null;await p.goto(`${base}/page`, { maxMs: 8000 });await until(p, "!!window.OpenVibeDock", 'the panels again');await p.evaluate(OPEN('agent'));-- await until(p, `(() => { const c = ${PLACE}; return c && c.tab === 'agent' && c.items ? c : null; })()`, 'the Agents place again');++ await until(p, "!!document.querySelector('.ovs-door-host .ovr-top .ovdk-agent') && document.querySelectorAll('.ovs-door-host .ovr-rows .ovr-row:not(.is-ghost)').length >= 3", 'the Agents place again');await wait(400);await eventually(async () => assert.strictEqual(await p.evaluate("document.querySelector('.ovda').classList.contains('on')"), false, 'nothing to show, and nowhere to show it'));-- await eventually(async () => assert.strictEqual(await p.evaluate("document.querySelectorAll('.ovac-item').length"), 3, 'the list alone'));++ await eventually(async () => assert.ok(await p.evaluate("document.querySelectorAll('.ovs-door-host .ovr-rows .ovr-row:not(.is-ghost)').length >= 3"), 'the list alone'));await p.close();}@@@ -528,80 +541,36 @@ const NO_FLOATING = "!document.querySelector('#ovdk-pill, .ovdk-pill') && [...doconst st = await until(p, STATUS, 'the status at 320');assert.strictEqual(st.tone, 'running');await p.evaluate(OPEN('agent'));-- await until(p, PLACE, 'the menu on Agents at 320');++ await until(p, "!!document.querySelector('.ovs-door-host .ovr-rows .ovr-row:not(.is-ghost)')", 'the menu on Agents at 320');await eventually(async () => assert.strictEqual(await p.evaluate('document.documentElement.scrollWidth'), 320, 'no sideways scroll'));await p.close();}}if (PART === 1) { console.log('dock-chrome (1 of 2): no pill and no floating Dock, the status on the menu button, Agents in the menu, Chats, Send to, the agent view, 320 px clean'); return; }-- // ── Vibe (L48 V2): starters, a question, Vibe's answer in place; the Network keeps the tab. ──++ // ── 4.240.0, one agent: your agent heads the menu's Vibe; a starter starts a conversation at once (your words, the++ // typing), the Frame's run carries Vibe's answer and what it did. The Dock's own Vibe panel is not used. ──{-- scenario = { counts: { running: 0, waiting: 0, unread: 0 }, state: { ...defaultState(), pinned: true } };-- puts.length = 0;-- const p = await fresh(1280, 860);++ scenario = { counts: { running: 0, waiting: 0, unread: 0 }, state: { ...defaultState() } };++ asked.length = 0;++ const p = await fresh(390, 780);await p.goto(`${base}/page`, { maxMs: 5000 });await until(p, "!!window.OpenVibeDock", 'the panels');await p.evaluate(OPEN('vibe'));await until(p, "document.querySelector('.ovs.is-open [data-dock=\"vibe\"]') && document.querySelector('.ovs [data-dock=\"vibe\"]').getAttribute('aria-selected') === 'true'", 'the menu on Vibe');-- await until(p, "!!document.querySelector('.ovdk-vibe-new')", 'the conversation list');-- await p.evaluate("document.querySelector('.ovdk-vibe-new').click()");-- await until(p, "document.querySelectorAll('.ovdk [data-panel=vibe] .ovdk-chip').length === 6", 'the starters before the first message, each with its icon');-- assert.strictEqual(await p.evaluate("[...document.querySelectorAll('.ovdk [data-panel=vibe] .ovdk-chip')].filter((c) => c.querySelector('.ovdk-chip-ic svg')).length"), 6, 'every starter has its icon');-- await p.evaluate("document.querySelector('.ovdk [data-panel=vibe] .ovdk-chip').click()");-- await until(p, "!!document.querySelector('.ovdk [data-panel=vibe] .ovdk-msg.is-mine')", 'my question in place');++ const me = await until(p, "(() => { const a = document.querySelector('.ovs-door-host .ovr-agent'); return a && /Juniper/.test(a.innerText) ? { text: a.innerText.replace(/\\s+/g, ' '), go: !!a.querySelector('.ovr-agent-go') } : null; })()", 'your agent at the top');++ assert.match(me.text, /Juniper your agent Ready · \$0\.12 of \$1\.00 today/, 'its name, its state and today\'s spend');++ assert.ok(me.go, 'and Pause beside it');++ await p.evaluate("document.querySelector('.ovs-door-host .ovr-filter[data-filter=chat]').click()");++ await until(p, "document.querySelectorAll('.ovs-door-host .ovr-starter').length === 6 && [...document.querySelectorAll('.ovs-door-host .ovr-starter')].every((b) => b.querySelector('svg'))", 'the starters, each with its glyph');++ await p.evaluate("document.querySelector('.ovs-door-host .ovr-starter').click()");++ await until(p, "!!document.querySelector('.ovs-door-host .ovr-thread .ovr-step.is-you')", 'my words at once');assert.strictEqual(asked[0].message, 'Remind me to stretch every 2 hours');-- assert.ok(asked[0].timezone, 'with the person\'s time zone, for "at 9"');-- const answer = await until(p, "(() => { const m = document.querySelector('.ovdk [data-panel=vibe] .ovdk-msg.is-vibe:not(.ovdk-typing)'); return m ? { text: m.innerText, link: (m.querySelector('a') || {}).href || null } : null; })()", 'Vibe\'s answer', 6000);-- assert.match(answer.text, /Reminder set: Stretch, every 2 hours/);-- assert.strictEqual(answer.link, 'https://openvibe.today/reminders', 'an https link is a link');-- // What Vibe did, under its answer: a reminder chip with Undo and Open (plan L48 V2, result chips).-- const chip = await until(p, "(() => { const c = document.querySelector('.ovdk [data-panel=vibe] .ovdk-res-c'); return c ? { text: c.innerText, open: (c.querySelector('a') || {}).href, undo: !!c.querySelector('button') } : null; })()", 'the result chip');-- assert.match(chip.text, /Reminder set: Stretch/);-- assert.match(chip.text, /Every 2 hours, 9 am–5 pm/);-- assert.strictEqual(chip.open, 'https://openvibe.today/me#reminders');-- assert.ok(chip.undo, 'Undo while it can be undone');-- await p.evaluate("document.querySelector('.ovdk .ovdk-res-c button').click()");-- await until(p, "!!document.querySelector('.ovdk .ovdk-res-c.is-stopped')", 'the chip says it is stopped');-- assert.deepStrictEqual(undone[0], { message_id: vibeMsgs.find((m) => m.from === 'vibe').id, index: 0 }, 'undo names the answer and the result');-- await eventually(async () => assert.match(await p.evaluate("document.querySelector('.ovdk .ovdk-res-c').innerText"), /Reminder stopped: Stretch/));-- await eventually(async () => assert.strictEqual(await p.evaluate("!!document.querySelector('.ovdk .ovdk-res-c button')"), false, 'and is not offered again'));-- await eventually(async () => assert.strictEqual(await p.evaluate("!!document.querySelector('.ovdk .ovdk-typing')"), false, 'the writing dots go when it answers'));-- await eventually(async () => assert.strictEqual(await p.evaluate("document.querySelector('.ovdk .ovdk-away').href"), 'https://openvibe.chat/vibe/v1', 'Open in Chat goes to the same chat'));-- await eventually(async () => assert.ok(puts.some((b) => b.tab === 'vibe'), `the Network keeps the Vibe tab, so it follows the person to the next site (${JSON.stringify(puts)})`));-- await p.close();-- }---- // ── Vibe by voice without the browser's speech recognition (Firefox): the mic records, Chat's /api/vibe/listen-- // says what was said, and it is asked like typed text. The recorder is a stand-in: no microphone in CI. ──-- {-- scenario = { counts: { running: 0, waiting: 0, unread: 0 }, state: { ...defaultState(), pinned: true, tab: 'vibe' } };-- asked.length = 0;-- const p = await fresh(1280, 860);-- await p.send('Page.addScriptToEvaluateOnNewDocument', { source: `-- delete window.SpeechRecognition; delete window.webkitSpeechRecognition;-- window.__tracksStopped = 0;-- navigator.mediaDevices.getUserMedia = async () => ({ getTracks: () => [{ stop() { window.__tracksStopped++; } }] });-- window.MediaRecorder = class {-- static isTypeSupported(t) { return t === 'audio/ogg;codecs=opus'; }-- constructor(stream, o) { this.mimeType = (o && o.mimeType) || ''; this.state = 'inactive'; }-- start() { this.state = 'recording'; window.__recording = true; }-- stop() { this.state = 'inactive'; window.__recording = false; this.ondataavailable({ data: new Blob([new Uint8Array(4096)], { type: 'audio/ogg' }) }); this.onstop(); }-- };` });-- await p.goto(`${base}/page`, { maxMs: 5000 });-- await until(p, "!!window.OpenVibeDock", 'the panels');-- await p.evaluate(OPEN('vibe'));-- await until(p, "!!document.querySelector('.ovdk-vibe-new')", 'the Vibe list');-- await p.evaluate("document.querySelector('.ovdk-vibe-new').click()");-- await until(p, "!!document.querySelector('.ovdk [data-panel=vibe] .ovdk-mic')", 'a mic, though the browser has no speech recognition');-- await eventually(async () => assert.strictEqual(await p.evaluate("document.querySelector('.ovdk [data-panel=vibe] textarea').placeholder"), 'Ask Vibe, or hold the mic'));-- await p.evaluate("document.querySelector('.ovdk .ovdk-mic').dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))");-- await until(p, "window.__recording === true && document.querySelector('.ovdk .ovdk-mic').getAttribute('aria-pressed') === 'true'", 'recording, and the mic says so');-- await p.evaluate("document.querySelector('.ovdk .ovdk-mic').dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))");-- await until(p, "[...document.querySelectorAll('.ovdk [data-panel=vibe] .ovdk-msg.is-mine')].some((m) => /drink water at 3/.test(m.innerText))", 'what was said, asked');-- assert.deepStrictEqual(listened[0], { type: 'audio/ogg', bytes: 4096, lang: 'en', auth: 'Bearer tok_user' }, 'the recording goes to Chat as itself, with the person\'s token');-- assert.strictEqual(asked[0].message, 'remind me to drink water at 3');-- await eventually(async () => assert.strictEqual(await p.evaluate('window.__tracksStopped'), 1, 'the microphone is released'));-- await eventually(async () => assert.strictEqual(await p.evaluate("document.querySelector('.ovdk .ovdk-mic').getAttribute('aria-pressed')"), 'false'));++ assert.strictEqual(asked[0].kind, null, 'no kind: the Frame places the words');++ const answer = await until(p, "(() => { const m = document.querySelector('.ovs-door-host .ovr-step.is-agent'); return m ? m.innerText : null; })()", 'the answer in place');++ assert.match(answer, /Reminder set: Stretch, every 2 hours/);++ await until(p, "!!document.querySelector('.ovs-door-host .ovr-chip') && !document.querySelector('.ovs-door-host .ovr-typing')", 'what it did as a chip, and the typing gone');++ assert.strictEqual(await p.evaluate("[...document.querySelectorAll('.ovs-door-host .ovr-step.is-you')].length"), 1, 'my words once, not twice');++ assert.strictEqual(await p.evaluate('document.documentElement.scrollWidth'), 390, 'nothing off the screen');await p.close();}@@@ -629,7 +598,7 @@ const NO_FLOATING = "!document.querySelector('#ovdk-pill, .ovdk-pill') && [...doawait p.goto(`${base}/page`, { maxMs: 5000 });await until(p, "!!window.OpenVibeDock", 'the panels');await p.evaluate(OPEN('vibe'));-- await until(p, PLACE, 'the menu on Vibe');++ await until(p, "!!document.querySelector('.ovs.is-open .ovs-door-host .ovr-compose textarea')", 'the menu on Vibe');// 4.223.0: the timer is in the box's + (every composer's), not a button of Vibe's own.await p.evaluate("(document.querySelector('.ovs .ovdk-vibe-new') || { click() {} }).click()");await until(p, "!!document.querySelector('.ovs .ovc-more')", 'the box and its +');@@@ -696,8 +665,9 @@ const NO_FLOATING = "!document.querySelector('#ovdk-pill, .ovdk-pill') && [...doawait p.goto(`${base}/page`, { maxMs: 5000 });await until(p, "!!window.OpenVibeNavbar && typeof OpenVibeNavbar.askAgent === 'function'", 'the navbar');await p.evaluate("(() => { const b = document.createElement('button'); b.type = 'button'; b.id = 'ask'; b.setAttribute('data-ov-ask', 'About this stream: night walk in Asakusa. '); document.body.appendChild(b); b.click(); })()");-- await until(p, PLACE, 'the menu opens');-- const box = await until(p, "(() => { const t = document.querySelector('.ovdk [data-panel=vibe] textarea'); return t && t.value ? { value: t.value } : null; })()", 'the question in the box');++ await until(p, "!!document.querySelector('.ovs.is-open')", 'the menu opens');++ // 4.240.0: the question waits in the one box of the menu's Vibe (your agent), unsent.++ const box = await until(p, "(() => { const t = document.querySelector('.ovs-door-host .ovr-compose textarea'); return t && t.value ? { value: t.value } : null; })()", 'the question in the box');assert.strictEqual(box.value, 'About this stream: night walk in Asakusa. ', 'the page question, as given');await eventually(async () => assert.strictEqual(await p.evaluate("document.querySelector('.ovs [data-dock=\"vibe\"]').getAttribute('aria-selected')"), 'true', 'on Vibe'));await wait(400);@@@ -13,7 +13,7 @@ const { brotli } = require('../scripts/size-report');const ROOT = path.join(__dirname, '..');const NAVBAR_PATH = path.join(ROOT, 'navbar.js');const ICONS = require('../nav-icons');--const NAVBAR_BROTLI_BUDGET = 52 * 1024; // 51.6 KB with menu addresses (data-ov-menu, #ov-menu=, menuAddress; 4.229.0, 2026-11-12: the owner's "other tools and buttons open specific areas of the menu"; budgets grow for a feature); before that: 50.9 KB with Back to the menu, the links' warm-up and token() (4.229.0, 2026-11-12: the owner's back button and fast links; budgets grow for a feature); before that: 50.1 KB with the app icon's squares held at their size before ov-icons.js (4.227.0, 2026-11-12: design rule 142; budgets grow for a feature); before that: 49.9 KB with the content hashes of 4.211.0's siblings (2026-11-11: no new code in the bar; the agent sheet is a sibling); 49.8 KB with the page view a guest sends at once (4.191.0, 2026-11-10); 49.7 KB with the links that step out when the bar is too narrow (4.190.0, 2026-11-10: it lays out the first paint, so it cannot be a sibling; a diet of the bar is owed); 49.3 KB with Sign In's drawn icon and the menu button's idle hop (4.191.0, 2026-11-10; a signed-out first paint, so it cannot be a sibling); 49.0 KB with the press on the site name that opens its pages (4.191.0, 2026-11-10: the list itself is ov-drop.js); 48.7 KB with the page-view beacon's signed-in marker (4.189.0, 2026-11-10); 48.6 KB with the wallet's cookie mode and its two content hashes (4.173.0, 2026-11-09); 48.2 KB with the same-site prerender rule, cross-site prefetch and the file-cache worker registration (4.165.0, 2026-11-09); 47.6 KB with the six glyph styles' refined paths and the sheets' surface loader (4.162.0, 2026-11-09); 47.1 KB with the shared pop motion every site's menus use and the account menu's Wallet and Profile (4.161.0, 2026-11-09); 46.6 KB with the keyboard watcher every site needs and the slots' loader (4.160.0, 2026-11-09; the slots themselves are a sibling, nav-slots.js); 45.0 KB with the floating cards' loader and link warming (4.152.0, 2026-11-08; the cards themselves are a sibling, overlay.js); 44.3 KB with the phone's menu button drawn as the orb (4.148.0, 2026-11-08); 43.5 KB with the menu warmed at idle and on intent (4.137.0); 43.2 KB with every sibling's ?v= content hash (4.137.0, 2026-11-07: the owner's menu was a day stale without them); 42.8 KB keeping the menu's Frame fields (4.132.0, 2026-11-07); 42.6 KB with Messages and the balances on every site (4.130.0, 2026-11-07); 42.1 KB with the featured link's phone button (4.119.0, 2026-11-06); 41.4 KB with the page-move code (4.70.0: busy(), the mark on plain links, prefetch rules on unboosted sites); was 41 KB with the Dock's loader (4.51.0)++const NAVBAR_BROTLI_BUDGET = 52.6 * 1024; // 52.3 KB with the menu button drawn from the menu (4.240.0, 2026-11-13: the owner's "regenerate this icon … based on what the menu actually looks like … glow"); before: // 51.6 KB with menu addresses (data-ov-menu, #ov-menu=, menuAddress; 4.229.0, 2026-11-12: the owner's "other tools and buttons open specific areas of the menu"; budgets grow for a feature); before that: 50.9 KB with Back to the menu, the links' warm-up and token() (4.229.0, 2026-11-12: the owner's back button and fast links; budgets grow for a feature); before that: 50.1 KB with the app icon's squares held at their size before ov-icons.js (4.227.0, 2026-11-12: design rule 142; budgets grow for a feature); before that: 49.9 KB with the content hashes of 4.211.0's siblings (2026-11-11: no new code in the bar; the agent sheet is a sibling); 49.8 KB with the page view a guest sends at once (4.191.0, 2026-11-10); 49.7 KB with the links that step out when the bar is too narrow (4.190.0, 2026-11-10: it lays out the first paint, so it cannot be a sibling; a diet of the bar is owed); 49.3 KB with Sign In's drawn icon and the menu button's idle hop (4.191.0, 2026-11-10; a signed-out first paint, so it cannot be a sibling); 49.0 KB with the press on the site name that opens its pages (4.191.0, 2026-11-10: the list itself is ov-drop.js); 48.7 KB with the page-view beacon's signed-in marker (4.189.0, 2026-11-10); 48.6 KB with the wallet's cookie mode and its two content hashes (4.173.0, 2026-11-09); 48.2 KB with the same-site prerender rule, cross-site prefetch and the file-cache worker registration (4.165.0, 2026-11-09); 47.6 KB with the six glyph styles' refined paths and the sheets' surface loader (4.162.0, 2026-11-09); 47.1 KB with the shared pop motion every site's menus use and the account menu's Wallet and Profile (4.161.0, 2026-11-09); 46.6 KB with the keyboard watcher every site needs and the slots' loader (4.160.0, 2026-11-09; the slots themselves are a sibling, nav-slots.js); 45.0 KB with the floating cards' loader and link warming (4.152.0, 2026-11-08; the cards themselves are a sibling, overlay.js); 44.3 KB with the phone's menu button drawn as the orb (4.148.0, 2026-11-08); 43.5 KB with the menu warmed at idle and on intent (4.137.0); 43.2 KB with every sibling's ?v= content hash (4.137.0, 2026-11-07: the owner's menu was a day stale without them); 42.8 KB keeping the menu's Frame fields (4.132.0, 2026-11-07); 42.6 KB with Messages and the balances on every site (4.130.0, 2026-11-07); 42.1 KB with the featured link's phone button (4.119.0, 2026-11-06); 41.4 KB with the page-move code (4.70.0: busy(), the mark on plain links, prefetch rules on unboosted sites); was 41 KB with the Dock's loader (4.51.0)// ── size budget and generated block ──────────────────────────{@@@ -161,7 +161,8 @@ const server = http.createServer((req, res) => {await page.evaluate(`document.querySelector('.ovr-thead .ovr-ib').click()`);assert.ok(await until(`document.querySelectorAll('.ovr-rows .ovr-row:not(.is-ghost)').length === 3`), `${width}: back to the list`);await page.evaluate(`document.querySelector('.ovr-compose textarea').value = 'A raid alert'; document.querySelector('.ovr-compose form').requestSubmit()`);-- assert.ok(await until(`document.querySelector('.ovr-who b') && document.querySelector('.ovr-thread .ovr-none')`), `${width}: the new run opens`);++ // 4.240.0: the thread opens at once with your words and your agent typing, until its first words arrive.++ assert.ok(await until(`document.querySelector('.ovr-who b') && [...document.querySelectorAll('.ovr-thread .ovr-step.is-you')].some((s) => s.textContent === 'A raid alert') && document.querySelector('.ovr-thread .ovr-typing')`), `${width}: the new run opens with your words and your agent typing`);const start = seen.posts.find((p) => p.path === '');assert.deepEqual(start.body, { text: 'A raid alert' }, `${width}: started with the words`);assert.match(start.key, /^ovr-[a-z0-9]+-[a-z0-9]+$/, `${width}: once (an idempotency key)`);@@@ -189,10 +190,11 @@ const server = http.createServer((req, res) => {// Code hides the slot and the starters; Chats shows the starters only.await page.evaluate(`document.querySelector('.ovr-filter[data-filter="code"]').click()`);assert.ok(await until(`document.querySelectorAll('.ovr-rows .ovr-row:not(.is-ghost)').length === 1`), `${width}: Code`);-- assert.deepEqual(await page.evaluate(`[document.querySelector('.ovr-top').hidden, document.querySelector('.ovr-starters').hidden]`), [true, true], `${width}: no slot and no starters on Code`);++ // 4.240.0: the host's slot (Now: timers, an agent at work) shows on every filter; the starters stay on All and Chats.++ assert.deepEqual(await page.evaluate(`[document.querySelector('.ovr-top').hidden, document.querySelector('.ovr-starters').hidden]`), [false, true], `${width}: the slot, and no starters, on Code`);await page.evaluate(`document.querySelector('.ovr-filter[data-filter="chat"]').click()`);assert.ok(await until(`document.querySelector('.ovr-filter[aria-selected="true"]').dataset.filter === 'chat' && !document.querySelector('.ovr-rows .is-ghost')`), `${width}: Chats`);-- assert.deepEqual(await page.evaluate(`[document.querySelector('.ovr-top').hidden, document.querySelector('.ovr-starters').hidden]`), [true, false], `${width}: the starters on Chats`);++ assert.deepEqual(await page.evaluate(`[document.querySelector('.ovr-top').hidden, document.querySelector('.ovr-starters').hidden]`), [false, false], `${width}: the slot and the starters on Chats`);// Ask your agent: the words in the box, unsent.await page.evaluate(`window.view.prefill('What is this page about?')`);assert.ok(await until(`document.querySelector('.ovr-compose textarea').value === 'What is this page about?' && document.activeElement === document.querySelector('.ovr-compose textarea')`), `${width}: prefill puts the words in the box, focused`);@@@ -21,6 +21,9 @@ const wait = (ms) => new Promise((r) => setTimeout(r, ms));await p.evaluate(`document.querySelector('[data-chip-id]').click()`); await wait(500);// 4.202.0: the wallet opens in the one window, which grows in (.28 s): measure once it has settled.{ const end = Date.now() + 4000; while (Date.now() < end && !(await p.evaluate(`(() => { const w = document.querySelector('.ovw'); return !w || (w.getAnimations ? w.getAnimations().every((a) => a.playState !== 'running') : true); })()`))) await wait(50); }++ // The wallet opens with a scale animation; sizes are read once it has settled (a measurement mid-animation read++ // 40 px buttons as smaller, about one run in four).++ await p.evaluate(`Promise.all(document.getAnimations().filter((a) => a.effect && a.effect.getComputedTiming().iterations !== Infinity).map((a) => a.finished.catch(() => {}))).then(() => true)`);const metrics = await p.evaluate(`(() => { const d=document.querySelector('.ov-wallet'); return {open:!!(d&&(d.open||d.closest('.ovw'))), width:document.documentElement.scrollWidth, inner:d&&d.scrollWidth, box:d&&d.clientWidth, lock:document.documentElement.style.overflow, text:d&&d.innerText, buttons:[...d.querySelectorAll('button,a')].map(b=>b.getBoundingClientRect().height)} })()`);assert(metrics.open, 'first chip tap opens wallet'); assert.strictEqual(metrics.width, width); assert(metrics.inner <= metrics.box + 1);assert.strictEqual(metrics.lock, 'hidden'); assert(metrics.text.includes('$0.00')); assert(metrics.text.includes('Buy Vibes')); assert(metrics.text.includes('Top up'));
@@ -1,5 +1,32 @@
@@ -1,5 +1,32 @@
# Changelog
# Changelog
+## 4.240.0 — 2026-11-13
+
+**One agent in the menu's Vibe, and a menu button drawn from the menu** (the owner, 11-13: "all this separation of
+tasks and codes and chats … we shouldn't even have that. It's just like filters … your actor is just your agent … it
+can orchestrate to openvibe.codes, or timer, task, desktop, browser"; "I tried typing test in and just like nothing
+happens … not seamless and sleek"; "regenerate this icon … based on what the menu actually looks like and what the
+menu has to offer … good animations and effects … highlight differently and glow").
+
+- **The menu's Vibe is one agent (ov-run.js, search.js).**
+ - Your agent heads the list (`o.agent`): its face with a state dot (ready, working, asks you, paused), its name,
+ what it is doing and today's spend, and Pause or Resume. A tap shows its work. It reads the Frame's `/actor`.
+ - All · Chats · Tasks · Code are filter chips over one list, never modes: the Dock's Chat, Agents and Code panels
+ and its Actor card are no longer lent into the menu (only Now and the live view of an agent at work, the Dock's
+ new `part: 'live'`).
+ - One box starts everything ("Ask, or say what to do"); the Frame places the words (Network #911: Codes for a change
+ to code, your computer for "on my computer", Vibe for the rest). The thread opens at once with your words and
+ your agent typing until it answers; a run your agent handed on says where it went ("Sent to Codes"). A start that
+ fails goes back to the list with the words in the box and why.
+ - Now (timers, what is on) shows above the list on every filter; Ask your agent (`[data-ov-ask]`, the Dock's
+ `ask()`) puts the words in the one box, unsent; words put in the box before the shared composer loads carry over.
+- **The menu button (navbar.js)** is the menu, small: the ask bar you lead (your face, the words, Vibe's spark) over
+ the place tabs, one lit. Now and then the light walks across the tabs; under the pointer the tabs lift and light,
+ the spark turns and the mark glows; while an agent works the spark turns slowly (`.is-working`); open, the bar and
+ the tabs fold into the ×. Calm and Off keep it still. Class names are M57's, so callers and tests are unchanged.
+- Tests: `run-thread-chrome` (a new run opens with your words and the typing), `dock-chrome` (the menu's Vibe is one
+ list with your agent; Tasks is a filter; the live view above it).
+
## 4.239.0 — 2026-11-13
## 4.239.0 — 2026-11-13
**One floating action, a composer that fits its column, a close button with depth** (the owner, 11-13: "this button
**One floating action, a composer that fits its column, a close button with depth** (the owner, 11-13: "this button
@@ -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.239.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
+**Status:** alpha, v4.240.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
consumer installs a tagged release and none keeps a vendored copy
consumer installs a tagged release and none keeps a vendored copy
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
the tag it pins.
the tag it pins.
@@ -4,7 +4,7 @@
@@ -4,7 +4,7 @@
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
"package": "openvibe-shared",
"package": "openvibe-shared",
- "version": "4.239.0",
+ "version": "4.240.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)",
@@ -271,9 +271,11 @@
@@ -271,9 +271,11 @@
if (mounts[id]) { if (mounts[id].refresh) mounts[id].refresh(); if (mounts[id + 'Timeline']) mounts[id + 'Timeline'].refresh(); return; }
if (mounts[id]) { if (mounts[id].refresh) mounts[id].refresh(); if (mounts[id + 'Timeline']) mounts[id + 'Timeline'].refresh(); return; }
mounts[id] = { refresh: null };
mounts[id] = { refresh: null };
panel.innerHTML = '<div class="ovdk-wait"><span></span></div>';
panel.innerHTML = '<div class="ovdk-wait"><span></span></div>';
- if (id === 'agent' && part === 'top') {
- // 4.225.0: only the top is lent (the menu's Vibe draws the runs and the Action Center's work itself).
- panel.innerHTML = '<div class="ovdk-actor"></div><div class="ovdk-timeline"></div><div class="ovdk-agent"></div>';
+ if (id === 'agent' && (part === 'top' || part === 'live')) {
+ // 4.225.0: only the top is lent (the menu's Vibe draws the runs and the Action Center's work itself). 4.240.0,
+ // part 'live': Now (timers, what is on) and the live view of an agent at work, without the Actor card (the
+ // menu's Vibe draws your agent itself).
+ panel.innerHTML = part === 'live' ? '<div class="ovdk-timeline"></div><div class="ovdk-agent"></div>' : '<div class="ovdk-actor"></div><div class="ovdk-timeline"></div><div class="ovdk-agent"></div>';
agentTop(id, panel, epoch);
agentTop(id, panel, epoch);
mounts[id] = { refresh() { ['Timeline', 'Actor', 'View'].forEach((k) => { const m = mounts[id + k]; if (m && m.refresh) m.refresh(); }); } };
mounts[id] = { refresh() { ['Timeline', 'Actor', 'View'].forEach((k) => { const m = mounts[id + k]; if (m && m.refresh) m.refresh(); }); } };
return;
return;
@@ -310,7 +312,7 @@
@@ -310,7 +312,7 @@
load('dock-timeline.js', () => !!(root.OpenVibeDockTimeline && root.OpenVibeDockTimeline.mount)).then((on) => {
load('dock-timeline.js', () => !!(root.OpenVibeDockTimeline && root.OpenVibeDockTimeline.mount)).then((on) => {
if (on && epoch === panelEpoch && !mounts[id + 'Timeline'] && panel.isConnected && panelActive(id)) mounts[id + 'Timeline'] = root.OpenVibeDockTimeline.mount(panel.querySelector('.ovdk-timeline'), { token: o.token, base: directBase(o.actorBase), frameBase: frameBase(), active: () => panelActive('agent'), focus: () => o.task || null });
if (on && epoch === panelEpoch && !mounts[id + 'Timeline'] && panel.isConnected && panelActive(id)) mounts[id + 'Timeline'] = root.OpenVibeDockTimeline.mount(panel.querySelector('.ovdk-timeline'), { token: o.token, base: directBase(o.actorBase), frameBase: frameBase(), active: () => panelActive('agent'), focus: () => o.task || null });
});
});
- load('dock-actor.js', () => !!(root.OpenVibeDockActor && root.OpenVibeDockActor.mount)).then((on) => {
+ if (panel.querySelector('.ovdk-actor')) load('dock-actor.js', () => !!(root.OpenVibeDockActor && root.OpenVibeDockActor.mount)).then((on) => {
if (on && epoch === panelEpoch && !mounts[id + 'Actor'] && panel.isConnected && panelActive(id)) mounts[id + 'Actor'] = root.OpenVibeDockActor.mount(panel.querySelector('.ovdk-actor'), { token: o.token, base: directBase(o.actorBase), frameBase: frameBase(), networkBase: o.apiBase || 'https://openvibe.network', active: () => panelActive('agent') });
if (on && epoch === panelEpoch && !mounts[id + 'Actor'] && panel.isConnected && panelActive(id)) mounts[id + 'Actor'] = root.OpenVibeDockActor.mount(panel.querySelector('.ovdk-actor'), { token: o.token, base: directBase(o.actorBase), frameBase: frameBase(), networkBase: o.apiBase || 'https://openvibe.network', active: () => panelActive('agent') });
});
});
load('dock-agent.js', () => !!(root.OpenVibeDockAgent && root.OpenVibeDockAgent.mount)).then((on) => {
load('dock-agent.js', () => !!(root.OpenVibeDockAgent && root.OpenVibeDockAgent.mount)).then((on) => {
@@ -689,6 +691,14 @@
@@ -689,6 +691,14 @@
// ── Open: the menu, on one of its places ──
// ── Open: the menu, on one of its places ──
// Ask your agent (4.135.0): Vibe opens with the question in its box, unsent.
// Ask your agent (4.135.0): Vibe opens with the question in its box, unsent.
function ask({ text = '' } = {}, n = 0) {
function ask({ text = '' } = {}, n = 0) {
+ // 4.240.0: the menu's Vibe is one agent with one box (ov-run.js): the words wait there, unsent.
+ const S = root.OpenVibeSearch;
+ if (S && typeof S.prefill === 'function') {
+ if (!n) open({ tab: 'vibe' });
+ if (S.prefill(text)) return;
+ if (n < 40) setTimeout(() => ask({ text }, n + 1), 100);
+ return;
+ }
if (!n && !panelActive('vibe')) open({ tab: 'vibe' }); if (vibe.api && vibe.api.ask && panelActive('vibe')) vibe.api.ask(text); else if (n < 40) setTimeout(() => ask({ text }, n + 1), 100); }
if (!n && !panelActive('vibe')) open({ tab: 'vibe' }); if (vibe.api && vibe.api.ask && panelActive('vibe')) vibe.api.ask(text); else if (n < 40) setTimeout(() => ask({ text }, n + 1), 100); }
function isOpen() { return !!inline; }
function isOpen() { return !!inline; }
function share({ text = '', title = '' } = {}) {
function share({ text = '', title = '' } = {}) {
@@ -735,7 +745,7 @@
@@ -735,7 +745,7 @@
mountHandle = handle;
mountHandle = handle;
load('dock-mount.js', () => !!root.OpenVibeDockMount).then((ok) => {
load('dock-mount.js', () => !!root.OpenVibeDockMount).then((ok) => {
if (ok && !gone) api = root.OpenVibeDockMount.mount(host, wanted, {
if (ok && !gone) api = root.OpenVibeDockMount.mount(host, wanted, {
- start(wrap) { if (!bodyEl) build(); part = lend === 'top' ? 'top' : null; wrap.classList.toggle('ovdk-part-top', part === 'top'); inline = wrap; wrap.append(bodyEl); },
+ start(wrap) { if (!bodyEl) build(); part = lend === 'top' || lend === 'live' ? lend : null; wrap.classList.toggle('ovdk-part-top', !!part); inline = wrap; wrap.append(bodyEl); },
// A lent top is not a place the person chose: the menu's Vibe remembers its own (remember()).
// A lent top is not a place the person chose: the menu's Vibe remembers its own (remember()).
show(id) { setTab(id, part !== 'top'); },
show(id) { setTab(id, part !== 'top'); },
end() { disposePanel(); inline = null; part = null; if (bodyEl) bodyEl.remove(); paintStatus(); },
end() { disposePanel(); inline = null; part = null; if (bodyEl) bodyEl.remove(); paintStatus(); },
@@ -318,35 +318,50 @@
@@ -318,35 +318,50 @@
.openvibe-navbar .ovnav-orb::before { content: ""; position: absolute; inset: -3px; border-radius: 50%; background: radial-gradient(closest-side, color-mix(in srgb, var(--accent, #3b82f6) 38%, transparent), transparent); opacity: 0; transform: scale(.5); transition: opacity .25s, transform .4s cubic-bezier(.2,1.4,.3,1); pointer-events: none; }
.openvibe-navbar .ovnav-orb::before { content: ""; position: absolute; inset: -3px; border-radius: 50%; background: radial-gradient(closest-side, color-mix(in srgb, var(--accent, #3b82f6) 38%, transparent), transparent); opacity: 0; transform: scale(.5); transition: opacity .25s, transform .4s cubic-bezier(.2,1.4,.3,1); pointer-events: none; }
.openvibe-navbar .ovnav-orb:hover::before, .openvibe-navbar .ovnav-orb:focus-visible::before, .openvibe-navbar .ovnav-orb[aria-expanded="true"]::before { opacity: 1; transform: none; }
.openvibe-navbar .ovnav-orb:hover::before, .openvibe-navbar .ovnav-orb:focus-visible::before, .openvibe-navbar .ovnav-orb[aria-expanded="true"]::before { opacity: 1; transform: none; }
.openvibe-navbar .ovnav-orb svg { position: relative; display: block; overflow: visible; }
.openvibe-navbar .ovnav-orb svg { position: relative; display: block; overflow: visible; }
- .ovmb { display: block; overflow: visible; }
- .ovmb-bar { fill: transparent; stroke: currentColor; stroke-width: 1.9; }
- .ovmb-app { fill: currentColor; }
- .ovmb-dot, .ovmb-a1 { fill: var(--ovo-a, var(--accent-light, #60a5fa)); }
- .ovmb :is(.ovmb-bar, .ovmb-dot, .ovmb-app) { transform-box: fill-box; transform-origin: center; transition: transform .5s cubic-bezier(.2,1.3,.3,1), fill .3s, stroke-width .3s, opacity .25s; }
+ .ovmb { display: block; overflow: visible; transition: filter .3s; }
+ .ovmb-bar { fill: transparent; stroke: currentColor; stroke-width: 1.7; }
+ .ovmb-dot { fill: currentColor; }
+ .ovmb-spark { fill: var(--ovo-b, #e879f9); }
+ .ovmb-words { fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; opacity: .55; }
+ .ovmb-app { fill: currentColor; opacity: .42; }
+ .ovmb-lit { fill: var(--ovo-a, var(--accent-light, #60a5fa)); }
+ .ovmb :is(.ovmb-bar, .ovmb-dot, .ovmb-words, .ovmb-spark, .ovmb-app, .ovmb-lit) { transform-box: fill-box; transform-origin: center; transition: transform .5s cubic-bezier(.2,1.3,.3,1), fill .3s, stroke-width .3s, opacity .25s; }
.ovmb-a0 { transition-delay: .03s; } .ovmb-a1 { transition-delay: .06s; } .ovmb-a2 { transition-delay: .09s; }
.ovmb-a0 { transition-delay: .03s; } .ovmb-a1 { transition-delay: .06s; } .ovmb-a2 { transition-delay: .09s; }
- /* Under the pointer (and once, the first time it is seen): the bar and the three apps lift in turn. */
- :is(.ovnav-orb, .ovs-orb):is(:hover, .ovo-intro) .ovmb:not(.is-x) :is(.ovmb-bar, .ovmb-dot, .ovmb-app) { animation: ovmb-wave .7s ease-in-out both; }
- .ovmb .ovmb-a0 { animation-delay: .08s; } .ovmb .ovmb-a1 { animation-delay: .15s; } .ovmb .ovmb-a2 { animation-delay: .22s; }
- :is(.ovnav-orb, .ovs-orb):is(:hover, :focus-visible) .ovmb-app { fill: var(--ovo-a, var(--accent-light, #60a5fa)); }
- :is(.ovnav-orb, .ovs-orb):is(:hover, :focus-visible) .ovmb-a1 { fill: var(--ovo-b, #e879f9); }
- /* While the menu loads: the apps breathe in turn. */
+ /* Under the pointer (and once, the first time it is seen): the tabs lift in turn and light up, the spark
+ turns, your face brightens, and the mark glows in the accent. */
+ :is(.ovnav-orb, .ovs-orb):is(:hover, .ovo-intro) .ovmb:not(.is-x) :is(.ovmb-bar, .ovmb-app, .ovmb-lit) { animation: ovmb-wave .7s ease-in-out both; }
+ .ovmb .ovmb-a0, .ovmb .ovmb-lit { animation-delay: .08s; } .ovmb .ovmb-a1 { animation-delay: .15s; } .ovmb .ovmb-a2 { animation-delay: .22s; }
+ :is(.ovnav-orb, .ovs-orb):is(:hover, .ovo-intro) .ovmb:not(.is-x) .ovmb-spark { animation: ovmb-twinkle .8s cubic-bezier(.3,1.4,.4,1) both; }
+ :is(.ovnav-orb, .ovs-orb):is(:hover, :focus-visible) .ovmb:not(.is-x) { filter: drop-shadow(0 0 3px color-mix(in srgb, var(--ovo-a, #60a5fa) 70%, transparent)); }
+ :is(.ovnav-orb, .ovs-orb):is(:hover, :focus-visible) .ovmb-app { fill: var(--ovo-a, var(--accent-light, #60a5fa)); opacity: .75; }
+ :is(.ovnav-orb, .ovs-orb):is(:hover, :focus-visible) .ovmb-dot { fill: var(--text-primary, #e6edf7); }
+ :is(.ovnav-orb, .ovs-orb):is(:hover, :focus-visible) .ovmb-words { opacity: .9; transform: scaleX(1.25); }
+ /* Something runs for you (an agent working, set by the menu's stack): the spark turns slowly. */
+ .ovnav-orb.is-working .ovmb:not(.is-x) .ovmb-spark { animation: ovmb-spin 2.4s linear infinite; }
+ /* While the menu loads: the tabs breathe in turn. */
.ovnav-orb.is-busy .ovmb-app { animation: ovmb-busy .9s ease-in-out infinite; }
.ovnav-orb.is-busy .ovmb-app { animation: ovmb-busy .9s ease-in-out infinite; }
- /* Open: the bar is one stroke of the close mark, the three apps the other. */
- :is(.ovnav-orb[aria-expanded="true"], .ovs-orb.is-x) .ovmb :is(.ovmb-bar, .ovmb-dot, .ovmb-app), .ovmb.is-x :is(.ovmb-bar, .ovmb-dot, .ovmb-app) { animation: none; }
- :is(.ovnav-orb[aria-expanded="true"], .ovs-orb.is-x) .ovmb .ovmb-bar, .ovmb.is-x .ovmb-bar { transform: translateY(4px) rotate(45deg) scale(.9, .43); fill: var(--ovo-a, var(--accent-light, #60a5fa)); stroke-width: 0; }
- :is(.ovnav-orb[aria-expanded="true"], .ovs-orb.is-x) .ovmb .ovmb-dot, .ovmb.is-x .ovmb-dot { transform: translate(4.6px, 4px) scale(0); opacity: 0; }
- :is(.ovnav-orb[aria-expanded="true"], .ovs-orb.is-x) .ovmb .ovmb-app, .ovmb.is-x .ovmb-app { fill: var(--ovo-a, var(--accent-light, #60a5fa)); }
- :is(.ovnav-orb[aria-expanded="true"], .ovs-orb.is-x) .ovmb .ovmb-a0, .ovmb.is-x .ovmb-a0 { transform: translate(2.4px, -1.2px) rotate(-45deg) scale(1.3, .6); }
- :is(.ovnav-orb[aria-expanded="true"], .ovs-orb.is-x) .ovmb .ovmb-a1, .ovmb.is-x .ovmb-a1 { transform: translate(0, -4.6px) rotate(-45deg) scale(1.3, .6); }
- :is(.ovnav-orb[aria-expanded="true"], .ovs-orb.is-x) .ovmb .ovmb-a2, .ovmb.is-x .ovmb-a2 { transform: translate(-2.4px, -8px) rotate(-45deg) scale(1.3, .6); }
+ /* Open: the bar is one stroke of the close mark, the three tabs the other; your face and the spark go. */
+ :is(.ovnav-orb[aria-expanded="true"], .ovs-orb.is-x) .ovmb :is(.ovmb-bar, .ovmb-dot, .ovmb-words, .ovmb-spark, .ovmb-app, .ovmb-lit), .ovmb.is-x :is(.ovmb-bar, .ovmb-dot, .ovmb-words, .ovmb-spark, .ovmb-app, .ovmb-lit) { animation: none; }
+ :is(.ovnav-orb[aria-expanded="true"], .ovs-orb.is-x) .ovmb .ovmb-bar, .ovmb.is-x .ovmb-bar { transform: translateY(4.1px) rotate(45deg) scale(.92, .38); fill: var(--ovo-a, var(--accent-light, #60a5fa)); stroke-width: 0; }
+ :is(.ovnav-orb[aria-expanded="true"], .ovs-orb.is-x) .ovmb :is(.ovmb-dot, .ovmb-words, .ovmb-spark), .ovmb.is-x :is(.ovmb-dot, .ovmb-words, .ovmb-spark) { transform: scale(0); opacity: 0; }
+ :is(.ovnav-orb[aria-expanded="true"], .ovs-orb.is-x) .ovmb .ovmb-app, .ovmb.is-x .ovmb-app { fill: var(--ovo-a, var(--accent-light, #60a5fa)); opacity: 1; }
+ :is(.ovnav-orb[aria-expanded="true"], .ovs-orb.is-x) .ovmb .ovmb-a0, .ovmb.is-x .ovmb-a0 { transform: translate(2.4px, -1.2px) rotate(-45deg) scale(1.12, .55); }
+ :is(.ovnav-orb[aria-expanded="true"], .ovs-orb.is-x) .ovmb .ovmb-a1, .ovmb.is-x .ovmb-a1 { transform: translate(0, -5.2px) rotate(-45deg) scale(1.12, .55); }
+ :is(.ovnav-orb[aria-expanded="true"], .ovs-orb.is-x) .ovmb .ovmb-a2, .ovmb.is-x .ovmb-a2 { transform: translate(-2.4px, -9.2px) rotate(-45deg) scale(1.12, .55); }
+ :is(.ovnav-orb[aria-expanded="true"], .ovs-orb.is-x) .ovmb .ovmb-lit, .ovmb.is-x .ovmb-lit { transform: translate(2.4px, -1.2px) rotate(-45deg) scale(1.12, .55); opacity: 0; }
/* A close mark that arrives already made (the Dock's sheet) forms from the button it came from. */
/* A close mark that arrives already made (the Dock's sheet) forms from the button it came from. */
.ovmb.is-forming :is(.ovmb-bar, .ovmb-app) { animation: ovmb-form .5s cubic-bezier(.2,1.3,.3,1) both; }
.ovmb.is-forming :is(.ovmb-bar, .ovmb-app) { animation: ovmb-form .5s cubic-bezier(.2,1.3,.3,1) both; }
@keyframes ovmb-form { from { transform: none; } }
@keyframes ovmb-form { from { transform: none; } }
- .ovnav-orb:not([aria-expanded="true"]) .ovmb:not(.is-x) .ovmb-app { animation: ovmb-idle 9s ease-in-out 3s infinite; }
- .ovnav-orb .ovmb .ovmb-a1 { animation-delay: 3.09s !important; } .ovnav-orb .ovmb .ovmb-a2 { animation-delay: 3.18s !important; }
- @keyframes ovmb-idle { 0%, 4%, 100% { transform: none; } 2% { transform: translateY(calc(-2.2px * var(--ov-move, 1))); } }
- @keyframes ovmb-wave { 0%, 100% { transform: translateY(0); } 40% { transform: translateY(calc(-2.6px * var(--ov-move, 1))); } }
- @keyframes ovmb-busy { 0%, 100% { transform: scale(.72); opacity: .5; } 50% { transform: scale(1.18); opacity: 1; } }
+ /* At rest, every 9 s: the light walks across the places and comes home (Calm and Off leave it still). */
+ .ovnav-orb:not([aria-expanded="true"]):not(:hover) .ovmb:not(.is-x) .ovmb-lit { animation: ovmb-walk 9s cubic-bezier(.5,0,.3,1) 3s infinite; }
+ @keyframes ovmb-walk { 0%, 62%, 100% { transform: none; } 70%, 74% { transform: translateX(6.4px); } 82%, 86% { transform: translateX(12.8px); } 94% { transform: translateX(0); } }
+ @keyframes ovmb-wave { 0%, 100% { transform: translateY(0); } 40% { transform: translateY(calc(-2.4px * var(--ov-move, 1))); } }
+ @keyframes ovmb-twinkle { 0% { transform: none; } 50% { transform: rotate(45deg) scale(1.45); } 100% { transform: rotate(90deg); } }
+ @keyframes ovmb-spin { to { transform: rotate(360deg); } }
+ @keyframes ovmb-busy { 0%, 100% { transform: scale(.72); opacity: .35; } 50% { transform: scale(1.12); opacity: .9; } }
+ html[data-ov-motion=reduced] .ovmb :is(.ovmb-lit, .ovmb-spark, .ovmb-app) { animation-duration: 18s; }
+ html[data-ov-motion=reduced] :is(.ovnav-orb, .ovs-orb):is(:hover, .ovo-intro) .ovmb :is(.ovmb-bar, .ovmb-app, .ovmb-lit, .ovmb-spark) { animation-duration: 1.4s; }
+ html[data-ov-motion=off] .ovmb :is(.ovmb-bar, .ovmb-dot, .ovmb-words, .ovmb-spark, .ovmb-app, .ovmb-lit) { animation: none !important; transition: none; }
.openvibe-navbar .ovnav-orb[aria-expanded="true"] { color: var(--text-primary, #e6edf7); }
.openvibe-navbar .ovnav-orb[aria-expanded="true"] { color: var(--text-primary, #e6edf7); }
.openvibe-navbar :is(.ovnav-orb, .openvibe-bell, .openvibe-navbar-avatar) { transition: transform .18s cubic-bezier(.2, 1.4, .3, 1), background-color .15s, color .15s; }
.openvibe-navbar :is(.ovnav-orb, .openvibe-bell, .openvibe-navbar-avatar) { transition: transform .18s cubic-bezier(.2, 1.4, .3, 1), background-color .15s, color .15s; }
.openvibe-navbar :is(.ovnav-orb, .openvibe-bell, .openvibe-navbar-avatar):active { transform: scale(.9); }
.openvibe-navbar :is(.ovnav-orb, .openvibe-bell, .openvibe-navbar-avatar):active { transform: scale(.9); }
@@ -609,7 +624,7 @@
@@ -609,7 +624,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': 'f490466f57df', 'ov-glyphs.js': '5fa4615e0c7a', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': '2f6cab5e0778', 'ov-fx.js': '947820d69097', 'ov-labels.js': 'd40f7ca14bc4', 'ov-forms.js': '2eb9e2604c3e', 'ov-forms.css': '07e8f59d9e6d', 'ov-share.js': 'd419508a797e', 'ov-share.css': 'd609a71f611a', 'search.js': '228a974d7922', 'dock.js': '6adcf85f7bd8', 'presence.js': '5f195d41f8c5', 'panels.js': 'd90bfe987d81', 'surfaces.js': '3e6f2784c85f', 'notification-ui.js': 'c993615033ae', 'history.js': 'f9fb9e096c65', 'bottom.js': '1671b6949137', 'appbar.js': 'b2a16f53eae0', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': '8701ebc6f8a3', 'sso-client.js': 'f574ac3331a5', 'overlay.js': '2bc56b3cf726', 'ov-home.js': '4e7d405117de', 'nav-slots.js': '7b11e6916732', 'ov-wallet.js': 'f1a2d6774087', 'ov-wallet.css': '4923c808c9ad', 'purse.js': '9d3fea0dae02', 'ov-drop.js': '445689a5eb80', 'ov-tip.js': '1d918e7de69d', 'ov-arrange.js': 'ae24086bd2be', 'soft.js': '98ab9b8080b1', 'ov-run.js': '504bfeabcd49', 'ov-composer.js': '79b34aff4452' };
+ const SIBLING_REV = { 'ov-mark.js': 'f490466f57df', 'ov-glyphs.js': '5fa4615e0c7a', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': '2f6cab5e0778', 'ov-fx.js': '947820d69097', 'ov-labels.js': 'd40f7ca14bc4', 'ov-forms.js': '2eb9e2604c3e', 'ov-forms.css': '07e8f59d9e6d', 'ov-share.js': 'd419508a797e', 'ov-share.css': 'd609a71f611a', 'search.js': '73c7387a6ec5', 'dock.js': '8de9086a2cfd', 'presence.js': '5f195d41f8c5', 'panels.js': 'd90bfe987d81', 'surfaces.js': '3e6f2784c85f', 'notification-ui.js': 'c993615033ae', 'history.js': 'f9fb9e096c65', 'bottom.js': '1671b6949137', 'appbar.js': 'b2a16f53eae0', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': '8701ebc6f8a3', 'sso-client.js': 'f574ac3331a5', 'overlay.js': '2bc56b3cf726', 'ov-home.js': '4e7d405117de', 'nav-slots.js': '7b11e6916732', 'ov-wallet.js': 'f1a2d6774087', 'ov-wallet.css': '4923c808c9ad', 'purse.js': '9d3fea0dae02', 'ov-drop.js': '445689a5eb80', 'ov-tip.js': '1d918e7de69d', 'ov-arrange.js': 'ae24086bd2be', 'soft.js': '98ab9b8080b1', 'ov-run.js': '34f19495bc12', 'ov-composer.js': '79b34aff4452' };
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.
@@ -1015,12 +1030,19 @@
@@ -1015,12 +1030,19 @@
try { const sc = document.createElement('script'); sc.id = 'ov-slots-loader'; sc.src = siblingV('nav-slots.js'); sc.async = true; sc.onload = () => paintSlots(); document.head.appendChild(sc); } catch { /* */ }
try { const sc = document.createElement('script'); sc.id = 'ov-slots-loader'; sc.src = siblingV('nav-slots.js'); sc.async = true; sc.onload = () => paintSlots(); document.head.appendChild(sc); } catch { /* */ }
}
}
- // The OpenVibe menu's button (the owner's pick, 2026-11-10: the Arena's M57, "Search and apps"): the menu itself,
- // small: a search bar over three apps. Open, the bar turns into one stroke of the close mark and the three apps
- // line up as the other, so the button that opens the menu is, in the same place, the one that closes it. One
- // drawing for the bar, the menu's sheet and the Dock's (root.OpenVibeOrb); its styles are below (.ovmb).
- const ORB_PARTS = '<rect class="ovmb-bar" x="4" y="5.2" width="16" height="5.6" rx="2.8"/><circle class="ovmb-dot" cx="7.4" cy="8" r="1.15"/>'
- + '<rect class="ovmb-app ovmb-a0" x="4.2" y="14.6" width="4" height="4" rx="1.3"/><rect class="ovmb-app ovmb-a1" x="10" y="14.6" width="4" height="4" rx="1.3"/><rect class="ovmb-app ovmb-a2" x="15.8" y="14.6" width="4" height="4" rx="1.3"/>';
+ // The OpenVibe menu's button, 4.240.0 (the owner, 11-13: "regenerate based on what the menu actually looks like and
+ // what the menu has to offer"): the menu itself, small. On top, the ask bar you lead: your face at its left, Vibe's
+ // spark at its right. Under it, the place tabs, the one you are on lit. Now and then the light walks across the tabs
+ // (the menu's places); under the pointer the tabs lift in turn, the spark turns and the whole mark glows. Open, the
+ // bar is one stroke of the close mark and the tabs line up as the other, so the button that opens the menu is, in
+ // the same place, the one that closes it (rule opens-it-closes-it). One drawing for the bar, the menu's sheet and
+ // the Dock's (root.OpenVibeOrb); its styles are above (.ovmb). Class names kept from M57 (.ovmb-bar, .ovmb-dot for
+ // the face, .ovmb-app .ovmb-a0..2 for the tabs) so every caller and test still finds its parts.
+ const ORB_PARTS = '<rect class="ovmb-bar" x="3.2" y="4.3" width="17.6" height="7.2" rx="3.6"/>'
+ + '<circle class="ovmb-dot" cx="6.9" cy="7.9" r="1.5"/><path class="ovmb-words" d="M10 7.9h3.4"/>'
+ + '<path class="ovmb-spark" d="M16.6 5.75l.62 1.53 1.53.62-1.53.62-.62 1.53-.62-1.53-1.53-.62 1.53-.62z"/>'
+ + '<rect class="ovmb-app ovmb-a0" x="3" y="14.7" width="5.2" height="5" rx="1.6"/><rect class="ovmb-app ovmb-a1" x="9.4" y="14.7" width="5.2" height="5" rx="1.6"/><rect class="ovmb-app ovmb-a2" x="15.8" y="14.7" width="5.2" height="5" rx="1.6"/>'
+ + '<rect class="ovmb-lit" x="3" y="14.7" width="5.2" height="5" rx="1.6"/>';
function orbSVG(size = 22, open = false) {
function orbSVG(size = 22, open = false) {
return `<svg class="ovmb${open ? ' is-x' : ''}" viewBox="0 0 24 24" width="${size}" height="${size}" aria-hidden="true">${ORB_PARTS}</svg>`;
return `<svg class="ovmb${open ? ' is-x' : ''}" viewBox="0 0 24 24" width="${size}" height="${size}" aria-hidden="true">${ORB_PARTS}</svg>`;
}
}
@@ -33,6 +33,15 @@
@@ -33,6 +33,15 @@
* OpenVibeRun.card(type, (data, step, ctx) => Node) a rich card for a step's `ui.type` (a site or a mod adds its
* OpenVibeRun.card(type, (data, step, ctx) => Node) a rich card for a step's `ui.type` (a site or a mod adds its
* own); an unknown type shows the step's words.
* own); an unknown type shows the step's words.
*
*
+ * 4.240.0, one agent (the owner, 11-13: "all this separation of tasks and codes and chats … we shouldn't even have
+ * that. It's just like filters … your actor is just your agent … it can orchestrate to openvibe.codes, or timer, task,
+ * desktop, browser"):
+ * o.agent: true | { name } your agent heads the list: its face, its state and today's spend (the Frame's
+ * /actor, Actor's GET /api/v1/me/actor), Pause or Resume; a tap on it shows its work
+ * the filters are chips over one list, never modes; one box starts everything (no kind: the Frame hands the words to
+ * chat, Codes, a task or a machine), and the thread opens at once with your words and your agent typing. A run your
+ * agent handed on says where it went ("Sent to Codes").
+ *
* The list: needs-you first, then newest, filtered All · Chats · Tasks · Code; each run is its agent's face (glyph and
* The list: needs-you first, then newest, filtered All · Chats · Tasks · Code; each run is its agent's face (glyph and
* hue, or its picture, with what it is doing), its title, its last line and when. The thread: your words on the right,
* hue, or its picture, with what it is doing), its title, its last line and when. The thread: your words on the right,
* the agent's on the left (paragraphs, lists, links, code, numbered sources), thinking folded, each tool one quiet row,
* the agent's on the left (paragraphs, lists, links, code, numbered sources), thinking folded, each tool one quiet row,
@@ -268,6 +277,61 @@
@@ -268,6 +277,61 @@
} catch { return []; }
} catch { return []; }
}
}
const state = { view: 'list', filter: FILTER_IDS.includes(o.filter) ? o.filter : 'all', runs: [], run: null, steps: new Map(), stopStream: null, abort: null, gen: 0 };
const state = { view: 'list', filter: FILTER_IDS.includes(o.filter) ? o.filter : 'all', runs: [], run: null, steps: new Map(), stopStream: null, abort: null, gen: 0 };
+ // Your agent (4.240.0): one, whatever it is doing. The Frame relays Actor's /api/v1/me/actor (every site's
+ // connect-src allows openvibe.network); its name and picture are the person's choice, "Vibe" until they choose.
+ let agent = null, agentAt = 0;
+ const agentName = () => String((agent && agent.name) || (o.agent && o.agent.name) || 'Vibe').slice(0, 40);
+ const agentState = () => { const a = agent; if (!a) return 'resting'; if (a.status === 'paused' || a.enabled === false) return 'paused'; if (Array.isArray(a.asks) && a.asks.length) return 'asks'; return a.status === 'working' ? 'working' : 'resting'; };
+ function agentFace(size, st) {
+ const f = el('span', `ovr-face is-${size} ovr-me is-${st || agentState()}`);
+ f.style.setProperty('--ovr-h', String(hueOf(agentName())));
+ const pic = agent && agent.avatar && httpsUrl(agent.avatar.url);
+ if (pic) { const i = el('img'); i.src = pic; i.alt = ''; f.append(i); } else f.append(svg('spark'));
+ return f;
+ }
+ async function loadAgent() {
+ if (!o.agent || !tokenOf()) return null;
+ if (agentAt && Date.now() - agentAt < 20000) return agent;
+ try { const r = await fetch(`${base}/actor`, { credentials: 'omit', headers: { Accept: 'application/json', ...headers() } }); agent = r.ok ? await r.json() : null; }
+ catch { agent = null; }
+ agentAt = Date.now();
+ return agent;
+ }
+ const money = (v) => `$${(Number(v) || 0).toFixed(2)}`;
+ const AGENT_WORD = { working: 'Working', asks: 'Asks you something', resting: 'Ready', paused: 'Paused' };
+ /** The list's head: your agent, what it is doing, and the one action that fits. */
+ function paintAgent(slot) {
+ slot.textContent = '';
+ if (!o.agent || !tokenOf()) { slot.hidden = true; return; }
+ slot.hidden = false;
+ const st = agentState();
+ const card = el('div', `ovr-agent is-${st}`);
+ const who = el('button', 'ovr-agent-who'); who.type = 'button';
+ who.setAttribute('aria-label', `${agentName()}, your agent: ${AGENT_WORD[st]}. Show its work`);
+ const now = (agent && agent.now) || {};
+ const step = st === 'working' ? String(now.current_step || '').slice(0, 120) : st === 'asks' ? String((agent.asks[0] && agent.asks[0].text) || '').slice(0, 120) : '';
+ const cap = agent && agent.caps && Number(agent.caps.usd) > 0 ? ` of ${money(agent.caps.usd)}` : '';
+ const t = el('span', 'ovr-agent-t');
+ const top = el('span', 'ovr-agent-top'); top.append(el('b', '', agentName()), el('i', 'ovr-agent-tag', 'your agent'));
+ t.append(top, el('span', 'ovr-agent-line', [AGENT_WORD[st], step, agent ? `${money(now.cost_today)}${cap} today` : ''].filter(Boolean).join(' · ')));
+ who.append(agentFace('m', st), t);
+ who.addEventListener('click', () => list('task'));
+ card.append(who);
+ if (agent && agent.id) {
+ const paused = st === 'paused';
+ const go = el('button', `ovr-agent-go${paused ? ' is-main' : ''}`); go.type = 'button';
+ go.setAttribute('aria-label', paused ? `Resume ${agentName()}` : `Pause ${agentName()}`); go.title = paused ? 'Resume' : 'Pause';
+ go.innerHTML = paused ? '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5.5v13l10.5-6.5z" fill="currentColor"/></svg>' : '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8.5 5.5v13M15.5 5.5v13"/></svg>';
+ go.addEventListener('click', async () => {
+ go.disabled = true;
+ try { const r = await fetch(`${base}/actor/${paused ? 'resume' : 'pause'}`, { method: 'POST', credentials: 'omit', headers: { Accept: 'application/json', 'Content-Type': 'application/json', ...headers() }, body: '{}' }); if (r.ok) { agentAt = 0; await loadAgent(); } }
+ catch { /* the row stays as it was */ }
+ if (slot.isConnected) paintAgent(slot);
+ });
+ card.append(go);
+ }
+ slot.append(card);
+ }
function clearStream() { if (state.stopStream) state.stopStream(); state.stopStream = null; if (state.abort) state.abort.abort(); state.abort = null; }
function clearStream() { if (state.stopStream) state.stopStream(); state.stopStream = null; if (state.abort) state.abort.abort(); state.abort = null; }
@@ -302,16 +366,47 @@
@@ -302,16 +366,47 @@
let sc = document.getElementById('ov-composer-script');
let sc = document.getElementById('ov-composer-script');
if (!sc) { sc = document.createElement('script'); sc.id = 'ov-composer-script'; sc.async = true; sc.src = o.sibling('ov-composer.js'); document.head.appendChild(sc); }
if (!sc) { sc = document.createElement('script'); sc.id = 'ov-composer-script'; sc.async = true; sc.src = o.sibling('ov-composer.js'); document.head.appendChild(sc); }
plain();
plain();
- sc.addEventListener('load', () => { if (slot.isConnected && root.OpenVibeComposer) { slot.textContent = ''; real(); } }, { once: true });
+ sc.addEventListener('load', () => { if (slot.isConnected && root.OpenVibeComposer) { const was = (slot.querySelector('textarea') || {}).value || ''; slot.textContent = ''; real(); if (was) { const ta = slot.querySelector('textarea'); if (ta) { ta.value = was; ta.dispatchEvent(new Event('input', { bubbles: true })); } } } }, { once: true }); // words put in the plain box (Ask your agent) carry over
} else plain();
} else plain();
}
}
/** A new run with these words (Vibe answers, or hands the work on), then its thread. */
/** A new run with these words (Vibe answers, or hands the work on), then its thread. */
async function start(text) {
async function start(text) {
- const run = await api('', { method: 'POST', headers: { 'Idempotency-Key': `ovr-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}` }, body: JSON.stringify({ ...(o.start || {}), text: String(text || '').slice(0, 16000) }) });
- open(run.ref || run.id, run);
+ const words = String(text || '').slice(0, 16000);
+ const gen0 = pending(words);
+ let run;
+ try { run = await api('', { method: 'POST', headers: { 'Idempotency-Key': `ovr-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}` }, body: JSON.stringify({ ...(o.start || {}), text: words }) }); }
+ catch (err) {
+ // It did not start: back to the list, the words back in the box, and why, in plain words.
+ if (gen0 !== state.gen) return null;
+ await list();
+ const foot = box.querySelector('.ovr-list-note'); if (foot) foot.prepend(el('p', 'ovr-note is-err', err.status === 401 ? 'Sign in to talk to your agent.' : (err && err.message) || 'That did not start. Try again.'));
+ const ta = box.querySelector('.ovr-compose textarea'); if (ta && !ta.value) { ta.value = words; ta.dispatchEvent(new Event('input', { bubbles: true })); ta.focus(); }
+ return null;
+ }
+ if (gen0 !== state.gen) return run; // the person went elsewhere while it started
+ open(run.ref || run.id, run, { said: words });
return run;
return run;
}
}
+ /** The thread before the run exists: your words on the right, your agent typing on the left. */
+ function pending(words) {
+ clearStream(); dropTop(); dropPane();
+ const gen = ++state.gen;
+ state.view = 'thread'; state.run = null; state.steps = new Map();
+ box.textContent = ''; box.className = 'ovr is-thread is-pending';
+ const head = el('div', 'ovr-thead');
+ const back = el('button', 'ovr-ib'); back.type = 'button'; back.setAttribute('aria-label', 'Back to your runs'); back.append(svg('back'));
+ back.addEventListener('click', () => list());
+ const who = el('div', 'ovr-who'); who.append(el('b', '', agentName()), el('small', '', 'Starting'));
+ head.append(back, agentFace('s', 'working'), who);
+ const thread = el('div', 'ovr-thread'); thread.setAttribute('role', 'log'); thread.setAttribute('aria-live', 'polite');
+ const me = el('div', 'ovr-step is-you'); const b = el('div', 'ovr-bubble'); b.append(document.createTextNode(words)); me.append(b);
+ thread.append(me, typing());
+ box.append(head, thread);
+ return gen;
+ }
+ function typing() { const t = el('div', 'ovr-typing'); t.setAttribute('aria-label', `${agentName()} is writing`); t.append(el('i'), el('i'), el('i')); return t; }
+
// ── the list ──
// ── the list ──
async function list(filter) {
async function list(filter) {
@@ -337,6 +432,8 @@
@@ -337,6 +432,8 @@
e.preventDefault(); next.focus(); next.click();
e.preventDefault(); next.focus(); next.click();
});
});
head.append(tabs);
head.append(tabs);
+ const me = el('div', 'ovr-agent-slot'); me.hidden = true;
+ if (o.agent) { box.append(me); paintAgent(me); loadAgent().then(() => { if (me.isConnected) paintAgent(me); }); }
const rows = el('ul', 'ovr-rows'); rows.setAttribute('aria-label', 'Your runs');
const rows = el('ul', 'ovr-rows'); rows.setAttribute('aria-label', 'Your runs');
const foot = el('div', 'ovr-list-note');
const foot = el('div', 'ovr-list-note');
const top = el('div', 'ovr-top');
const top = el('div', 'ovr-top');
@@ -353,7 +450,7 @@
@@ -353,7 +450,7 @@
});
});
box.append(st);
box.append(st);
}
}
- composer(box, { placeholder: o.placeholder || 'Ask, or say what to make', onSend: (v) => start(v.text) });
+ composer(box, { placeholder: o.placeholder || (o.agent ? 'Ask, or say what to do' : 'Ask, or say what to make'), onSend: (v) => start(v.text) });
}
}
place({ filter: state.filter });
place({ filter: state.filter });
box.querySelectorAll('.ovr-filter').forEach((b) => { const on = b.dataset.filter === state.filter; b.setAttribute('aria-selected', String(on)); b.tabIndex = on ? 0 : -1; });
box.querySelectorAll('.ovr-filter').forEach((b) => { const on = b.dataset.filter === state.filter; b.setAttribute('aria-selected', String(on)); b.tabIndex = on ? 0 : -1; });
@@ -361,7 +458,7 @@
@@ -361,7 +458,7 @@
// pieces may share one owner (the Dock lends one panel at a time), so the old one goes before the new one comes.
// pieces may share one owner (the Dock lends one panel at a time), so the old one goes before the new one comes.
const pf = paneFor(state.filter);
const pf = paneFor(state.filter);
const topEl = box.querySelector('.ovr-top'), paneEl = box.querySelector('.ovr-pane');
const topEl = box.querySelector('.ovr-top'), paneEl = box.querySelector('.ovr-pane');
- const wantTop = !pf && ['all', 'task'].includes(state.filter) && typeof o.top === 'function';
+ const wantTop = !pf && typeof o.top === 'function'; // 4.240.0: Now (timers) and an agent at work show on every filter
if (!wantTop) { dropTop(); topEl.textContent = ''; }
if (!wantTop) { dropTop(); topEl.textContent = ''; }
if (paneId !== (pf ? state.filter : null)) { dropPane(); paneEl.textContent = ''; }
if (paneId !== (pf ? state.filter : null)) { dropPane(); paneEl.textContent = ''; }
box.classList.toggle('has-pane', !!pf);
box.classList.toggle('has-pane', !!pf);
@@ -394,7 +491,7 @@
@@ -394,7 +491,7 @@
state.runs = state.runs.concat(extra).sort((x, y) => needs(y) - needs(x) || String(y.updated_at || '').localeCompare(String(x.updated_at || '')));
state.runs = state.runs.concat(extra).sort((x, y) => needs(y) - needs(x) || String(y.updated_at || '').localeCompare(String(x.updated_at || '')));
}
}
rows.textContent = ''; rows.removeAttribute('aria-busy');
rows.textContent = ''; rows.removeAttribute('aria-busy');
- if (!state.runs.length) foot.append(empty('Nothing here yet', state.filter === 'code' ? 'Say what to build below: a page, a fix, controls for your stream.' : 'Ask anything below. Chats, tasks and code you start show up here.'));
+ if (!state.runs.length) foot.append(empty('Nothing here yet', state.filter === 'code' ? 'Say what to build below: a page, a fix, controls for your stream. Your agent hands it to Codes.' : o.agent ? `Ask ${agentName()} anything below. It answers, sets timers and reminders, and hands coding and computer work on.` : 'Ask anything below. Chats, tasks and code you start show up here.'));
state.runs.forEach((r, i) => { const li = el('li'); li.append(row(r, i)); const acts = answerRow(r); if (acts) { li.classList.add('has-acts'); li.append(acts); } rows.append(li); });
state.runs.forEach((r, i) => { const li = el('li'); li.append(row(r, i)); const acts = answerRow(r); if (acts) { li.classList.add('has-acts'); li.append(acts); } rows.append(li); });
if (Array.isArray(out.partial) && out.partial.length) foot.append(el('p', 'ovr-note', `Some of your runs did not load (${out.partial.join(', ')}).`));
if (Array.isArray(out.partial) && out.partial.length) foot.append(el('p', 'ovr-note', `Some of your runs did not load (${out.partial.join(', ')}).`));
}
}
@@ -444,7 +541,7 @@
@@ -444,7 +541,7 @@
function empty(title, sub) { const e = el('div', 'ovr-empty'); e.append(svg('spark')); const w = el('div'); w.append(el('b', '', title), el('span', '', sub)); e.append(w); return e; }
function empty(title, sub) { const e = el('div', 'ovr-empty'); e.append(svg('spark')); const w = el('div'); w.append(el('b', '', title), el('span', '', sub)); e.append(w); return e; }
// ── the thread ──
// ── the thread ──
- async function open(ref, known) {
+ async function open(ref, known, how = {}) {
clearStream();
clearStream();
dropTop(); dropPane();
dropTop(); dropPane();
const gen = ++state.gen;
const gen = ++state.gen;
@@ -470,9 +567,17 @@
@@ -470,9 +567,17 @@
box.classList.toggle('is-needs', needsPerson(r));
box.classList.toggle('is-needs', needsPerson(r));
};
};
if (known) paintHead(known);
if (known) paintHead(known);
+ // A run your agent handed on (it started from the one box and is not a chat) says where it went.
+ const WENT = { code: ['code', 'Sent to Codes: it works on the code and shows you the change'], task: ['robot', `Sent to ${agentName()}'s tasks: it works on it and asks before anything that matters`], machine: ['tool', 'Sent to your computer: it runs there and shows you each step'], goal: ['spark', 'A goal: your agent plans the steps'] };
+ if (how.said && known && WENT[known.kind]) { const w = el('p', 'ovr-went'); w.append(svg(WENT[known.kind][0]), el('span', '', WENT[known.kind][1])); thread.append(w); }
+ let wait = null;
+ if (how.said) { const me = el('div', 'ovr-step is-you is-said'); const b = el('div', 'ovr-bubble'); b.append(document.createTextNode(how.said)); me.append(b); thread.append(me); wait = typing(); thread.append(wait); }
composer(box, { placeholder: 'Say the next thing', onSend: async (v) => { const step = await api(`/${encodeURIComponent(ref)}/messages`, { method: 'POST', body: JSON.stringify({ text: v.text }) }); add(step); } });
composer(box, { placeholder: 'Say the next thing', onSend: async (v) => { const step = await api(`/${encodeURIComponent(ref)}/messages`, { method: 'POST', body: JSON.stringify({ text: v.text }) }); add(step); } });
const add = (s) => {
const add = (s) => {
if (!s || !Number.isInteger(s.seq)) return;
if (!s || !Number.isInteger(s.seq)) return;
+ // Your words came back as a step: the stand-in goes. Your agent's first words: the typing goes.
+ if (s.kind === 'message' && (s.from === 'person') && how.said) { const said = thread.querySelector(':scope > .is-said'); if (said) said.remove(); }
+ if (wait && !(s.kind === 'message' && s.from === 'person')) { wait.remove(); wait = null; }
if (state.steps.has(s.seq)) { const old = state.steps.get(s.seq); const n = stepNode(s); old.replaceWith(n); state.steps.set(s.seq, n); return; }
if (state.steps.has(s.seq)) { const old = state.steps.get(s.seq); const n = stepNode(s); old.replaceWith(n); state.steps.set(s.seq, n); return; }
const wasNear = thread.scrollHeight - thread.scrollTop - thread.clientHeight < 160;
const wasNear = thread.scrollHeight - thread.scrollTop - thread.clientHeight < 160;
const none = thread.querySelector(':scope > .ovr-none'); if (none) none.remove();
const none = thread.querySelector(':scope > .ovr-none'); if (none) none.remove();
@@ -488,7 +593,8 @@
@@ -488,7 +593,8 @@
paintHead(r);
paintHead(r);
(page.steps || []).forEach(add);
(page.steps || []).forEach(add);
requestAnimationFrame(() => { thread.scrollTop = thread.scrollHeight; });
requestAnimationFrame(() => { thread.scrollTop = thread.scrollHeight; });
- if (!(page.steps || []).length) thread.append(el('p', 'ovr-note ovr-none', 'Nothing here yet.'));
+ if (!(page.steps || []).length && !how.said) thread.append(el('p', 'ovr-note ovr-none', 'Nothing here yet.'));
+ if (wait) thread.append(wait); // still writing: the dots stay last
} catch (err) { if (gen !== state.gen) return; thread.append(empty('This run did not load', err.message)); return; }
} catch (err) { if (gen !== state.gen) return; thread.append(empty('This run did not load', err.message)); return; }
if (isEnd(state.run)) return;
if (isEnd(state.run)) return;
const last = Math.max(0, ...state.steps.keys());
const last = Math.max(0, ...state.steps.keys());
@@ -598,7 +704,8 @@
@@ -598,7 +704,8 @@
.ovr { --ovr-gap: 12px; display: flex; flex-direction: column; min-height: 0; height: 100%; font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: var(--text-primary, #e6edf7); }
.ovr { --ovr-gap: 12px; display: flex; flex-direction: column; min-height: 0; height: 100%; font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: var(--text-primary, #e6edf7); }
.ovr svg { width: 18px; height: 18px; flex: none; fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
.ovr svg { width: 18px; height: 18px; flex: none; fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
.ovr-head { padding: 4px 4px 8px; }
.ovr-head { padding: 4px 4px 8px; }
-.ovr-filters { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 4px; padding: 4px; border-radius: 16px; background: color-mix(in srgb, var(--text-primary, #e6edf7) 5%, transparent); }
+.ovr-filters { display: flex; gap: 6px; overflow-x: auto; padding: 2px 2px 4px; scrollbar-width: none; }
+.ovr-filters::-webkit-scrollbar { display: none; }
.ovr-filter { all: unset; box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; gap: 7px; min-height: 42px; border-radius: 12px; cursor: pointer; font-family: 'OV Brand', Nunito, Inter, sans-serif; font-weight: 800; font-size: 14px; color: var(--text-secondary, #a8b3c4); transition: background-color .18s, color .18s; }
.ovr-filter { all: unset; box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; gap: 7px; min-height: 42px; border-radius: 12px; cursor: pointer; font-family: 'OV Brand', Nunito, Inter, sans-serif; font-weight: 800; font-size: 14px; color: var(--text-secondary, #a8b3c4); transition: background-color .18s, color .18s; }
.ovr-filter[aria-selected="true"] { color: var(--text-primary, #e6edf7); background: var(--bg-secondary, #121826); box-shadow: 0 1px 2px rgba(0,0,0,.3), inset 0 0 0 1px color-mix(in srgb, var(--accent, #3b82f6) 35%, transparent); }
.ovr-filter[aria-selected="true"] { color: var(--text-primary, #e6edf7); background: var(--bg-secondary, #121826); box-shadow: 0 1px 2px rgba(0,0,0,.3), inset 0 0 0 1px color-mix(in srgb, var(--accent, #3b82f6) 35%, transparent); }
.ovr-filter[aria-selected="true"] svg { color: var(--accent-light, var(--accent, #60a5fa)); }
.ovr-filter[aria-selected="true"] svg { color: var(--accent-light, var(--accent, #60a5fa)); }
@@ -723,6 +830,44 @@ a.ovr-row { color: inherit; text-decoration: none; }
@@ -723,6 +830,44 @@ a.ovr-row { color: inherit; text-decoration: none; }
.ovr-btn.is-primary { color: var(--on-accent-strong, #fff); background: var(--accent-strong, var(--accent, #2563eb)); } .ovr-btn:disabled { opacity: .5; cursor: default; }
.ovr-btn.is-primary { color: var(--on-accent-strong, #fff); background: var(--accent-strong, var(--accent, #2563eb)); } .ovr-btn:disabled { opacity: .5; cursor: default; }
.ovr-files { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; } .ovr-step.is-you .ovr-files { justify-content: flex-end; }
.ovr-files { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; } .ovr-step.is-you .ovr-files { justify-content: flex-end; }
.ovr-thumb { width: 120px; height: 90px; object-fit: cover; border-radius: 12px; }
.ovr-thumb { width: 120px; height: 90px; object-fit: cover; border-radius: 12px; }
+/* 4.240.0: one agent. The filters are chips over one list; your agent heads it; the thread opens at once. */
+.ovr-filter { flex: none; min-height: 36px !important; padding: 0 13px 0 11px; border-radius: 999px !important; font-size: 13.5px !important; gap: 6px !important; border: 1px solid color-mix(in srgb, var(--text-primary, #e6edf7) 12%, transparent); background: transparent !important; box-shadow: none !important; transition: background-color .2s, color .2s, border-color .2s, transform .18s cubic-bezier(.3,1.4,.4,1); }
+.ovr-filter svg { width: 16px !important; height: 16px !important; }
+.ovr-filter[aria-selected="true"] { color: var(--accent-light, #93c5fd) !important; border-color: color-mix(in srgb, var(--accent, #3b82f6) 55%, transparent); background: color-mix(in srgb, var(--accent, #3b82f6) 16%, transparent) !important; }
+.ovr-filter:active { transform: scale(.94); }
+.ovr-agent-slot { flex: none; width: 100%; max-width: 780px; margin-inline: auto; box-sizing: border-box; }
+.ovr-agent-slot[hidden] { display: none; }
+.ovr-agent { display: flex; align-items: center; gap: 8px; padding: 8px 8px 8px 6px; border-radius: 20px; background: linear-gradient(135deg, color-mix(in srgb, hsl(var(--ovr-ah, 220) 70% 55%) 14%, transparent), color-mix(in srgb, var(--text-primary, #e6edf7) 3%, transparent)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text-primary, #e6edf7) 9%, transparent); animation: ovr-rise .32s cubic-bezier(.2,.9,.3,1.1) both; }
+.ovr-agent-who { all: unset; box-sizing: border-box; flex: 1; min-width: 0; display: flex; align-items: center; gap: 12px; min-height: 52px; padding: 2px 6px; border-radius: 14px; cursor: pointer; }
+.ovr-agent-who:focus-visible, .ovr-agent-go:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 2px; }
+.ovr-agent-t { min-width: 0; display: grid; gap: 2px; }
+.ovr-agent-top { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
+.ovr-agent-top b { font: 800 16px/1.2 'OV Brand', Nunito, Inter, sans-serif; color: var(--text-primary, #e6edf7); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.ovr-agent-tag { font-style: normal; font-size: 12px; font-weight: 650; color: var(--text-muted, #8b98ad); white-space: nowrap; }
+.ovr-agent-line { font-size: 13px; color: var(--text-secondary, #a8b3c4); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.ovr-agent.is-asks .ovr-agent-line { color: #fbbf24; }
+.ovr-agent-go { all: unset; box-sizing: border-box; flex: none; display: grid; place-items: center; width: 44px; height: 44px; border-radius: 50%; cursor: pointer; color: var(--text-secondary, #a8b3c4); background: color-mix(in srgb, var(--text-primary, #e6edf7) 7%, transparent); transition: transform .18s cubic-bezier(.3,1.4,.4,1), background-color .2s; }
+.ovr-agent-go svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 2.4; stroke-linecap: round; }
+.ovr-agent-go.is-main { color: var(--on-accent, #fff); background: var(--accent, #3b82f6); box-shadow: 0 6px 18px -6px color-mix(in srgb, var(--accent, #3b82f6) 70%, transparent); }
+.ovr-agent-go:active { transform: scale(.9); }
+.ovr-agent-go:disabled { opacity: .5; }
+.ovr-face.ovr-me { position: relative; }
+.ovr-face.ovr-me::after { content: ""; position: absolute; right: -1px; bottom: -1px; width: 11px; height: 11px; border-radius: 50%; border: 2px solid var(--bg-secondary, #121826); background: #64748b; }
+.ovr-face.ovr-me.is-resting::after { background: #22c55e; }
+.ovr-face.ovr-me.is-working::after { background: var(--accent, #3b82f6); animation: ovr-pulse 1.2s ease-in-out infinite; }
+.ovr-face.ovr-me.is-asks::after { background: #f59e0b; }
+.ovr-face.ovr-me.is-working { animation: ovr-breathe 2.2s ease-in-out infinite; }
+.ovr-face.ovr-me.is-paused { filter: saturate(.4); opacity: .8; }
+.ovr-typing { align-self: flex-start; display: inline-flex; gap: 5px; padding: 12px 14px; margin: 2px 0 6px; border-radius: 18px 18px 18px 6px; background: color-mix(in srgb, var(--text-primary, #e6edf7) 7%, transparent); animation: ovr-rise .25s ease both; }
+.ovr-typing i { width: 7px; height: 7px; border-radius: 50%; background: var(--text-secondary, #a8b3c4); animation: ovr-dot 1.1s ease-in-out infinite; }
+.ovr-typing i:nth-child(2) { animation-delay: .15s; } .ovr-typing i:nth-child(3) { animation-delay: .3s; }
+@keyframes ovr-dot { 0%, 60%, 100% { transform: translateY(0); opacity: .45; } 30% { transform: translateY(-4px); opacity: 1; } }
+.ovr.is-pending .ovr-step.is-you, .ovr-step.is-said { animation: ovr-send .32s cubic-bezier(.2,.9,.3,1.1) both; }
+@keyframes ovr-send { from { opacity: 0; transform: translateY(18px) scale(.96); } }
+.ovr-went { display: flex; align-items: center; gap: 8px; align-self: center; max-width: 92%; margin: 4px auto 8px; padding: 7px 12px; border-radius: 999px; font-size: 12.5px; font-weight: 650; color: var(--accent-light, #93c5fd); background: color-mix(in srgb, var(--accent, #3b82f6) 12%, transparent); animation: ovr-rise .3s ease both; }
+.ovr-went svg { flex: none; width: 15px; height: 15px; }
+html[data-ov-motion=reduced] .ovr-typing i, html[data-ov-motion=reduced] .ovr-face.ovr-me.is-working { animation-duration: 3s; }
+html[data-ov-motion=off] :is(.ovr-agent, .ovr-typing, .ovr-typing i, .ovr-went, .ovr-step.is-said, .ovr-face.ovr-me, .ovr-face.ovr-me::after) { animation: none !important; }
@keyframes ovr-rise { from { opacity: 0; transform: translateY(6px); } }
@keyframes ovr-rise { from { opacity: 0; transform: translateY(6px); } }
@keyframes ovr-shimmer { 50% { opacity: .55; } }
@keyframes ovr-shimmer { 50% { opacity: .55; } }
@keyframes ovr-breathe { 50% { box-shadow: 0 0 0 5px color-mix(in srgb, var(--accent, #3b82f6) 22%, transparent); } }
@keyframes ovr-breathe { 50% { box-shadow: 0 0 0 5px color-mix(in srgb, var(--accent, #3b82f6) 22%, transparent); } }
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.239.0",
+ "version": "4.240.0",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.239.0",
+ "version": "4.240.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.239.0",
+ "version": "4.240.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": {
@@ -1651,12 +1651,12 @@ html[data-ov-motion=off] .ovs-youpanel { animation: none; }
@@ -1651,12 +1651,12 @@ html[data-ov-motion=off] .ovs-youpanel { animation: none; }
const D = () => root.OpenVibeDock;
const D = () => root.OpenVibeDock;
const opts = {
const opts = {
base: `${net}/api/frame`, token: () => ctx.token, me, sibling: ctx.sibling, filter: runFilter,
base: `${net}/api/frame`, token: () => ctx.token, me, sibling: ctx.sibling, filter: runFilter,
- // All: every run, with the Action Center's work from every product, the Dock's Now, your Actor and the live
- // agent view above it, and Vibe's starters. Chats, Tasks and Code are still the Dock's own panels (its Vibe,
- // Agents and Code: their history, skills, voice, undo, repos), lent whole until the one view does all they do.
- actions: { base: net }, starters: VIBE_STARTERS,
- top: (slot) => lend(slot, { tab: 'agent', part: 'top' }),
- panes: { chat: (slot) => lend(slot, { tab: 'vibe' }), task: (slot) => lend(slot, { tab: 'agent' }), code: (slot) => lend(slot, { tab: 'code' }) },
+ // 4.240.0 (the owner, 11-13: "it's just like filters … your actor is just your agent"): one view. Your agent
+ // heads the list, Chats, Tasks and Code are filters over the same runs, and one box starts everything; the
+ // Dock's Vibe, Agents and Code panels are no longer lent here.
+ actions: { base: net }, starters: VIBE_STARTERS, agent: true,
+ // Above the list: Now (timers, what is on) and the live view of an agent at work, from the Dock's agent top.
+ top: (slot) => lend(slot, { tab: 'agent', part: 'live' }),
onPlace: (p) => { if (p.filter) runFilter = p.filter; },
onPlace: (p) => { if (p.filter) runFilter = p.filter; },
};
};
const go = (R) => {
const go = (R) => {
@@ -126,6 +126,19 @@ const server = http.createServer((req, res) => {
@@ -126,6 +126,19 @@ const server = http.createServer((req, res) => {
return json(200, { state: scenario.state || defaultState(), counts: scenario.counts || { running: 0, waiting: 0, unread: 0 }, sources_partial: false });
return json(200, { state: scenario.state || defaultState(), counts: scenario.counts || { running: 0, waiting: 0, unread: 0 }, sources_partial: false });
}
}
if (u.pathname === '/api/v1/me/actions') return json(200, scenario.actions || ACTIONS);
if (u.pathname === '/api/v1/me/actions') return json(200, scenario.actions || ACTIONS);
+ // 4.240.0: one agent. A run starts through the Frame with the words alone; its thread and stream follow.
+ if (u.pathname === '/api/frame/runs' && req.method === 'POST') {
+ const b = JSON.parse(Buffer.concat(chunks).toString() || '{}');
+ asked.push({ message: b.text, kind: b.kind || null });
+ return json(201, { id: 'c_new', ref: 'chat:c_new', kind: 'chat', title: String(b.text || '').slice(0, 60), status: 'running', agent: { id: 'vibe', name: 'Vibe', glyph: 'spark' }, updated_at: new Date().toISOString() });
+ }
+ if (decodeURIComponent(u.pathname) === '/api/frame/runs/chat:c_new') return json(200, { id: 'c_new', ref: 'chat:c_new', kind: 'chat', title: (asked.at(-1) || {}).message || 'Chat', status: 'waiting', waiting: { for: 'message' }, agent: { id: 'vibe', name: 'Vibe', glyph: 'spark' }, updated_at: new Date().toISOString() });
+ if (decodeURIComponent(u.pathname) === '/api/frame/runs/chat:c_new/steps') return json(200, { steps: [
+ { seq: 1, kind: 'message', from: 'person', text: (asked.at(-1) || {}).message || '' },
+ { seq: 2, kind: 'message', from: 'agent', text: 'Reminder set: Stretch, every 2 hours from 9 am to 5 pm.', ui: { type: 'results', data: { results: [{ kind: 'reminder', label: 'Stretch, every 2 hours' }] } } },
+ ], next: null });
+ if (decodeURIComponent(u.pathname) === '/api/frame/runs/chat:c_new/events') { res.writeHead(200, { 'content-type': 'text/event-stream' }); return res.end('event: end\ndata: {"status":"waiting"}\n\n'); }
+ if (u.pathname === '/api/frame/actor') return json(200, { id: 'act_1', name: 'Juniper', status: 'resting', enabled: true, asks: [], now: { cost_today: 0.12 }, caps: { usd: 1 } });
// 4.225.0: the menu's Vibe lists runs from the Frame (Network /api/frame/runs, agents.run@1).
// 4.225.0: the menu's Vibe lists runs from the Frame (Network /api/frame/runs, agents.run@1).
if (u.pathname === '/api/frame/runs' && req.method === 'GET') {
if (u.pathname === '/api/frame/runs' && req.method === 'GET') {
assert.strictEqual(req.headers.authorization, 'Bearer tok_user', 'the runs are read with the person\'s own token');
assert.strictEqual(req.headers.authorization, 'Bearer tok_user', 'the runs are read with the person\'s own token');
@@ -320,22 +333,23 @@ const NO_FLOATING = "!document.querySelector('#ovdk-pill, .ovdk-pill') && [...do
@@ -320,22 +333,23 @@ const NO_FLOATING = "!document.querySelector('#ovdk-pill, .ovdk-pill') && [...do
const st = await until(p, STATUS, 'the status');
const st = await until(p, STATUS, 'the status');
assert.deepStrictEqual([st.tone, st.label], ['waiting', '1 needs you'], 'what needs you comes first');
assert.deepStrictEqual([st.tone, st.label], ['waiting', '1 needs you'], 'what needs you comes first');
await p.evaluate(MENU_BUTTON);
await p.evaluate(MENU_BUTTON);
- const place = await until(p, `(() => { const c = ${PLACE}; return c && c.items ? c : null; })()`, 'the menu, open on Agents');
- assert.deepStrictEqual([place.tab, place.groups, place.items], ['agent', ['waiting', 'running'], 3], 'what needs you first, then what runs');
+ // 4.240.0 (one agent): the menu opens on Vibe with Tasks chosen, one list where what needs you comes first; no
+ // Dock panel is lent into it.
+ const place = await until(p, `(() => { const f = document.querySelector('.ovs.is-open .ovs-door-host .ovr-filter[aria-selected="true"]'); const rows = [...document.querySelectorAll('.ovs-door-host .ovr-rows .ovr-row:not(.is-ghost)')]; return f && rows.length >= 3 ? { filter: f.dataset.filter, needs: rows.map((r) => r.classList.contains('is-needs')), lent: !!document.querySelector('.ovs-door-host .ovdk .ovac-item, .ovs-door-host .ovr-pane .ovdk') } : null; })()`, 'the menu, open on your agent\'s tasks');
+ assert.deepStrictEqual([place.filter, place.needs[0], place.lent], ['task', true, false], 'Tasks chosen, what needs you first, and no Dock panel lent');
assert.strictEqual(await p.evaluate(NO_FLOATING), true, 'inside the menu, nothing floating');
assert.strictEqual(await p.evaluate(NO_FLOATING), true, 'inside the menu, nothing floating');
- await p.evaluate("document.querySelector('.ovdk .ovac-item[data-bucket=waiting] button[data-do=approve]').click()");
- await until(p, "document.querySelector('.ovdk .ovac-item[data-bucket=waiting] .primary').textContent === 'Approved'", 'approved in place');
+ await until(p, "!!document.querySelector('.ovs-door-host .ovr-rows li.has-acts .ovr-btn.is-primary')", 'its answer buttons');
+ decided.length = 0;
+ await p.evaluate("document.querySelector('.ovs-door-host .ovr-rows li.has-acts .ovr-btn.is-primary').click()");
+ await until(p, "(document.querySelector('.ovs-door-host .ovr-rows li.has-acts .ovr-row-acts') || {}).textContent === 'Approved'", 'approved in place');
assert.deepStrictEqual(decided, ['actor:apr_1:approve'], 'the approval goes to the Network for that item');
assert.deepStrictEqual(decided, ['actor:apr_1:approve'], 'the approval goes to the Network for that item');
await p.evaluate("document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true }))");
await p.evaluate("document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true }))");
await until(p, "!document.querySelector('.ovs.is-open')", 'Escape closes the menu');
await until(p, "!document.querySelector('.ovs.is-open')", 'Escape closes the menu');
- await eventually(async () => assert.ok(puts.some((b) => b.tab === 'agent'), `the Network heard the place (${JSON.stringify(puts)})`));
- assert.ok(puts.length <= 3, `changes go as debounced PUTs, not one per click (${puts.length})`);
await p.close();
await p.close();
}
}
- // ── 4.225.0: the menu's Vibe is one list (ov-run.js) under All · Chats · Tasks · Code: runs from the Frame and the
- // Action Center's work together, the Dock's Now, Actor and live view above; Chats, Tasks and Code are the Dock's
- // own panels, lent whole. No Chat · Code · Agents switch. ──
+ // ── 4.240.0: the menu's Vibe is one agent: one list (ov-run.js) with All · Chats · Tasks · Code as filters over it,
+ // runs from the Frame and the Action Center's work together, your agent at the top; no Dock panel lent. ──
{
{
scenario = { counts: { running: 0, waiting: 0, unread: 0 } };
scenario = { counts: { running: 0, waiting: 0, unread: 0 } };
agentView = null;
agentView = null;
@@ -348,19 +362,17 @@ const NO_FLOATING = "!document.querySelector('#ovdk-pill, .ovdk-pill') && [...do
@@ -348,19 +362,17 @@ const NO_FLOATING = "!document.querySelector('#ovdk-pill, .ovdk-pill') && [...do
await until(p, "document.querySelectorAll('.ovs-door-host .ovr-rows .ovr-row:not(.is-ghost)').length === 4", 'the run and the Action Center\'s three');
await until(p, "document.querySelectorAll('.ovs-door-host .ovr-rows .ovr-row:not(.is-ghost)').length === 4", 'the run and the Action Center\'s three');
const all = await p.evaluate(`({ filters: [...document.querySelectorAll('.ovs-door-host .ovr-filter')].map((b) => b.textContent + (b.getAttribute('aria-selected') === 'true' ? '*' : '')),
const all = await p.evaluate(`({ filters: [...document.querySelectorAll('.ovs-door-host .ovr-filter')].map((b) => b.textContent + (b.getAttribute('aria-selected') === 'true' ? '*' : '')),
titles: [...document.querySelectorAll('.ovs-door-host .ovr-row-top b')].map((b) => b.textContent), sw: !!document.querySelector('.ovs-door-sw, [data-part]'),
titles: [...document.querySelectorAll('.ovs-door-host .ovr-row-top b')].map((b) => b.textContent), sw: !!document.querySelector('.ovs-door-sw, [data-part]'),
- top: !!document.querySelector('.ovs-door-host .ovr-top .ovdk.ovdk-part-top'), runsList: !!document.querySelector('.ovs-door-host .ovr-top .ovdk-scroll, .ovs-door-host .ovr-top .ovac-item'),
+ top: !!document.querySelector('.ovs-door-host .ovr-agent'), runsList: !!document.querySelector('.ovs-door-host .ovdk .ovac-item, .ovs-door-host .ovdk .ovdk-actor, .ovs-door-host .ovr-pane .ovdk'),
starters: document.querySelectorAll('.ovs-door-host .ovr-starter').length, wide: document.documentElement.scrollWidth > innerWidth })`);
starters: document.querySelectorAll('.ovs-door-host .ovr-starter').length, wide: document.documentElement.scrollWidth > innerWidth })`);
assert.deepStrictEqual(all.filters, ['All*', 'Chats', 'Tasks', 'Code'], 'All · Chats · Tasks · Code, on All');
assert.deepStrictEqual(all.filters, ['All*', 'Chats', 'Tasks', 'Code'], 'All · Chats · Tasks · Code, on All');
assert.deepStrictEqual(all.titles, ['Send the weekly digest?', 'Fix the flaky upload test', 'Compare three VPS offers', 'Patch notes for 4.162'], 'what needs you first, then newest (the work a minute ago, the chat four): a run and the Action Center\'s work in one list');
assert.deepStrictEqual(all.titles, ['Send the weekly digest?', 'Fix the flaky upload test', 'Compare three VPS offers', 'Patch notes for 4.162'], 'what needs you first, then newest (the work a minute ago, the chat four): a run and the Action Center\'s work in one list');
- assert.deepStrictEqual([all.sw, all.top, all.runsList, all.starters, all.wide], [false, true, false, 6, false], 'no switch; the Dock\'s top above (not its list); the six starters; nothing off the screen');
+ assert.deepStrictEqual([all.sw, all.top, all.runsList, all.starters, all.wide], [false, true, false, 6, false], 'no switch; your agent above, no Dock list or Actor card; the six starters; nothing off the screen');
if (process.env.OV_CAPTURE_VIBE) { await wait(500); const r = await p.send('Page.captureScreenshot', { format: 'png' }); fs.writeFileSync(path.join(process.env.OV_CAPTURE_VIBE, 'vibe-all-390.png'), Buffer.from(r.data, 'base64')); }
if (process.env.OV_CAPTURE_VIBE) { await wait(500); const r = await p.send('Page.captureScreenshot', { format: 'png' }); fs.writeFileSync(path.join(process.env.OV_CAPTURE_VIBE, 'vibe-all-390.png'), Buffer.from(r.data, 'base64')); }
- // Tasks: the Dock's Agents panel, whole; the Network hears the place.
+ // Tasks: a filter over the same list, never a panel of its own.
await p.evaluate(`document.querySelector('.ovs-door-host .ovr-filter[data-filter="task"]').click()`);
await p.evaluate(`document.querySelector('.ovs-door-host .ovr-filter[data-filter="task"]').click()`);
- const place = await until(p, `(() => { const c = ${PLACE}; return c && c.tab === 'agent' && c.items ? c : null; })()`, 'Tasks: the Agents panel');
- assert.strictEqual(place.items, 3, 'its own list, as before');
- assert.strictEqual(await p.evaluate("!!document.querySelector('.ovs-door-host .ovr-pane .ovdk') && document.querySelector('.ovs-door-host .ovr-top').hidden"), true, 'in the pane, and the top handed back');
+ await until(p, "document.querySelector('.ovs-door-host .ovr-filter[data-filter=task]').getAttribute('aria-selected') === 'true' && document.querySelectorAll('.ovs-door-host .ovr-rows .ovr-row:not(.is-ghost)').length >= 3", 'Tasks: the same list, filtered');
+ assert.strictEqual(await p.evaluate("!document.querySelector('.ovs-door-host .ovr-pane .ovdk') && !document.querySelector('.ovs-door-host .ovdk .ovac-item')"), true, 'no Dock panel');
if (process.env.OV_CAPTURE_VIBE) { await wait(500); const r = await p.send('Page.captureScreenshot', { format: 'png' }); fs.writeFileSync(path.join(process.env.OV_CAPTURE_VIBE, 'vibe-tasks-390.png'), Buffer.from(r.data, 'base64')); }
if (process.env.OV_CAPTURE_VIBE) { await wait(500); const r = await p.send('Page.captureScreenshot', { format: 'png' }); fs.writeFileSync(path.join(process.env.OV_CAPTURE_VIBE, 'vibe-tasks-390.png'), Buffer.from(r.data, 'base64')); }
- await eventually(async () => assert.ok(puts.some((b) => b.tab === 'agent'), 'the Network heard Tasks'));
// Approve in place from All, through the Action Center.
// Approve in place from All, through the Action Center.
await p.evaluate(`document.querySelector('.ovs-door-host .ovr-filter[data-filter="all"]').click()`);
await p.evaluate(`document.querySelector('.ovs-door-host .ovr-filter[data-filter="all"]').click()`);
await until(p, "!!document.querySelector('.ovs-door-host .ovr-rows li.has-acts .ovr-btn.is-primary')", 'All again, with its answers');
await until(p, "!!document.querySelector('.ovs-door-host .ovr-rows li.has-acts .ovr-btn.is-primary')", 'All again, with its answers');
@@ -481,7 +493,8 @@ const NO_FLOATING = "!document.querySelector('#ovdk-pill, .ovdk-pill') && [...do
@@ -481,7 +493,8 @@ const NO_FLOATING = "!document.querySelector('#ovdk-pill, .ovdk-pill') && [...do
await p.goto(`${base}/page`, { maxMs: 8000 });
await p.goto(`${base}/page`, { maxMs: 8000 });
await until(p, "!!window.OpenVibeDock", 'the panels');
await until(p, "!!window.OpenVibeDock", 'the panels');
await p.evaluate(OPEN('agent'));
await p.evaluate(OPEN('agent'));
- await until(p, `(() => { const c = ${PLACE}; return c && c.tab === 'agent' && c.items ? c : null; })()`, 'the Agents place');
+ // 4.240.0: the agent at work shows above your one list in the menu's Vibe (Tasks chosen).
+ await until(p, "!!document.querySelector('.ovs-door-host .ovr-top .ovdk-agent')", 'the Agents place');
// The stream: Live's embed, the task, the numbers.
// The stream: Live's embed, the task, the numbers.
const src = await until(p, "document.querySelector('.ovda iframe') && document.querySelector('.ovda iframe').getAttribute('src')", 'the embed');
const src = await until(p, "document.querySelector('.ovda iframe') && document.querySelector('.ovda iframe').getAttribute('src')", 'the embed');
assert.strictEqual(src, view().embed_url, 'the embed Codes answered, with the ticket in its address');
assert.strictEqual(src, view().embed_url, 'the embed Codes answered, with the ticket in its address');
@@ -507,16 +520,16 @@ const NO_FLOATING = "!document.querySelector('#ovdk-pill, .ovdk-pill') && [...do
@@ -507,16 +520,16 @@ const NO_FLOATING = "!document.querySelector('#ovdk-pill, .ovdk-pill') && [...do
assert.deepStrictEqual(controls, ['curtain', 'pause', 'mute', 'end'], 'Pause, Mute and End reached Codes, which puts them to Live');
assert.deepStrictEqual(controls, ['curtain', 'pause', 'mute', 'end'], 'Pause, Mute and End reached Codes, which puts them to Live');
await eventually(async () => assert.strictEqual(await p.evaluate("!!document.querySelector('.ovda iframe')"), false, 'no player once it has ended'));
await eventually(async () => assert.strictEqual(await p.evaluate("!!document.querySelector('.ovda iframe')"), false, 'no player once it has ended'));
await eventually(async () => assert.strictEqual(await p.evaluate("document.querySelector('.ovda-meta').textContent"), 'Ended · worked 12 min'));
await eventually(async () => assert.strictEqual(await p.evaluate("document.querySelector('.ovda-meta').textContent"), 'Ended · worked 12 min'));
- await eventually(async () => assert.strictEqual(await p.evaluate("document.querySelectorAll('.ovac-item').length"), 3, 'the agents list is untouched under the stream'));
+ await eventually(async () => assert.ok(await p.evaluate("document.querySelectorAll('.ovs-door-host .ovr-rows .ovr-row:not(.is-ghost)').length >= 3"), 'the one list is untouched under the stream'));
// No stream at all: the view hides itself and the tab is just the list.
// No stream at all: the view hides itself and the tab is just the list.
agentView = null;
agentView = null;
await p.goto(`${base}/page`, { maxMs: 8000 });
await p.goto(`${base}/page`, { maxMs: 8000 });
await until(p, "!!window.OpenVibeDock", 'the panels again');
await until(p, "!!window.OpenVibeDock", 'the panels again');
await p.evaluate(OPEN('agent'));
await p.evaluate(OPEN('agent'));
- await until(p, `(() => { const c = ${PLACE}; return c && c.tab === 'agent' && c.items ? c : null; })()`, 'the Agents place again');
+ await until(p, "!!document.querySelector('.ovs-door-host .ovr-top .ovdk-agent') && document.querySelectorAll('.ovs-door-host .ovr-rows .ovr-row:not(.is-ghost)').length >= 3", 'the Agents place again');
await wait(400);
await wait(400);
await eventually(async () => assert.strictEqual(await p.evaluate("document.querySelector('.ovda').classList.contains('on')"), false, 'nothing to show, and nowhere to show it'));
await eventually(async () => assert.strictEqual(await p.evaluate("document.querySelector('.ovda').classList.contains('on')"), false, 'nothing to show, and nowhere to show it'));
- await eventually(async () => assert.strictEqual(await p.evaluate("document.querySelectorAll('.ovac-item').length"), 3, 'the list alone'));
+ await eventually(async () => assert.ok(await p.evaluate("document.querySelectorAll('.ovs-door-host .ovr-rows .ovr-row:not(.is-ghost)').length >= 3"), 'the list alone'));
await p.close();
await p.close();
}
}
@@ -528,80 +541,36 @@ const NO_FLOATING = "!document.querySelector('#ovdk-pill, .ovdk-pill') && [...do
@@ -528,80 +541,36 @@ const NO_FLOATING = "!document.querySelector('#ovdk-pill, .ovdk-pill') && [...do
const st = await until(p, STATUS, 'the status at 320');
const st = await until(p, STATUS, 'the status at 320');
assert.strictEqual(st.tone, 'running');
assert.strictEqual(st.tone, 'running');
await p.evaluate(OPEN('agent'));
await p.evaluate(OPEN('agent'));
- await until(p, PLACE, 'the menu on Agents at 320');
+ await until(p, "!!document.querySelector('.ovs-door-host .ovr-rows .ovr-row:not(.is-ghost)')", 'the menu on Agents at 320');
await eventually(async () => assert.strictEqual(await p.evaluate('document.documentElement.scrollWidth'), 320, 'no sideways scroll'));
await eventually(async () => assert.strictEqual(await p.evaluate('document.documentElement.scrollWidth'), 320, 'no sideways scroll'));
await p.close();
await p.close();
}
}
}
}
if (PART === 1) { console.log('dock-chrome (1 of 2): no pill and no floating Dock, the status on the menu button, Agents in the menu, Chats, Send to, the agent view, 320 px clean'); return; }
if (PART === 1) { console.log('dock-chrome (1 of 2): no pill and no floating Dock, the status on the menu button, Agents in the menu, Chats, Send to, the agent view, 320 px clean'); return; }
- // ── Vibe (L48 V2): starters, a question, Vibe's answer in place; the Network keeps the tab. ──
+ // ── 4.240.0, one agent: your agent heads the menu's Vibe; a starter starts a conversation at once (your words, the
+ // typing), the Frame's run carries Vibe's answer and what it did. The Dock's own Vibe panel is not used. ──
{
{
- scenario = { counts: { running: 0, waiting: 0, unread: 0 }, state: { ...defaultState(), pinned: true } };
- puts.length = 0;
- const p = await fresh(1280, 860);
+ scenario = { counts: { running: 0, waiting: 0, unread: 0 }, state: { ...defaultState() } };
+ asked.length = 0;
+ const p = await fresh(390, 780);
await p.goto(`${base}/page`, { maxMs: 5000 });
await p.goto(`${base}/page`, { maxMs: 5000 });
await until(p, "!!window.OpenVibeDock", 'the panels');
await until(p, "!!window.OpenVibeDock", 'the panels');
await p.evaluate(OPEN('vibe'));
await p.evaluate(OPEN('vibe'));
await until(p, "document.querySelector('.ovs.is-open [data-dock=\"vibe\"]') && document.querySelector('.ovs [data-dock=\"vibe\"]').getAttribute('aria-selected') === 'true'", 'the menu on Vibe');
await until(p, "document.querySelector('.ovs.is-open [data-dock=\"vibe\"]') && document.querySelector('.ovs [data-dock=\"vibe\"]').getAttribute('aria-selected') === 'true'", 'the menu on Vibe');
- await until(p, "!!document.querySelector('.ovdk-vibe-new')", 'the conversation list');
- await p.evaluate("document.querySelector('.ovdk-vibe-new').click()");
- await until(p, "document.querySelectorAll('.ovdk [data-panel=vibe] .ovdk-chip').length === 6", 'the starters before the first message, each with its icon');
- assert.strictEqual(await p.evaluate("[...document.querySelectorAll('.ovdk [data-panel=vibe] .ovdk-chip')].filter((c) => c.querySelector('.ovdk-chip-ic svg')).length"), 6, 'every starter has its icon');
- await p.evaluate("document.querySelector('.ovdk [data-panel=vibe] .ovdk-chip').click()");
- await until(p, "!!document.querySelector('.ovdk [data-panel=vibe] .ovdk-msg.is-mine')", 'my question in place');
+ const me = await until(p, "(() => { const a = document.querySelector('.ovs-door-host .ovr-agent'); return a && /Juniper/.test(a.innerText) ? { text: a.innerText.replace(/\\s+/g, ' '), go: !!a.querySelector('.ovr-agent-go') } : null; })()", 'your agent at the top');
+ assert.match(me.text, /Juniper your agent Ready · \$0\.12 of \$1\.00 today/, 'its name, its state and today\'s spend');
+ assert.ok(me.go, 'and Pause beside it');
+ await p.evaluate("document.querySelector('.ovs-door-host .ovr-filter[data-filter=chat]').click()");
+ await until(p, "document.querySelectorAll('.ovs-door-host .ovr-starter').length === 6 && [...document.querySelectorAll('.ovs-door-host .ovr-starter')].every((b) => b.querySelector('svg'))", 'the starters, each with its glyph');
+ await p.evaluate("document.querySelector('.ovs-door-host .ovr-starter').click()");
+ await until(p, "!!document.querySelector('.ovs-door-host .ovr-thread .ovr-step.is-you')", 'my words at once');
assert.strictEqual(asked[0].message, 'Remind me to stretch every 2 hours');
assert.strictEqual(asked[0].message, 'Remind me to stretch every 2 hours');
- assert.ok(asked[0].timezone, 'with the person\'s time zone, for "at 9"');
- const answer = await until(p, "(() => { const m = document.querySelector('.ovdk [data-panel=vibe] .ovdk-msg.is-vibe:not(.ovdk-typing)'); return m ? { text: m.innerText, link: (m.querySelector('a') || {}).href || null } : null; })()", 'Vibe\'s answer', 6000);
- assert.match(answer.text, /Reminder set: Stretch, every 2 hours/);
- assert.strictEqual(answer.link, 'https://openvibe.today/reminders', 'an https link is a link');
- // What Vibe did, under its answer: a reminder chip with Undo and Open (plan L48 V2, result chips).
- const chip = await until(p, "(() => { const c = document.querySelector('.ovdk [data-panel=vibe] .ovdk-res-c'); return c ? { text: c.innerText, open: (c.querySelector('a') || {}).href, undo: !!c.querySelector('button') } : null; })()", 'the result chip');
- assert.match(chip.text, /Reminder set: Stretch/);
- assert.match(chip.text, /Every 2 hours, 9 am–5 pm/);
- assert.strictEqual(chip.open, 'https://openvibe.today/me#reminders');
- assert.ok(chip.undo, 'Undo while it can be undone');
- await p.evaluate("document.querySelector('.ovdk .ovdk-res-c button').click()");
- await until(p, "!!document.querySelector('.ovdk .ovdk-res-c.is-stopped')", 'the chip says it is stopped');
- assert.deepStrictEqual(undone[0], { message_id: vibeMsgs.find((m) => m.from === 'vibe').id, index: 0 }, 'undo names the answer and the result');
- await eventually(async () => assert.match(await p.evaluate("document.querySelector('.ovdk .ovdk-res-c').innerText"), /Reminder stopped: Stretch/));
- await eventually(async () => assert.strictEqual(await p.evaluate("!!document.querySelector('.ovdk .ovdk-res-c button')"), false, 'and is not offered again'));
- await eventually(async () => assert.strictEqual(await p.evaluate("!!document.querySelector('.ovdk .ovdk-typing')"), false, 'the writing dots go when it answers'));
- await eventually(async () => assert.strictEqual(await p.evaluate("document.querySelector('.ovdk .ovdk-away').href"), 'https://openvibe.chat/vibe/v1', 'Open in Chat goes to the same chat'));
- await eventually(async () => assert.ok(puts.some((b) => b.tab === 'vibe'), `the Network keeps the Vibe tab, so it follows the person to the next site (${JSON.stringify(puts)})`));
- await p.close();
- }
-
- // ── Vibe by voice without the browser's speech recognition (Firefox): the mic records, Chat's /api/vibe/listen
- // says what was said, and it is asked like typed text. The recorder is a stand-in: no microphone in CI. ──
- {
- scenario = { counts: { running: 0, waiting: 0, unread: 0 }, state: { ...defaultState(), pinned: true, tab: 'vibe' } };
- asked.length = 0;
- const p = await fresh(1280, 860);
- await p.send('Page.addScriptToEvaluateOnNewDocument', { source: `
- delete window.SpeechRecognition; delete window.webkitSpeechRecognition;
- window.__tracksStopped = 0;
- navigator.mediaDevices.getUserMedia = async () => ({ getTracks: () => [{ stop() { window.__tracksStopped++; } }] });
- window.MediaRecorder = class {
- static isTypeSupported(t) { return t === 'audio/ogg;codecs=opus'; }
- constructor(stream, o) { this.mimeType = (o && o.mimeType) || ''; this.state = 'inactive'; }
- start() { this.state = 'recording'; window.__recording = true; }
- stop() { this.state = 'inactive'; window.__recording = false; this.ondataavailable({ data: new Blob([new Uint8Array(4096)], { type: 'audio/ogg' }) }); this.onstop(); }
- };` });
- await p.goto(`${base}/page`, { maxMs: 5000 });
- await until(p, "!!window.OpenVibeDock", 'the panels');
- await p.evaluate(OPEN('vibe'));
- await until(p, "!!document.querySelector('.ovdk-vibe-new')", 'the Vibe list');
- await p.evaluate("document.querySelector('.ovdk-vibe-new').click()");
- await until(p, "!!document.querySelector('.ovdk [data-panel=vibe] .ovdk-mic')", 'a mic, though the browser has no speech recognition');
- await eventually(async () => assert.strictEqual(await p.evaluate("document.querySelector('.ovdk [data-panel=vibe] textarea').placeholder"), 'Ask Vibe, or hold the mic'));
- await p.evaluate("document.querySelector('.ovdk .ovdk-mic').dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))");
- await until(p, "window.__recording === true && document.querySelector('.ovdk .ovdk-mic').getAttribute('aria-pressed') === 'true'", 'recording, and the mic says so');
- await p.evaluate("document.querySelector('.ovdk .ovdk-mic').dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))");
- await until(p, "[...document.querySelectorAll('.ovdk [data-panel=vibe] .ovdk-msg.is-mine')].some((m) => /drink water at 3/.test(m.innerText))", 'what was said, asked');
- assert.deepStrictEqual(listened[0], { type: 'audio/ogg', bytes: 4096, lang: 'en', auth: 'Bearer tok_user' }, 'the recording goes to Chat as itself, with the person\'s token');
- assert.strictEqual(asked[0].message, 'remind me to drink water at 3');
- await eventually(async () => assert.strictEqual(await p.evaluate('window.__tracksStopped'), 1, 'the microphone is released'));
- await eventually(async () => assert.strictEqual(await p.evaluate("document.querySelector('.ovdk .ovdk-mic').getAttribute('aria-pressed')"), 'false'));
+ assert.strictEqual(asked[0].kind, null, 'no kind: the Frame places the words');
+ const answer = await until(p, "(() => { const m = document.querySelector('.ovs-door-host .ovr-step.is-agent'); return m ? m.innerText : null; })()", 'the answer in place');
+ assert.match(answer, /Reminder set: Stretch, every 2 hours/);
+ await until(p, "!!document.querySelector('.ovs-door-host .ovr-chip') && !document.querySelector('.ovs-door-host .ovr-typing')", 'what it did as a chip, and the typing gone');
+ assert.strictEqual(await p.evaluate("[...document.querySelectorAll('.ovs-door-host .ovr-step.is-you')].length"), 1, 'my words once, not twice');
+ assert.strictEqual(await p.evaluate('document.documentElement.scrollWidth'), 390, 'nothing off the screen');
await p.close();
await p.close();
}
}
@@ -629,7 +598,7 @@ const NO_FLOATING = "!document.querySelector('#ovdk-pill, .ovdk-pill') && [...do
@@ -629,7 +598,7 @@ const NO_FLOATING = "!document.querySelector('#ovdk-pill, .ovdk-pill') && [...do
await p.goto(`${base}/page`, { maxMs: 5000 });
await p.goto(`${base}/page`, { maxMs: 5000 });
await until(p, "!!window.OpenVibeDock", 'the panels');
await until(p, "!!window.OpenVibeDock", 'the panels');
await p.evaluate(OPEN('vibe'));
await p.evaluate(OPEN('vibe'));
- await until(p, PLACE, 'the menu on Vibe');
+ await until(p, "!!document.querySelector('.ovs.is-open .ovs-door-host .ovr-compose textarea')", 'the menu on Vibe');
// 4.223.0: the timer is in the box's + (every composer's), not a button of Vibe's own.
// 4.223.0: the timer is in the box's + (every composer's), not a button of Vibe's own.
await p.evaluate("(document.querySelector('.ovs .ovdk-vibe-new') || { click() {} }).click()");
await p.evaluate("(document.querySelector('.ovs .ovdk-vibe-new') || { click() {} }).click()");
await until(p, "!!document.querySelector('.ovs .ovc-more')", 'the box and its +');
await until(p, "!!document.querySelector('.ovs .ovc-more')", 'the box and its +');
@@ -696,8 +665,9 @@ const NO_FLOATING = "!document.querySelector('#ovdk-pill, .ovdk-pill') && [...do
@@ -696,8 +665,9 @@ const NO_FLOATING = "!document.querySelector('#ovdk-pill, .ovdk-pill') && [...do
await p.goto(`${base}/page`, { maxMs: 5000 });
await p.goto(`${base}/page`, { maxMs: 5000 });
await until(p, "!!window.OpenVibeNavbar && typeof OpenVibeNavbar.askAgent === 'function'", 'the navbar');
await until(p, "!!window.OpenVibeNavbar && typeof OpenVibeNavbar.askAgent === 'function'", 'the navbar');
await p.evaluate("(() => { const b = document.createElement('button'); b.type = 'button'; b.id = 'ask'; b.setAttribute('data-ov-ask', 'About this stream: night walk in Asakusa. '); document.body.appendChild(b); b.click(); })()");
await p.evaluate("(() => { const b = document.createElement('button'); b.type = 'button'; b.id = 'ask'; b.setAttribute('data-ov-ask', 'About this stream: night walk in Asakusa. '); document.body.appendChild(b); b.click(); })()");
- await until(p, PLACE, 'the menu opens');
- const box = await until(p, "(() => { const t = document.querySelector('.ovdk [data-panel=vibe] textarea'); return t && t.value ? { value: t.value } : null; })()", 'the question in the box');
+ await until(p, "!!document.querySelector('.ovs.is-open')", 'the menu opens');
+ // 4.240.0: the question waits in the one box of the menu's Vibe (your agent), unsent.
+ const box = await until(p, "(() => { const t = document.querySelector('.ovs-door-host .ovr-compose textarea'); return t && t.value ? { value: t.value } : null; })()", 'the question in the box');
assert.strictEqual(box.value, 'About this stream: night walk in Asakusa. ', 'the page question, as given');
assert.strictEqual(box.value, 'About this stream: night walk in Asakusa. ', 'the page question, as given');
await eventually(async () => assert.strictEqual(await p.evaluate("document.querySelector('.ovs [data-dock=\"vibe\"]').getAttribute('aria-selected')"), 'true', 'on Vibe'));
await eventually(async () => assert.strictEqual(await p.evaluate("document.querySelector('.ovs [data-dock=\"vibe\"]').getAttribute('aria-selected')"), 'true', 'on Vibe'));
await wait(400);
await wait(400);
@@ -13,7 +13,7 @@ const { brotli } = require('../scripts/size-report');
@@ -13,7 +13,7 @@ const { brotli } = require('../scripts/size-report');
const ROOT = path.join(__dirname, '..');
const ROOT = path.join(__dirname, '..');
const NAVBAR_PATH = path.join(ROOT, 'navbar.js');
const NAVBAR_PATH = path.join(ROOT, 'navbar.js');
const ICONS = require('../nav-icons');
const ICONS = require('../nav-icons');
-const NAVBAR_BROTLI_BUDGET = 52 * 1024; // 51.6 KB with menu addresses (data-ov-menu, #ov-menu=, menuAddress; 4.229.0, 2026-11-12: the owner's "other tools and buttons open specific areas of the menu"; budgets grow for a feature); before that: 50.9 KB with Back to the menu, the links' warm-up and token() (4.229.0, 2026-11-12: the owner's back button and fast links; budgets grow for a feature); before that: 50.1 KB with the app icon's squares held at their size before ov-icons.js (4.227.0, 2026-11-12: design rule 142; budgets grow for a feature); before that: 49.9 KB with the content hashes of 4.211.0's siblings (2026-11-11: no new code in the bar; the agent sheet is a sibling); 49.8 KB with the page view a guest sends at once (4.191.0, 2026-11-10); 49.7 KB with the links that step out when the bar is too narrow (4.190.0, 2026-11-10: it lays out the first paint, so it cannot be a sibling; a diet of the bar is owed); 49.3 KB with Sign In's drawn icon and the menu button's idle hop (4.191.0, 2026-11-10; a signed-out first paint, so it cannot be a sibling); 49.0 KB with the press on the site name that opens its pages (4.191.0, 2026-11-10: the list itself is ov-drop.js); 48.7 KB with the page-view beacon's signed-in marker (4.189.0, 2026-11-10); 48.6 KB with the wallet's cookie mode and its two content hashes (4.173.0, 2026-11-09); 48.2 KB with the same-site prerender rule, cross-site prefetch and the file-cache worker registration (4.165.0, 2026-11-09); 47.6 KB with the six glyph styles' refined paths and the sheets' surface loader (4.162.0, 2026-11-09); 47.1 KB with the shared pop motion every site's menus use and the account menu's Wallet and Profile (4.161.0, 2026-11-09); 46.6 KB with the keyboard watcher every site needs and the slots' loader (4.160.0, 2026-11-09; the slots themselves are a sibling, nav-slots.js); 45.0 KB with the floating cards' loader and link warming (4.152.0, 2026-11-08; the cards themselves are a sibling, overlay.js); 44.3 KB with the phone's menu button drawn as the orb (4.148.0, 2026-11-08); 43.5 KB with the menu warmed at idle and on intent (4.137.0); 43.2 KB with every sibling's ?v= content hash (4.137.0, 2026-11-07: the owner's menu was a day stale without them); 42.8 KB keeping the menu's Frame fields (4.132.0, 2026-11-07); 42.6 KB with Messages and the balances on every site (4.130.0, 2026-11-07); 42.1 KB with the featured link's phone button (4.119.0, 2026-11-06); 41.4 KB with the page-move code (4.70.0: busy(), the mark on plain links, prefetch rules on unboosted sites); was 41 KB with the Dock's loader (4.51.0)
+const NAVBAR_BROTLI_BUDGET = 52.6 * 1024; // 52.3 KB with the menu button drawn from the menu (4.240.0, 2026-11-13: the owner's "regenerate this icon … based on what the menu actually looks like … glow"); before: // 51.6 KB with menu addresses (data-ov-menu, #ov-menu=, menuAddress; 4.229.0, 2026-11-12: the owner's "other tools and buttons open specific areas of the menu"; budgets grow for a feature); before that: 50.9 KB with Back to the menu, the links' warm-up and token() (4.229.0, 2026-11-12: the owner's back button and fast links; budgets grow for a feature); before that: 50.1 KB with the app icon's squares held at their size before ov-icons.js (4.227.0, 2026-11-12: design rule 142; budgets grow for a feature); before that: 49.9 KB with the content hashes of 4.211.0's siblings (2026-11-11: no new code in the bar; the agent sheet is a sibling); 49.8 KB with the page view a guest sends at once (4.191.0, 2026-11-10); 49.7 KB with the links that step out when the bar is too narrow (4.190.0, 2026-11-10: it lays out the first paint, so it cannot be a sibling; a diet of the bar is owed); 49.3 KB with Sign In's drawn icon and the menu button's idle hop (4.191.0, 2026-11-10; a signed-out first paint, so it cannot be a sibling); 49.0 KB with the press on the site name that opens its pages (4.191.0, 2026-11-10: the list itself is ov-drop.js); 48.7 KB with the page-view beacon's signed-in marker (4.189.0, 2026-11-10); 48.6 KB with the wallet's cookie mode and its two content hashes (4.173.0, 2026-11-09); 48.2 KB with the same-site prerender rule, cross-site prefetch and the file-cache worker registration (4.165.0, 2026-11-09); 47.6 KB with the six glyph styles' refined paths and the sheets' surface loader (4.162.0, 2026-11-09); 47.1 KB with the shared pop motion every site's menus use and the account menu's Wallet and Profile (4.161.0, 2026-11-09); 46.6 KB with the keyboard watcher every site needs and the slots' loader (4.160.0, 2026-11-09; the slots themselves are a sibling, nav-slots.js); 45.0 KB with the floating cards' loader and link warming (4.152.0, 2026-11-08; the cards themselves are a sibling, overlay.js); 44.3 KB with the phone's menu button drawn as the orb (4.148.0, 2026-11-08); 43.5 KB with the menu warmed at idle and on intent (4.137.0); 43.2 KB with every sibling's ?v= content hash (4.137.0, 2026-11-07: the owner's menu was a day stale without them); 42.8 KB keeping the menu's Frame fields (4.132.0, 2026-11-07); 42.6 KB with Messages and the balances on every site (4.130.0, 2026-11-07); 42.1 KB with the featured link's phone button (4.119.0, 2026-11-06); 41.4 KB with the page-move code (4.70.0: busy(), the mark on plain links, prefetch rules on unboosted sites); was 41 KB with the Dock's loader (4.51.0)
// ── size budget and generated block ──────────────────────────
// ── size budget and generated block ──────────────────────────
{
{
@@ -161,7 +161,8 @@ const server = http.createServer((req, res) => {
@@ -161,7 +161,8 @@ const server = http.createServer((req, res) => {
await page.evaluate(`document.querySelector('.ovr-thead .ovr-ib').click()`);
await page.evaluate(`document.querySelector('.ovr-thead .ovr-ib').click()`);
assert.ok(await until(`document.querySelectorAll('.ovr-rows .ovr-row:not(.is-ghost)').length === 3`), `${width}: back to the list`);
assert.ok(await until(`document.querySelectorAll('.ovr-rows .ovr-row:not(.is-ghost)').length === 3`), `${width}: back to the list`);
await page.evaluate(`document.querySelector('.ovr-compose textarea').value = 'A raid alert'; document.querySelector('.ovr-compose form').requestSubmit()`);
await page.evaluate(`document.querySelector('.ovr-compose textarea').value = 'A raid alert'; document.querySelector('.ovr-compose form').requestSubmit()`);
- assert.ok(await until(`document.querySelector('.ovr-who b') && document.querySelector('.ovr-thread .ovr-none')`), `${width}: the new run opens`);
+ // 4.240.0: the thread opens at once with your words and your agent typing, until its first words arrive.
+ assert.ok(await until(`document.querySelector('.ovr-who b') && [...document.querySelectorAll('.ovr-thread .ovr-step.is-you')].some((s) => s.textContent === 'A raid alert') && document.querySelector('.ovr-thread .ovr-typing')`), `${width}: the new run opens with your words and your agent typing`);
const start = seen.posts.find((p) => p.path === '');
const start = seen.posts.find((p) => p.path === '');
assert.deepEqual(start.body, { text: 'A raid alert' }, `${width}: started with the words`);
assert.deepEqual(start.body, { text: 'A raid alert' }, `${width}: started with the words`);
assert.match(start.key, /^ovr-[a-z0-9]+-[a-z0-9]+$/, `${width}: once (an idempotency key)`);
assert.match(start.key, /^ovr-[a-z0-9]+-[a-z0-9]+$/, `${width}: once (an idempotency key)`);
@@ -189,10 +190,11 @@ const server = http.createServer((req, res) => {
@@ -189,10 +190,11 @@ const server = http.createServer((req, res) => {
// Code hides the slot and the starters; Chats shows the starters only.
// Code hides the slot and the starters; Chats shows the starters only.
await page.evaluate(`document.querySelector('.ovr-filter[data-filter="code"]').click()`);
await page.evaluate(`document.querySelector('.ovr-filter[data-filter="code"]').click()`);
assert.ok(await until(`document.querySelectorAll('.ovr-rows .ovr-row:not(.is-ghost)').length === 1`), `${width}: Code`);
assert.ok(await until(`document.querySelectorAll('.ovr-rows .ovr-row:not(.is-ghost)').length === 1`), `${width}: Code`);
- assert.deepEqual(await page.evaluate(`[document.querySelector('.ovr-top').hidden, document.querySelector('.ovr-starters').hidden]`), [true, true], `${width}: no slot and no starters on Code`);
+ // 4.240.0: the host's slot (Now: timers, an agent at work) shows on every filter; the starters stay on All and Chats.
+ assert.deepEqual(await page.evaluate(`[document.querySelector('.ovr-top').hidden, document.querySelector('.ovr-starters').hidden]`), [false, true], `${width}: the slot, and no starters, on Code`);
await page.evaluate(`document.querySelector('.ovr-filter[data-filter="chat"]').click()`);
await page.evaluate(`document.querySelector('.ovr-filter[data-filter="chat"]').click()`);
assert.ok(await until(`document.querySelector('.ovr-filter[aria-selected="true"]').dataset.filter === 'chat' && !document.querySelector('.ovr-rows .is-ghost')`), `${width}: Chats`);
assert.ok(await until(`document.querySelector('.ovr-filter[aria-selected="true"]').dataset.filter === 'chat' && !document.querySelector('.ovr-rows .is-ghost')`), `${width}: Chats`);
- assert.deepEqual(await page.evaluate(`[document.querySelector('.ovr-top').hidden, document.querySelector('.ovr-starters').hidden]`), [true, false], `${width}: the starters on Chats`);
+ assert.deepEqual(await page.evaluate(`[document.querySelector('.ovr-top').hidden, document.querySelector('.ovr-starters').hidden]`), [false, false], `${width}: the slot and the starters on Chats`);
// Ask your agent: the words in the box, unsent.
// Ask your agent: the words in the box, unsent.
await page.evaluate(`window.view.prefill('What is this page about?')`);
await page.evaluate(`window.view.prefill('What is this page about?')`);
assert.ok(await until(`document.querySelector('.ovr-compose textarea').value === 'What is this page about?' && document.activeElement === document.querySelector('.ovr-compose textarea')`), `${width}: prefill puts the words in the box, focused`);
assert.ok(await until(`document.querySelector('.ovr-compose textarea').value === 'What is this page about?' && document.activeElement === document.querySelector('.ovr-compose textarea')`), `${width}: prefill puts the words in the box, focused`);
@@ -21,6 +21,9 @@ const wait = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -21,6 +21,9 @@ const wait = (ms) => new Promise((r) => setTimeout(r, ms));
await p.evaluate(`document.querySelector('[data-chip-id]').click()`); await wait(500);
await p.evaluate(`document.querySelector('[data-chip-id]').click()`); await wait(500);
// 4.202.0: the wallet opens in the one window, which grows in (.28 s): measure once it has settled.
// 4.202.0: the wallet opens in the one window, which grows in (.28 s): measure once it has settled.
{ const end = Date.now() + 4000; while (Date.now() < end && !(await p.evaluate(`(() => { const w = document.querySelector('.ovw'); return !w || (w.getAnimations ? w.getAnimations().every((a) => a.playState !== 'running') : true); })()`))) await wait(50); }
{ const end = Date.now() + 4000; while (Date.now() < end && !(await p.evaluate(`(() => { const w = document.querySelector('.ovw'); return !w || (w.getAnimations ? w.getAnimations().every((a) => a.playState !== 'running') : true); })()`))) await wait(50); }
+ // The wallet opens with a scale animation; sizes are read once it has settled (a measurement mid-animation read
+ // 40 px buttons as smaller, about one run in four).
+ await p.evaluate(`Promise.all(document.getAnimations().filter((a) => a.effect && a.effect.getComputedTiming().iterations !== Infinity).map((a) => a.finished.catch(() => {}))).then(() => true)`);
const metrics = await p.evaluate(`(() => { const d=document.querySelector('.ov-wallet'); return {open:!!(d&&(d.open||d.closest('.ovw'))), width:document.documentElement.scrollWidth, inner:d&&d.scrollWidth, box:d&&d.clientWidth, lock:document.documentElement.style.overflow, text:d&&d.innerText, buttons:[...d.querySelectorAll('button,a')].map(b=>b.getBoundingClientRect().height)} })()`);
const metrics = await p.evaluate(`(() => { const d=document.querySelector('.ov-wallet'); return {open:!!(d&&(d.open||d.closest('.ovw'))), width:document.documentElement.scrollWidth, inner:d&&d.scrollWidth, box:d&&d.clientWidth, lock:document.documentElement.style.overflow, text:d&&d.innerText, buttons:[...d.querySelectorAll('button,a')].map(b=>b.getBoundingClientRect().height)} })()`);
assert(metrics.open, 'first chip tap opens wallet'); assert.strictEqual(metrics.width, width); assert(metrics.inner <= metrics.box + 1);
assert(metrics.open, 'first chip tap opens wallet'); assert.strictEqual(metrics.width, width); assert(metrics.inner <= metrics.box + 1);
assert.strictEqual(metrics.lock, 'hidden'); assert(metrics.text.includes('$0.00')); assert(metrics.text.includes('Buy Vibes')); assert(metrics.text.includes('Top up'));
assert.strictEqual(metrics.lock, 'hidden'); assert(metrics.text.includes('$0.00')); assert(metrics.text.includes('Buy Vibes')); assert(metrics.text.includes('Top up'));