openvibers/openvibe.actor Commits

Actor's home: your own AI that works for you while you watch (#366)

OP OpenVibers committed 4 days ago · dde45972d3 · 6 files, +120 −79

* Actor's home: your own AI that works for you while you watch

The owner (11-09): the router pitch and the errand examples (a Tokyo flight, a standing desk, Lisbon) were not
what Actor is for. The home now leads with your own AI working on your behalf: the cards are the Actor v3 lanes
(find me work, earn with a gig, grow my channel, brief me, watch for it, build it), each a task today's engine
runs; how you watch it and take over (you sign in yourself on its screen); the model is your call (OpenVibe's,
your own keys, your Codex or Claude Code plan, your computer); and a sourced comparison with Grok Bot, OpenAI Dots
and Meta Muse. The router's folds go; the agents list stays as what it runs on today. The card beside the hero is
marked as an example.

* perf budget: the Actor v3 home (68.8 KB html, 119.2 KB css; brotli unchanged at ~15 KB)

Changes

ddiff --git a/public/css/home.css b/public/css/home.css
iindex bfb85de..6b07e5b 100644
---- a/public/css/home.css
++++ b/public/css/home.css
@@@ -73,3 +73,22 @@
.fold-bare { margin-top: 0; }
.fold:not(.fold-bare) .fold-body > .sc-sec { margin-top: 0; }
}
++
++/* Actor v3 (2026-11-09): beside the hero, what watching your Actor looks like, marked as an example. */
++.hm-now { display: grid; gap: 10px; padding: 16px; border: 1px solid var(--border, #2a2f3a); border-radius: 16px;
++ background: var(--bg-card, #11151c); box-shadow: 0 18px 40px rgba(0,0,0,.25); max-width: 420px; }
++.hm-now-k { margin: 0; font-size: .72rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--text-muted, #8b93a7); }
++.hm-now-step { display: flex; gap: 10px; align-items: flex-start; margin: 0; font-weight: 650; line-height: 1.4; }
++.hm-now-dot { flex: none; width: 10px; height: 10px; margin-top: 6px; border-radius: 50%; background: var(--success, #22c55e);
++ box-shadow: 0 0 0 4px color-mix(in srgb, var(--success, #22c55e) 22%, transparent); animation: hmNowPulse 1.8s ease-in-out infinite; }
++@keyframes hmNowPulse { 50% { opacity: .45; } }
++@media (prefers-reduced-motion: reduce) { .hm-now-dot { animation: none; } }
++.hm-now-done { margin: 0; padding-left: 20px; display: grid; gap: 4px; color: var(--text-secondary, #b6bdd2); font-size: .9rem; }
++.hm-now-ask { display: grid; gap: 4px; padding: 12px; border-radius: 12px; background: color-mix(in srgb, var(--accent, #3b82f6) 12%, transparent);
++ border: 1px solid color-mix(in srgb, var(--accent, #3b82f6) 35%, transparent); }
++.hm-now-ask b { font-size: .72rem; letter-spacing: .06em; text-transform: uppercase; color: var(--accent-light, var(--accent, #60a5fa)); }
++.hm-now-btns { display: flex; gap: 8px; margin-top: 4px; }
++.hm-now-btn { padding: 6px 14px; border-radius: 999px; border: 1px solid var(--border, #2a2f3a); font-size: .85rem; font-weight: 650; }
++.hm-now-btn.is-yes { background: var(--accent-strong, #2563eb); border-color: transparent; color: var(--on-accent-strong, #fff); }
++.hm-now-meta { margin: 0; font-size: .82rem; color: var(--text-muted, #8b93a7); }
++.hm-compare-src { position: relative; clear: both; margin: 22px 0 28px; font-size: .78rem; color: var(--text-muted, #8b93a7); max-width: 70ch; }
ddiff --git a/server/http/discovery.js b/server/http/discovery.js
iindex 000410b..8f8b69b 100644
---- a/server/http/discovery.js
++++ b/server/http/discovery.js
@@@ -13,11 +13,11 @@ const cache = require('openvibe-shared/cache-policy');
const { asyncRouter } = require('./router');
const SITE_NAME = 'OpenVibe.Actor';
--const DESCRIPTION = 'OpenVibe.Actor is an AI agent router and OpenVibe\'s own agent: give it a task in plain words and it sends it to the best agent for it — OpenVibe\'s runtime acting through OpenVibe services, another platform\'s agent such as OpenAI\'s web agent, or an open model on OpenVibe hardware for private tasks — checks the answer with a second model and shows the cost and why. Like OpenRouter, for agents.';
++const DESCRIPTION = 'OpenVibe.Actor is your own AI that works for you: it finds work and drafts applications, writes and prices a gig, grows your channel, briefs you every morning, watches for deals and builds things. You watch it work from any OpenVibe page, it asks before it spends, posts or sends, and it runs on the model you pick, your own keys included.';
const DISALLOW = ['/auth/', '/api/', '/tasks'];
const PAGE_TEXT = {
-- '/': ['OpenVibe.Actor home', 'Give a task in plain words; Actor picks the agent, checks the answer and shows the cost and why. What it can do today and what comes next.'],
++ '/': ['OpenVibe.Actor home', 'Your own AI that works for you while you watch: jobs, a gig, your channel, a morning brief, deals and builds. It asks before it acts and runs on the model you pick.'],
'/agents': ['The agents', 'Every agent system Actor routes to, with what it can do, its trust class, its price per million tokens and whether it works now; the modes and the check.'],
'/docs': ['The Actor API', 'Create a task, watch it over server-sent events, read the result with its cost and explanation, cancel; list the agents and dry-run the router.'],
'/updates': ['What shipped on OpenVibe.Actor', 'This site\'s update log, from the network changelog feed.'],
@@@ -35,7 +35,7 @@ function homeJsonLd(config) {
const site = String(config.baseUrl).replace(/\/+$/, '');
return [
seo.jsonLd.website({ name: SITE_NAME, url: site, description: DESCRIPTION }),
-- seo.jsonLd.softwareApp({ name: SITE_NAME, url: site, description: DESCRIPTION, category: 'BusinessApplication', keywords: 'ai agent, ai agent router, openrouter for agents, autonomous ai agent, ai assistant that does tasks, computer use agent, browser agent, cheap ai agent, open source ai agent, self-hosted ai agent, agent api' }),
++ seo.jsonLd.softwareApp({ name: SITE_NAME, url: site, description: DESCRIPTION, category: 'BusinessApplication', keywords: 'personal ai agent, ai that works for you, ai agent that makes money, ai job application assistant, autonomous ai agent, ai assistant that does tasks, computer use agent, browser agent, cheap ai agent, open source ai agent, self-hosted ai agent, agent api' }),
seo.jsonLd.webPage({ name: SITE_NAME, url: `${site}/`, description: DESCRIPTION, siteUrl: site }),
];
}
ddiff --git a/server/http/pages.js b/server/http/pages.js
iindex aec4d45..a74875f 100644
---- a/server/http/pages.js
++++ b/server/http/pages.js
@@@ -52,47 +52,45 @@ const MODE_TEXT = {
const MODE_LABEL = { cheapest: 'Cheapest', balanced: 'Balanced', best: 'Best', fastest: 'Fastest', private: 'Private' };
const CLASS_WORD = { answer: 'Answers', lookup: 'Lookups', research: 'Reading pages', web: 'Web search', code: 'Code', robot: 'Your robot', computer: 'Your computer' };
const STATE_KIND = { queued: '', running: 'info', verifying: 'info', succeeded: 'ok', failed: 'bad', cancelled: '' };
--// What people really hand agents like this (the owner, 2026-11-01: "I hate all the current examples… people aren't
--// gonna use it for that… search what people use Grok-style agents for"; research the same day: OpenAI's agent launch,
--// Manus reviews and Perplexity's Comet usage study put travel and bookings, shopping and deals, fact-checks and
--// research, watching for something, and lists and tables first). The chat's starters are the same set.
++// What it does for you (the owner, 2026-11-09: the old examples, a flight, a desk, Lisbon, "suck"; lead with work,
++// money, a channel, a brief and building things; Actor v3 plan). Every one runs on today's engine: web and OpenVibe
++// search, reading pages, writing, files, Watch and coding. Grouped by the lane the Actor v3 plan names.
const EXAMPLES = [
-- 'Find the cheapest round trip from New York to Tokyo in March. Show me the best three and stop before paying.',
-- 'Compare the five best standing desks under $500: price, reviews, and where each is cheapest today.',
-- 'Fact-check this with real sources, and tell me what is true, what is false and what is unclear: ',
++ 'Find 10 remote junior video editing jobs posted this week on OpenVibe.Work and the web. Rank them by fit, then write a short tailored cover letter for the best three. Do not apply: show me first.',
++ 'Look at what the top 10 Fiverr gigs for "stream highlights into YouTube Shorts" charge and promise, then write my gig: a title, three price tiers with what each includes, a description and five search tags.',
++ 'Give me my morning brief: the three AI stories that matter today, what Bitcoin and the S&P 500 did overnight, and today\'s weather in my city. Under 150 words.',
'Watch eBay for a used RTX 4090 under $900 and tell me the moment one is listed.',
];
--// Tasks to start from, by what they need. On the home they are a rail of cards (kit.card): a short title, the kind as
--// the badge and the card type's icon; the card's link puts the whole task in the task box, ready to run or change. What a
--// robot, computer, desk or private task needs is said in the box once that task is chosen (NEEDS).
++// The cards to start from: the lane as the badge, the card type's icon; a card puts its whole task in the box, ready to
++// run or change. What a computer, desk or private task needs is said in the box once that task is chosen (NEEDS).
const TEMPLATES = [
-- { group: 'Book and buy', badge: 'Errand', type: 'site', items: [
-- { title: 'The cheapest flight to Tokyo in March', text: EXAMPLES[0] },
-- { title: 'The best standing desk under $500', text: EXAMPLES[1] },
++ { group: 'Find me work', badge: 'Work', type: 'post', items: [
++ { title: 'Find jobs and draft the applications', text: EXAMPLES[0] },
++ { title: 'Rewrite my CV for one job, honestly', text: 'Rewrite my CV for this job post so it matches what they ask for, without inventing anything. The post and my CV: ' },
] },
-- { group: 'Check and research', badge: 'Research', type: 'report', items: [
-- { title: 'Is this true? Check it with sources', text: EXAMPLES[2] },
-- { title: 'What people say about the new iPhone today', text: 'What are people saying about the newest iPhone today on Reddit, X and the news? Give me the short version.' },
++ { group: 'Earn with a gig', badge: 'Earn', type: 'site', items: [
++ { title: 'A Fiverr gig, priced from the market', text: EXAMPLES[1] },
++ { title: 'Price what I want to sell', text: 'I want to sell these: <list them>. Find what each sells for used on eBay this month, then suggest a price and a listing title for each.' },
] },
-- { group: 'Watch for it', badge: 'Watch', type: 'cam', items: [
-- { title: 'A used RTX 4090 under $900 on eBay', text: EXAMPLES[3] },
++ { group: 'Grow my channel', badge: 'Channel', type: 'stream', items: [
++ { title: 'Where to post about my stream', text: 'Find five places where people talk about <my game> (subreddits, Discord servers, forums), with each one\'s rule on self-promotion, and draft a helpful first post for each.' },
++ { title: 'A month of posts in one go', text: 'Write 20 short posts for X and Threads about my streaming channel (<what I stream>), one for each weekday of the next month: useful, funny, no hashtag spam.' },
] },
-- { group: 'Plan and list', badge: 'Plan', type: 'post', items: [
-- { title: 'A weekend in Lisbon for two', text: 'Plan a weekend in Lisbon for two: a hotel under $150 a night, where to eat, and a plan for each day.' },
-- { title: '20 small gaming streamers, in a table', text: 'Make a table of 20 small gaming streamers (under 10k followers) with their channel links and when they stream.' },
++ { group: 'Brief me', badge: 'Brief', type: 'report', items: [
++ { title: 'My morning brief', text: EXAMPLES[2] },
] },
-- { group: 'Across OpenVibe', badge: 'OpenVibe', type: 'stream', items: [
-- { title: 'Who is live on OpenVibe, and about what', text: 'Open openvibe.live, find the channels that are live right now, and tell me what is happening on each.' },
++ { group: 'Watch for it', badge: 'Watch', type: 'cam', items: [
++ { title: 'A used RTX 4090 under $900', text: EXAMPLES[3] },
] },
-- { group: 'Your robot', badge: 'Robot', type: 'toy', needs: 'Turn on Your agent on the robot\'s panel first.', items: [
-- { title: 'Drive your rover and say hello', text: 'Drive my rover forward for two seconds, turn on its headlights, then say hello.' },
++ { group: 'Build it', badge: 'Build', type: 'site', items: [
++ { title: 'A one-page site for my channel', text: 'Make me a one-page website for my streaming channel: my schedule, my links and a short about section, as a single HTML file I can upload.' },
] },
-- { group: 'Your computer', badge: 'Computer', type: 'site', needs: 'Needs the OpenVibe Link\'s computer plugin on that machine.', items: [
++ { group: 'On your computer', badge: 'Computer', type: 'site', needs: 'Needs OpenVibe Link on that computer.', items: [
{ title: 'The ten largest files in your Downloads', text: 'On my computer, list the ten largest files in my Downloads folder.' },
] },
-- { group: 'On a desk of its own', badge: 'Desk', type: 'cam', needs: 'Needs a computer or desk host with the OpenVibe Link\'s computer plugin. Watch the desk and take it over from the task.', items: [
-- { title: 'Sign in on its desk, then read your account', text: 'On my computer, open the sign-in page of <a site I use> on your desk and hand it to me to sign in; then tell me what my account page shows.' },
++ { group: 'On its screen', badge: 'Screen', type: 'cam', needs: 'It opens the page on its screen and hands it to you: you sign in yourself, so it never sees your password, and it stays signed in.', items: [
++ { title: 'You sign in on its screen; it reads your account', text: 'On my computer, open the sign-in page of <a site I use> on your desk and hand it to me to sign in; then tell me what my account page shows.' },
] },
{ group: 'Private', badge: 'Private', type: 'person', needs: 'Answered on OpenVibe\'s own model; nothing leaves OpenVibe.', items: [
{ title: 'Summarize a note without it leaving OpenVibe', text: 'Summarize the following note in three bullet points, without it leaving OpenVibe:', mode: 'private' },
@@@ -468,25 +466,25 @@ ${today ? html`<p class="task-allowance muted small">${leftLine(tier, today)}</p
const st = engine && typeof engine.status === 'function' ? engine.status() : null;
const running = st ? (Number(st.running) || 0) + (Number(st.waiting) || 0) : 0;
const faq = faqItems(agents);
++ // Actor v3 (the owner, 2026-11-09): your own AI that works for you, on your behalf, while you watch.
const hero = showcase.hero({
-- eyebrow: 'OpenVibe.Actor · your agent for everything · alpha',
-- title: 'Ask once.', accent: 'The right agent does it.',
-- lede: 'Give Actor a task in plain words. It picks the agent that fits, OpenVibe\'s own or another platform\'s, has a second model check the answer, and shows you what it cost and why. Like OpenRouter, for agents.',
-- actions: signed ? [{ label: 'Give it a task', href: '#task', primary: true }, { label: 'Your tasks', href: '/tasks' }] : [{ label: 'Sign in to start', href: '/auth/login?next=%2F', primary: true }, { label: 'See the agents', href: '/agents' }],
++ eyebrow: 'OpenVibe.Actor · alpha',
++ title: 'Your own AI.', accent: 'It works for you while you watch.',
++ lede: 'Tell it what you want: find you work, earn from a gig, grow your channel, brief you every morning, build something. It works on a screen you can watch from any OpenVibe page, uses your own accounts, asks before it spends, posts or applies, and runs on the model you pick.',
++ actions: signed ? [{ label: 'Give it a job', href: '#task', primary: true }, { label: 'Your Actor', href: '/console' }] : [{ label: 'Start your Actor', href: '/auth/login?next=%2F', primary: true }, { label: 'What it can do', href: '#templates' }],
note: `Open source (AGPL-3.0). Free: ${tier.tasksPerDay} tasks a day and ${usd(tier.perDayUsd)} of model use, no card.`,
-- aside: { html: String(routerCard(agents)) },
++ aside: { html: String(nowCard()) },
});
// One strip of numbers (it scrolls sideways on a phone instead of stacking four tiles); zero is left out.
const stats = kit.stats([
-- [running, running === 1 ? 'task running now' : 'tasks running now', '/tasks'],
-- [ready, ready === 1 ? 'agent to choose from' : 'agents to choose from', '#agents'],
++ [running, running === 1 ? 'job running now' : 'jobs running now', '/tasks'],
++ [ready, ready === 1 ? 'model to run on' : 'models to run on', '#agents'],
[tools.length, 'tools it can run', '#tools'],
-- [MODES.length, 'ways to choose', '/agents#modes'],
-- [tier.tasksPerDay, 'free tasks a day', '#faq'],
++ [tier.tasksPerDay, 'free jobs a day', '#faq'],
], { label: 'OpenVibe.Actor in numbers' });
// Tasks to start from: one card each, the kind as its badge and a non-default mode in its corner.
const starts = kit.rail({
-- id: 'templates', title: 'Start from one of these',
++ id: 'templates', title: 'What it can do for you',
items: TEMPLATES.flatMap((g) => g.items.map((t) => kit.card({ href: templateHref(t), title: t.title, type: g.type, ratio: '3/1', badge: g.badge, corner: t.mode && MODE_LABEL[t.mode] !== g.badge ? MODE_LABEL[t.mode] : '', peek: t.text, share: false }))),
});
// Signed out and nothing chosen yet, the hero's Sign in is the door: the box would only repeat it.
@@@ -523,42 +521,39 @@ ${today ? html`<p class="task-allowance muted small">${leftLine(tier, today)}</p
css: ['css/home.css'],
body: html`${raw(hero)}
<div class="hm-stats-sec">${raw(stats)}</div>
--<section class="sc-sec hm-task" id="task" aria-labelledby="h-task"><h2 id="h-task">Give Actor a task</h2>
++<section class="sc-sec hm-task" id="task" aria-labelledby="h-task"><h2 id="h-task">Give it a job</h2>
${box}
${raw(starts)}</section>
++${raw(showcase.features({ id: 'watch', title: 'Watch it work, from anywhere', lede: 'It is your AI, doing things on your behalf, so you can always see what it is doing.', items: [
++ { icon: 'fa-eye', title: 'Its screen, live', text: 'When it works on a computer, watch its screen from the Dock on any OpenVibe page, or on your phone.' },
++ { icon: 'fa-list-check', title: 'What it is doing, in plain words', text: 'Away from a screen: the step it is on, what it read, what it did last and what it will do next.' },
++ { icon: 'fa-hand-pointer', title: 'Take over any time', text: 'Tap to take the mouse, then hand it back. Signing in? It hands you its screen and you type your own password.' },
++ { icon: 'fa-bell', title: 'It asks before it acts', text: 'Spending money, posting, applying or messaging someone waits for your yes, on any OpenVibe page or your phone.' },
++ ] }))}
++${raw(showcase.features({ id: 'brains', title: 'The model is your call', lede: 'Pick what it thinks with for each job, and change it any time.', items: [
++ { icon: 'fa-wand-magic-sparkles', title: 'OpenVibe\'s models', text: 'DeepSeek, GPT and open models, at cost, through OpenVibe.Exchange.' },
++ { icon: 'fa-key', title: 'Your own keys, no fee', text: 'Claude, OpenAI, DeepSeek or Gemini on your own account: OpenVibe adds nothing.' },
++ { icon: 'fa-terminal', title: 'Your Codex or Claude Code plan', text: 'Coding jobs run through OpenVibe.Codes on the plan you already pay for.' },
++ { icon: 'fa-desktop', title: 'On your own computer', text: 'With OpenVibe Link it can work on your machine, with your models, from your own internet address.' },
++ ] }))}
++${raw(showcase.compare({ id: 'compare', title: 'Yours, not theirs', lede: 'The big personal agents of 2026 each lock you to one company\'s model and cloud.', columns: ['OpenVibe.Actor', 'Grok Bot', 'OpenAI Dots', 'Meta Muse'], rows: [
++ { label: 'Pick any model, including your own', values: [true, false, false, false] },
++ { label: 'Works on your own computer, free', values: [true, false, false, true] },
++ { label: 'Open source', values: [true, false, false, false] },
++ { label: 'Free to start', values: [true, false, false, true] },
++ ] }))}
++<p class="hm-compare-src">Grok Bot runs xAI's models on its own cloud computer, Dots runs GPT-6 Astra inside ChatGPT's paid plans, and Muse runs Meta's model on Meta's machine or its desktop app, with a free tier (as reported in September and October 2026).</p>
${raw(foldBlock({
id: 'agents',
-- head: '<h2 id="h-agents">The agents</h2>',
++ head: '<h2 id="h-agents">What it runs on today</h2>',
html: String(html`<section class="sc-sec" id="agents" aria-labelledby="h-agents">
--<p class="sc-lede">Every agent Actor can route to, what it charges and whether it is working right now. <a href="/agents">What each one is, its modes and details</a>.</p>
++<p class="sc-lede">The models and engines it can use right now, what each charges and whether it is working. <a href="/agents">Details for each</a>.</p>
${raw(kit.list(agents.map((a) => kit.row({ href: `/agents#${a.id}`, title: a.name, badge: a.available ? kit.badge('working', 'ok') : kit.badge('not here yet'), sub: `${priceText(a.rate_card)}${a.model ? ` · ${a.model}` : ''}` }))))}</section>`),
}))}
${raw(foldBlock({
id: 'how',
head: '<h2 id="h-how">How it works</h2>',
html: String(html`<section class="sc-sec hm-more" aria-labelledby="h-how">
--${fold({ id: 'route', title: 'How a task finds its agent', meta: '5 steps', body: showcase.steps({
-- items: [
-- { title: 'Sort the task', text: 'An answer, a lookup, research, a live web search, code, your own robot or your own computer: that decides what an agent must be able to do.' },
-- { title: 'Hard limits first', text: 'An agent that cannot do it, that private mode does not trust, that is down or that would cost more than your budget is out, and the task says why.' },
-- { title: 'Then your mode', text: 'Cheapest, Balanced, Best, Fastest or Private picks among the rest, through the same placement engine every OpenVibe service uses.' },
-- { title: 'Run, check, deliver', text: 'The agent works while you watch its steps; another model checks the answer, from another model family where one is available; you get the answer, its sources, the cost and the explanation.' },
-- { title: 'Or hand it on', text: 'An agent that fails, or an answer that fails its check, goes to the next agent that can do it, within your budget.' },
-- ],
-- }) })}
--${fold({ id: 'why', title: 'Why a router, not one agent', meta: 'compared', body: showcase.compare({
-- lede: 'One company\'s agent is as good as that company\'s models and tools. Actor picks per task, and can use theirs too.',
-- columns: ['OpenVibe.Actor', 'A single-vendor agent'],
-- rows: [
-- { label: 'Picks the best agent for each task', values: [true, false] },
-- { label: 'Can hand a task to another platform\'s agent', values: [true, false] },
-- { label: 'Shows the cost and why, per task', values: [true, false] },
-- { label: 'A second model family checks the answer', values: [true, false] },
-- { label: 'A private mode on its own hardware', values: [true, false] },
-- { label: 'Open source, self-hostable', values: [true, false] },
-- { label: 'Free tier', values: [true, null] },
-- ],
-- }) })}
${fold({ id: 'status', title: 'What works today', meta: `${status.length} pieces, all live`, body: String(table(['Piece', 'State'], status)) })}
${tools.length ? fold({ id: 'tools', title: 'Tools it can run', meta: `${tools.length} from OpenVibe.Tools`, body: String(html`<p class="muted">For exact answers, OpenVibe's own agent runs these ${tools.length} OpenVibe.Tools tools itself instead of guessing: the same public tools anyone can use on openvibe.tools.</p>
<ul class="hm-chips hm-tools">${tools.map((t) => html`<li>${/^https:\/\//.test(t.docs || '') ? html`<a href="${t.docs}">${t.name}</a>` : t.name}</li>`)}</ul>`) }) : ''}
@@@ -591,17 +586,29 @@ ${raw(foldBlock({
const names = agents.filter((a) => a.available).map((a) => a.name);
const list = names.length > 1 ? `${names.slice(0, -1).join(', ')} and ${names[names.length - 1]}` : names[0] || '';
return [
-- ['What does OpenVibe.Actor do?', 'You describe a task in plain words. Actor sorts it, picks the agent that can do it best within your budget and mode, runs it while you watch its steps, has a second model check the answer, and shows you the answer with its sources, what it cost and why that agent.'],
-- ['What does it cost?', `The free tier gives ${tier.tasksPerDay} tasks a day and ${usd(tier.perDayUsd)} of model use, with no card. Every task shows its real cost to a fraction of a cent, and what is left of today's allowance is beside the box.`],
-- list ? ['Which agents does it use?', `${list} are ready to pick from. The agents page lists each one with its price per million tokens and what it can do.`] : null,
-- ['Is the answer checked?', 'Yes. A second model reads every answer before you see it, from another model family where one is available. An answer that fails its check goes to the next agent that can do the task.'],
-- ['What is private mode?', 'Private mode uses only an open model on OpenVibe\'s own server, so the task never leaves OpenVibe. It is for short answers; tasks that need search or tools go to the other agents in the other modes.'],
-- ['Can it use my computer or my robot?', 'Only if you turn it on. On openvibe.bot you let your agent use a computer or drive a robot, one at a time. On a computer at the ask level, each program or write waits for your approval on that computer.'],
++ ['What does OpenVibe.Actor do?', 'It is your own AI that does things for you: finds work and drafts applications, writes and prices a gig, finds places to post about your channel, briefs you in the morning, watches for a deal, builds a page. You watch it work, it asks before anything that spends, posts or sends, and you get the result with its sources and what it cost.'],
++ ['Will it make me money?', 'It does the legwork that earning takes: finding jobs and tailoring applications, a gig that is priced from the market, listings for what you sell. You send, publish and get paid yourself, and every job shows what it cost, so you can see whether it pays.'],
++ ['Does it act as me?', 'Yes, on your behalf and while you can watch. It uses your own accounts: when a site needs a sign-in it hands you its screen and you type your own password, so it never sees it, and it stays signed in. With OpenVibe Link on your computer it also works from your own internet address.'],
++ ['What does it cost?', `The free tier gives ${tier.tasksPerDay} jobs a day and ${usd(tier.perDayUsd)} of model use, with no card. Every job shows its real cost, and what is left of today's allowance is beside the box. Your own model keys cost you nothing extra.`],
++ list ? ['Which models does it use?', `${list} are ready now. You can bring your own Claude, OpenAI, DeepSeek or Gemini keys, or your Codex or Claude Code plan for coding jobs.`] : null,
++ ['Is the answer checked?', 'Yes. A second model reads every answer before you see it, from another model family where one is available.'],
++ ['Can it use my computer?', 'Only if you turn it on. Install OpenVibe Link and allow your Actor on that computer; at the ask level, each program it runs or file it writes waits for your yes.'],
['Can I run it myself?', 'Yes. Actor is open source under the AGPL-3.0 and runs on your own server with Docker or plain Node and your own model, such as Ollama or any OpenAI-compatible one.'],
['Can my app use it?', 'Yes. Apps get actor.task.* grants on OpenVibe.Services and use the same API: create a task, follow its live stream, and receive signed webhooks when it ends.'],
].filter(Boolean);
}
++ /** Beside the hero: what watching your Actor looks like, marked as an example. */
++ function nowCard() {
++ return html`<div class="hm-now" aria-label="An example of an Actor at work">
++<p class="hm-now-k">Example · Find me work</p>
++<p class="hm-now-step"><span class="hm-now-dot" aria-hidden="true"></span> Tailoring a cover letter for a remote junior video editor job</p>
++<ol class="hm-now-done"><li>Read 14 new posts on OpenVibe.Work and two job boards</li><li>Kept 4 that match your CV and your pay floor</li></ol>
++<div class="hm-now-ask"><b>It asks you</b><span>Send this application?</span><span class="hm-now-btns"><span class="hm-now-btn is-yes">Send</span><span class="hm-now-btn">Change it</span></span></div>
++<p class="hm-now-meta">Model: DeepSeek · today $0.21 of your $2 cap</p>
++</div>`;
++ }
++
function routerCard(agents) {
const shown = agents.filter((a) => a.id !== 'codes');
return html`<figure class="router-card" aria-label="How Actor picked an agent for a sample task">
ddiff --git a/test/home-short.test.js b/test/home-short.test.js
iindex 9942453..2a95c23 100644
---- a/test/home-short.test.js
++++ b/test/home-short.test.js
@@@ -14,7 +14,7 @@ const { boot, check, done } = require('./helpers/boot');
const r = await t.get('/');
assert.strictEqual(r.status, 200, r.text.slice(0, 300));
const home = r.text;
-- for (const [id, title] of [['agents', 'The agents'], ['how', 'How it works'], ['faq', 'Questions'], ['shipped', 'Recently shipped on OpenVibe.Actor']]) {
++ for (const [id, title] of [['agents', 'What it runs on today'], ['how', 'How it works'], ['faq', 'Questions'], ['shipped', 'Recently shipped on OpenVibe.Actor']]) {
const re = new RegExp(`<div class="fold[^"]*" id="fold-${id}"><input class="fold-toggle" type="checkbox" id="fold-${id}-open">`
+ `<label class="fold-h" for="fold-${id}-open"><h2[^>]*>[^<]*${title}[^<]*</h2>.*?<div class="fold-body">`);
assert.ok(re.test(home), `the ${title} fold is on the home`);
ddiff --git a/test/home.test.js b/test/home.test.js
iindex dda6cbc..90feb63 100644
---- a/test/home.test.js
++++ b/test/home.test.js
@@@ -23,9 +23,9 @@ const { boot, check, done } = require('./helpers/boot');
assert.strictEqual((r.text.match(/<dl class="ovk-stats"/g) || []).length, 1, 'the numbers are one kit.stats strip');
assert.match(r.text, stat('tools it can run', '1'), 'only the callable tool (mx), not the job tool');
assert.match(r.text, /<dt><a href="#tools">tools it can run<\/a><\/dt>/, 'the tools number opens the tools fold');
-- assert.match(r.text, stat('ways to choose', '5'));
-- assert.match(r.text, stat('free tasks a day'));
-- assert.match(r.text, stat('agents? to choose from'));
++ assert.match(r.text, stat('free jobs a day'));
++ assert.match(r.text, stat('models? to run on'));
++ assert.doesNotMatch(r.text, /ways to choose|agents? to choose from/, 'no router numbers (Actor v3)');
const tools = foldOf(r.text, 'tools');
assert.match(tools, />MX Lookup</);
assert.ok(!tools.includes('Video download'), 'a job tool is not offered');
@@@ -40,7 +40,8 @@ const { boot, check, done } = require('./helpers/boot');
const r = await t.get('/');
// The folds hold showcase sections of their own, so the part runs from its heading to the questions.
const more = r.text.slice(r.text.indexOf('<h2 id="h-how">'), r.text.indexOf('<section class="sc-sec" id="faq"'));
-- for (const id of ['route', 'why', 'status', 'tools', 'api']) assert.ok(foldOf(more, id), `the ${id} part is a fold`);
++ for (const id of ['status', 'tools', 'api']) assert.ok(foldOf(more, id), `the ${id} part is a fold`);
++ for (const id of ['route', 'why']) assert.ok(!foldOf(more, id), `the router's ${id} fold is gone (Actor v3)`);
assert.ok(!/<details class="ovk-fold" open/.test(more), 'every fold starts closed');
assert.match(foldOf(more, 'status'), /OpenAI&#39;s web agent as one of the routed agents|OpenAI's web agent as one of the routed agents/, 'what works today is all still there');
assert.match(foldOf(more, 'api'), /\/api\/v1\/route/, 'the API sample');
@@@ -54,6 +55,20 @@ const { boot, check, done } = require('./helpers/boot');
assert.ok(!/class="(hm-template|hm-tgroup|agent-card|hm-qa)\b/.test(r.text), 'no hand-built card stacks are left');
});
++ await check('Actor v3 (owner, 11-09): your own AI working for you; the lanes, watching, the model and the comparison', async () => {
++ const r = await t.get('/');
++ assert.match(r.text, /Your own AI\./, 'the hero says what it is');
++ assert.doesNotMatch(r.text, /Like OpenRouter|Ask once\.|The right agent does it/, 'no router pitch');
++ assert.doesNotMatch(r.text, /Tokyo|standing desk|Lisbon/, 'none of the old errand examples');
++ const templates = (/<section class="ovk-rail[^"]*" id="templates"[\s\S]*?<\/section>/.exec(r.text) || [''])[0];
++ for (const lane of ['Work', 'Earn', 'Channel', 'Brief', 'Watch', 'Build']) assert.ok(templates.includes(`>${lane}<`), `the ${lane} lane has a card`);
++ assert.match(r.text, /id="watch"[\s\S]*Take over any time/, 'how you watch it and take over');
++ assert.match(r.text, /id="brains"[\s\S]*Your own keys, no fee/, 'the model is your call');
++ const cmp = (/<section class="sc-sec" id="compare"[\s\S]*?<\/section>/.exec(r.text) || [''])[0];
++ for (const name of ['Grok Bot', 'OpenAI Dots', 'Meta Muse']) assert.ok(cmp.includes(name), `compared with ${name}`);
++ assert.match(r.text, /<p class="hm-now-k">Example · Find me work<\/p>/, 'the card beside the hero is marked as an example');
++ });
++
await check('templates fill the task box; signed out, the chosen task waits for sign-in', async () => {
const home = await t.get('/');
const templates = (/<section class="ovk-rail[^"]*" id="templates"[\s\S]*?<\/section>/.exec(home.text) || [''])[0];
ddiff --git a/test/perf-budget.test.js b/test/perf-budget.test.js
iindex 40cb73d..dd361b1 100644
---- a/test/perf-budget.test.js
++++ b/test/perf-budget.test.js
@@@ -14,14 +14,14 @@ const { measure, check, format } = require('openvibe-shared/perf-budget');
// Budgets are set from the numbers measured on 2026-10-08 (the real ones, fresh database), rounded up with about 10%
// headroom; file counts are the measured counts and external files stay at 0.
const BUDGETS = {
-- htmlRawKB: 66.1, // 66.1 with openvibe-shared 4.135.0 (2026-11-07: Ask your agent from any page (kit.askAgent), the Dock's Agent panel reads the timeline again, and the menu's feeds follow you); 64.3 with the bar's icons and the phone app bar (2026-10-28); 59.2 with the Part of OpenVibe section (Shared 4.13, 2026-10-26); measured 35.8 (the guest task box lists example tasks); 39.6 with the home's "Ask in chat" row (10-15)
++ htmlRawKB: 69.5, // 68.8 with the Actor v3 home (2026-11-09: the lanes, watching, the model and the comparison with Grok Bot, Dots and Muse); 66.1 with openvibe-shared 4.135.0 (2026-11-07: Ask your agent from any page (kit.askAgent), the Dock's Agent panel reads the timeline again, and the menu's feeds follow you); 64.3 with the bar's icons and the phone app bar (2026-10-28); 59.2 with the Part of OpenVibe section (Shared 4.13, 2026-10-26); measured 35.8 (the guest task box lists example tasks); 39.6 with the home's "Ask in chat" row (10-15)
// 53.2 with the rich home (10-22): numbers, task templates, the 34 Tools chips, the API samples, the questions
htmlBrotliKB: 15, // 13.7 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 8.6; 9.6 with "Ask in chat"; 12.1 with the rich home
jsFiles: 5, // 5 (theme-loader, web-runtime, navbar, footer, boost: openvibe-shared/shell)
jsRawKB: 357.3, // 357.3 with openvibe-shared 4.153.0 (2026-11-08: signed in to the network, signed in here, on phones too; a page can turn away a carried card); 356.6 with openvibe-shared 4.152.0 (2026-11-08: the menu marks the page you are on and opens without the keyboard on a phone, floating cards follow you across OpenVibe sites (overlay.js), and the navbar preconnects to the next OpenVibe site on intent); 353.9 with openvibe-shared 4.150.0 (2026-11-08: the bell opens the Dock on Activity, the Dock's phone header is in two rows, and the menu's orb is the close button with an open/close animation); 352.9 with openvibe-shared 4.149.0 (2026-11-08: the menu has tabs at the top; the phone's menu button is the orb of 4.148.0); 350.2 with openvibe-shared 4.142.0 (2026-11-07: the menu has a close button on a phone); 349.2 with openvibe-shared 4.139.0 (2026-11-07: the menu's feed rows lead with their picture, the footer and glyphs defer 38 KB); 348.2 with openvibe-shared 4.137.0 (2026-11-07: every script by its content hash, one menu button on a phone, the menu warms at idle); 346.1 with openvibe-shared 4.135.0 (2026-11-07: Ask your agent from any page (kit.askAgent), the Dock's Agent panel reads the timeline again, and the menu's feeds follow you); 345.4 with openvibe-shared 4.133.0 (2026-11-07: your feeds in the menu, the navbar keeps the Frame cards again, Messages and the balances on every signed-in site, and your online status); 345.4 with openvibe-shared 4.133.0 (2026-11-07: your feeds in the menu, the navbar keeps the Frame cards again, Messages and the balances on every signed-in site, and your online status); 343 with openvibe-shared 4.129.0 (2026-11-07: the one menu (the burger opens the OpenVibe menu)); 342 with openvibe-shared 4.122.0 (2026-11-06: the featured link's phone button (4.119.0), the Dock's agent view (4.120.0), the agent view in the Dock's pop-out (4.121.0) and the thread start page's head (4.122.0)); 340.4 with openvibe-shared 4.118.0 (2026-11-06: the Locker re-auth link, the 44 px footer and TLD targets); 338.4 with openvibe-shared 4.114.0 (2026-11-06: the one Share sheet in the navbar (ov-share.js and ov-qr.js load on the first press); navbar.js gains its click handler); 337.1 with openvibe-shared 4.112.1 (2026-11-06: theme-loader.js (4.111.1) and the footer/appbar phone CSS (4.112.1)); 335.8 with openvibe-shared 4.109.2 (2026-11-03: OpenVibe's own select, the navbar loads ov-forms); 333.1 with openvibe-shared 4.107.0 (2026-11-03: open.<tld> sites are named for their domain; the brand knows its front door); 332.5 with openvibe-shared 4.106.2 (2026-11-02: the site's name reads whole on a signed-in phone; the balances stack); 331.3 with openvibe-shared 4.103.0 (2026-11-02: the puzzle glyph, two-line sidebar rows, boost lets parsed pages go, eight solids) (2026-11-02); 328.2 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 324.8 with openvibe-shared 4.66.0: the mark with depth and motion, ov-fx loaded on request (2026-10-31); 323.2 with openvibe-shared 4.59.0: the Pair mark beside the name, set in the brand face (2026-10-31); 321.1 with openvibe-shared 4.53.0: the footer's columns fold on phones (2026-10-30); 318.1 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 315.6 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 307.8 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 300.5 openvibe-shared 4.25.0: the menu's orb beside the brand, both balances on phones, messages, the account sheet on phones and the bar stepping away on phones; 293 openvibe-shared 4.22.0: the navbar's magnifier search button and More dropdown (search.js and loading.js load only when used); 287.2 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 273.2 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 264.3 with Shared 4.13.0 (2026-10-26: account links); 239.8
jsBrotliKB: 85.2, // 85.2 with openvibe-shared 4.155.0 (2026-11-08: the menu names each site by a short product name, so a name is never cut off); 85.1 with openvibe-shared 4.153.0 (2026-11-08: signed in to the network, signed in here, on phones too; a page can turn away a carried card); 85.0 with openvibe-shared 4.152.0 (2026-11-08: the menu marks the page you are on and opens without the keyboard on a phone, floating cards follow you across OpenVibe sites (overlay.js), and the navbar preconnects to the next OpenVibe site on intent); 84.4 with openvibe-shared 4.150.0 (2026-11-08: the bell opens the Dock on Activity, the Dock's phone header is in two rows, and the menu's orb is the close button with an open/close animation); 84.2 with openvibe-shared 4.149.0 (2026-11-08: the menu has tabs at the top; the phone's menu button is the orb of 4.148.0); 83.7 with openvibe-shared 4.142.0 (2026-11-07: the menu has a close button on a phone); 83.5 with openvibe-shared 4.139.0 (2026-11-07: the menu's feed rows lead with their picture, the footer and glyphs defer 38 KB); 83.1 with openvibe-shared 4.137.0 (2026-11-07: every script by its content hash, one menu button on a phone, the menu warms at idle); 82.6 with openvibe-shared 4.135.0 (2026-11-07: Ask your agent from any page (kit.askAgent), the Dock's Agent panel reads the timeline again, and the menu's feeds follow you); 82.5 with openvibe-shared 4.133.0 (2026-11-07: your feeds in the menu, the navbar keeps the Frame cards again, Messages and the balances on every signed-in site, and your online status); 82.5 with openvibe-shared 4.133.0 (2026-11-07: your feeds in the menu, the navbar keeps the Frame cards again, Messages and the balances on every signed-in site, and your online status); 81.9 with openvibe-shared 4.129.0 (2026-11-07: the one menu (the burger opens the OpenVibe menu)); 81.7 with openvibe-shared 4.122.0 (2026-11-06: the featured link's phone button (4.119.0), the Dock's agent view (4.120.0), the agent view in the Dock's pop-out (4.121.0) and the thread start page's head (4.122.0)); 81.5 with openvibe-shared 4.118.0 (2026-11-06: the Locker re-auth link, the 44 px footer and TLD targets); 81.1 with openvibe-shared 4.114.0 (2026-11-06: the one Share sheet in the navbar (ov-share.js and ov-qr.js load on the first press); navbar.js gains its click handler); 80.9 with openvibe-shared 4.112.1 (2026-11-06: theme-loader.js (4.111.1) and the footer/appbar phone CSS (4.112.1)); 80.2 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 79.5 with openvibe-shared 4.59.0: the Pair mark beside the name, set in the brand face (2026-10-31); 78.2 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 77.6 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 76.1 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 74.2 openvibe-shared 4.25.0: the menu's orb beside the brand, both balances on phones, messages, the account sheet on phones and the bar stepping away on phones; 70.5 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 67.9 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 65.3 Shared 4.12.0: the light/dark switch, the seasonal mark and the theme pairs; 56.5
cssFiles: 5, // 5 with the shared kit.css and room.css (2026-10-29: the home as a taste and a door, 17 → 5.1 phone screens); 2 (actor.css + the cached /shared/showcase.css); 3 with home.css, the home's own sections (10-22)
-- cssRawKB: 117.2, // 117.2 with openvibe-shared 4.149.0 (2026-11-08: the menu has tabs at the top; the phone's menu button is the orb of 4.148.0); 117.1 with openvibe-shared 4.135.0 (2026-11-07: Ask your agent from any page (kit.askAgent), the Dock's Agent panel reads the timeline again, and the menu's feeds follow you); 116.4 with openvibe-shared 4.107.3 (2026-11-03: a custom room picture sits under the text on phones); 115.3 with 2026-11-02: openvibe-shared 4.100.1 (the conversation kit's parts, date covers, room CSS) (2026-11-02); 114.5 with openvibe-shared 4.93.2: content labels, glyph fallback, footer counts (2026-11-01); 113.5 with 2026-11-01: openvibe-shared 4.91.0 (card faces, audio-only player, busy buttons) (2026-11-01); 111.9 with 2026-11-01: phone table cards (2026-11-01); 110.4 with openvibe-shared 4.83.0: the -text status tokens in kit.css and room.css (2026-11-01); 109.5 with openvibe-shared 4.82.0: the motion tokens in kit.css and room.css (2026-11-01); 107.4 with openvibe-shared 4.81.0: shape and elevation tokens in kit.css and room.css (2026-11-01); 101.3 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 97.9 with openvibe-shared 4.62.0: the shared sidebar in kit.css, the glyphs and their fonts (2026-10-31); 92.5 with the home's folds (the parts a reader rarely needs fold on a phone, 2026-10-30); 83 the home's numbers, rows and folds (kit.css, 25.6 KB raw) and its rail of task cards (room.css, 15.7 KB raw), both cached /shared/ files: the page went from 17 phone screens to 5.1 (2026-10-29); 41.8 openvibe-shared 4.26.1 (hero visual); 35.1 with Shared 4.13.0 (2026-10-26: account links); 24.2 (the task page's outcome card and timeline); 28.9 with home.css
++ cssRawKB: 120.5, // 119.2 with the Actor v3 home's example card (2026-11-09); was 117.2. 117.2 with openvibe-shared 4.149.0 (2026-11-08: the menu has tabs at the top; the phone's menu button is the orb of 4.148.0); 117.1 with openvibe-shared 4.135.0 (2026-11-07: Ask your agent from any page (kit.askAgent), the Dock's Agent panel reads the timeline again, and the menu's feeds follow you); 116.4 with openvibe-shared 4.107.3 (2026-11-03: a custom room picture sits under the text on phones); 115.3 with 2026-11-02: openvibe-shared 4.100.1 (the conversation kit's parts, date covers, room CSS) (2026-11-02); 114.5 with openvibe-shared 4.93.2: content labels, glyph fallback, footer counts (2026-11-01); 113.5 with 2026-11-01: openvibe-shared 4.91.0 (card faces, audio-only player, busy buttons) (2026-11-01); 111.9 with 2026-11-01: phone table cards (2026-11-01); 110.4 with openvibe-shared 4.83.0: the -text status tokens in kit.css and room.css (2026-11-01); 109.5 with openvibe-shared 4.82.0: the motion tokens in kit.css and room.css (2026-11-01); 107.4 with openvibe-shared 4.81.0: shape and elevation tokens in kit.css and room.css (2026-11-01); 101.3 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 97.9 with openvibe-shared 4.62.0: the shared sidebar in kit.css, the glyphs and their fonts (2026-10-31); 92.5 with the home's folds (the parts a reader rarely needs fold on a phone, 2026-10-30); 83 the home's numbers, rows and folds (kit.css, 25.6 KB raw) and its rail of task cards (room.css, 15.7 KB raw), both cached /shared/ files: the page went from 17 phone screens to 5.1 (2026-10-29); 41.8 openvibe-shared 4.26.1 (hero visual); 35.1 with Shared 4.13.0 (2026-10-26: account links); 24.2 (the task page's outcome card and timeline); 28.9 with home.css
cssBrotliKB: 27, // 25.7 with openvibe-shared 4.107.3 (2026-11-03: a custom room picture sits under the text on phones); 25.2 with openvibe-shared 4.93.2: content labels, glyph fallback, footer counts (2026-11-01); 24.6 with 2026-11-01: phone table cards (2026-11-01); 24.3 with openvibe-shared 4.83.0: the -text status tokens in kit.css and room.css (2026-11-01); 24.3 with openvibe-shared 4.82.0: room over the rounded measure (2026-11-01); 24.0 with openvibe-shared 4.82.0: the motion tokens in kit.css and room.css (2026-11-01); 23.5 with openvibe-shared 4.81.0: shape and elevation tokens in kit.css and room.css (2026-11-01); 22.1 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 21.3 with openvibe-shared 4.62.0: the shared sidebar in kit.css, the glyphs and their fonts (2026-10-31); 20.5 with the home's folds (the parts a reader rarely needs fold on a phone, 2026-10-30); 18.2 with kit.css and room.css (the short home, 2026-10-29); 7.7 with Shared 4.13.0 (2026-10-26: account links); 5.4; 6.6 with home.css // raised 9 -> 10 for the openvibe-shared Kit 2 pin (kit.css grew)
externalFiles: 0, // 0
};