openvibers/openvibe.codes Commits

openvibe.codes: a visitor gets the front door, never empty panels (#217)

OP OpenVibers committed 12 days ago · 18c3d8003e · 6 files, +95 −9

* openvibe.codes: a visitor gets the front door (a hand-over shown, every agent, your plans and keys, the conductor), never empty panels

* Codes front door: tests follow it (a visitor gets the product, a signed-in person the chat); Your computer links Run; CSS budget 72

Changes

ddiff --git a/public/css/c4.css b/public/css/c4.css
iindex dd47074..4e015b1 100644
---- a/public/css/c4.css
++++ b/public/css/c4.css
@@@ -287,3 +287,18 @@
.c4-conflict-pair{grid-template-columns:1fr}
}
:root[data-theme-mode="dark"] .c4-form textarea{background:#11181b;color:var(--c4-ink)}
++
++/* The front door's example conversation (a visitor's home): one turn, a hand-over, the result. */
++.c4-demo{display:grid;gap:10px;padding:16px;border-radius:18px;border:1px solid var(--c4-line);background:var(--c4-card,#fff);font-size:14px;line-height:1.45;max-width:460px}
++.c4-demo-you{justify-self:end;max-width:85%;margin:0;padding:9px 13px;border-radius:16px 16px 4px 16px;background:color-mix(in srgb,var(--c4-accent,#2563eb) 14%,transparent)}
++.c4-demo-agent{display:grid;grid-template-columns:auto minmax(0,1fr);gap:10px;align-items:start}
++.c4-demo-agent p{margin:4px 0 0}
++.c4-demo-face{display:grid;place-items:center;width:30px;height:30px;border-radius:10px;font-size:11px;font-weight:800;color:#fff;background:#d97757}
++.c4-demo-face.is-cx{background:#10a37f}
++.c4-demo-meta{display:block;font-size:12px;color:var(--c4-muted,#64748b)}
++.c4-demo-hand{margin:2px 0;text-align:center;font-size:12px;color:var(--c4-muted,#64748b);display:flex;align-items:center;gap:10px}
++.c4-demo-hand::before,.c4-demo-hand::after{content:"";flex:1;height:1px;background:var(--c4-line)}
++.c4-demo-result{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:10px 12px;border-radius:12px;border:1px solid var(--c4-line);font-size:13px}
++.c4-demo-ok{padding:2px 9px;border-radius:999px;font-size:12px;font-weight:700;color:#15803d;background:color-mix(in srgb,#22c55e 14%,transparent)}
++.c4-composer-front{margin:8px 0 4px}
++.c4-composer-foot{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:10px;margin-top:10px}
ddiff --git a/server/http/c4.js b/server/http/c4.js
iindex 618654c..d1ef085 100644
---- a/server/http/c4.js
++++ b/server/http/c4.js
@@@ -309,6 +309,55 @@ ${pr.problem ? `<p class="c4-hint">${esc(pr.problem.detail)}</p>` : `<dl class="
page(req, res, 'Conductor', wrap(content));
}
++ // ── The front door (a visitor): what Codes is, shown rather than told ──
++ // The owner, 10-31: the empty "Running" and "Recent tasks" panels made the site look unfinished. A visitor gets the
++ // product: one conversation where an agent hands over to another, every agent in the catalogue, your own plans and
++ // keys, your machine or ours, the conductor, and the questions people ask. The composer stays: it is the start.
++ function visitorHome() {
++ const agents = (harnesses?.list?.() || []);
++ const thread = `<div class="c4-demo" aria-label="An example conversation">
++<p class="c4-demo-you">Add a dark mode switch to the settings page, with a test.</p>
++<div class="c4-demo-agent"><span class="c4-demo-face" aria-hidden="true">CC</span><div><b>Claude Code</b><span class="c4-demo-meta">Worked 3 min · 14 steps · read, edit, run</span><p>Added the switch to <code>settings.js</code>, saved the choice per person, and wrote <code>settings.test.js</code>. Two of three tests pass.</p></div></div>
++<p class="c4-demo-hand" role="note">Codex took over: Claude Code hit its plan limit</p>
++<div class="c4-demo-agent"><span class="c4-demo-face is-cx" aria-hidden="true">CX</span><div><b>Codex</b><span class="c4-demo-meta">Worked 1 min · 6 steps · picked up the diff</span><p>Fixed the failing case (the saved choice loaded after first paint).</p></div></div>
++<div class="c4-demo-result"><b>PR #42</b><span>3 files · +84 −12</span><span class="c4-demo-ok">CI passed</span><span class="c4-demo-ok">Deployed</span></div>
++</div>`;
++ const composer = `<section class="c4-card c4-composer c4-composer-front"><form method="post" action="/start"><label for="c4-prompt">What should an agent do?</label><textarea id="c4-prompt" name="prompt" placeholder="Describe the change, question or fix" required></textarea><div class="c4-composer-foot"><span class="c4-muted">Free with your own plans and keys. Sign in to start.</span><button class="c4-button" type="submit">Start a task</button></div></form></section>`;
++ return `${showcase.hero({
++ eyebrow: 'OpenVibe.Codes · alpha',
++ title: 'Every coding agent.', accent: 'One place.',
++ lede: 'Claude Code, Codex, OpenCode, Command Code, DeepSeek and models you run yourself, in one conversation. Bring the plans and keys you already pay for, run on your computer or ours, and follow the work from your phone.',
++ actions: [{ label: 'Start a task', href: '#c4-prompt', primary: true }, { label: 'How it works', href: '#how' }],
++ note: 'Open source (AGPL-3.0). No fee on your own plans and keys.',
++ aside: { html: thread },
++ })}
++${composer}
++${showcase.steps({ id: 'how', title: 'How it works', items: [
++ { title: 'Say what you want', text: 'A change, a question or a fix, in plain words, on any screen.' },
++ { title: 'Pick who works on it', text: 'Auto, or any agent and its model, with how hard it should think on each turn.' },
++ { title: 'It runs where you choose', text: 'Your own computer, paired with one command, with your tools and logins. A hosted sandbox is coming.' },
++ { title: 'Watch, approve, ship', text: 'Every step as it happens, the diff, the tests, page shots, then the pull request with CI and the deploy.' },
++ ] })}
++${agents.length ? showcase.features({ id: 'agents', title: 'Every agent you use', lede: `${agents.length} agents in the catalogue. When one stops or hits its limit, the next picks up with the diff and a summary.`, items: agents.map((o) => ({ title: o.name, text: [o.provider, ...(o.capabilities ? Object.keys(o.capabilities).filter((k) => o.capabilities[k] === true).slice(0, 2) : [])].filter(Boolean).join(' · '), href: `/harnesses#${o.id}` })) }) : ''}
++${showcase.features({ id: 'yours', title: 'Your plans, your keys, your machine', items: [
++ { title: 'Your subscriptions', text: 'Sign in with the Claude, ChatGPT, OpenCode or Command Code plan you already have. No key of ours and no fee.' },
++ { title: 'Your keys', text: 'Saved once in OpenVibe AI and used by every OpenVibe site. Never shown again, never sent anywhere else.' },
++ { title: 'Your computer', text: 'Pair it once from openvibe.network. Tasks run there with your own tools, and your logins never leave it.', href: '/run' },
++ { title: 'Costs you can follow', text: 'Every turn shows its tokens, what came from cache and who paid, on openvibe.report.' },
++ ] })}
++${showcase.features({ id: 'conductor', title: 'A conductor for the big jobs', lede: 'One Claude Code session plans the work, hands the parts to other agents in parallel, reads what they did, and asks you before anything risky.', items: [
++ { title: 'Plans, then delegates', text: 'Backend to one agent, tests to another, docs to a third, at the same time.' },
++ { title: 'Asks before it acts', text: 'Pushing, deleting or spending waits for your Allow in the app.' },
++ { title: 'Remembers the thread', text: 'One long conversation you can pick up from any device.' },
++ ] })}
++${showcase.faq({ items: [
++ ['Do I need an OpenVibe key?', 'No. Codes runs on the plans and keys you already have. An OpenVibe key is there for people who want one bill, with a small fee.'],
++ ['Where does my code run?', 'On a computer you pair, with your own tools. A hosted sandbox is coming for people without one.'],
++ ['Can I switch agents in the middle?', 'Yes. Any turn can name another agent; it picks up the diff and a summary of the conversation so far.'],
++ ['Is it open source?', 'Yes, AGPL-3.0, with the command-line harness you can run on its own.'],
++ ] })}`;
++ }
++
// ── Home ────────────────────────────────────────────────────────────────────────────────────────────────────
r.get('/', async (req, res) => {
const rows = await ownTasks(req, 8);
@@@ -330,7 +379,8 @@ ${signedIn ? `<section class="c4-card c4-conductor-cta"><div class="c4-card-head
<section class="c4-card"><div class="c4-card-head"><h2>Recent tasks</h2><a href="/tasks">View all</a></div>${rows.slice(0, 5).map(rowHtml).join('') || '<div class="c4-emptyline">No tasks yet. <a href="/#c4-prompt">Start a task</a></div>'}</section>
${signedIn && !mine.ok && mine.problem ? note(`Your machines could not be read, so the machine picker has only Auto. ${esc(mine.problem.detail || '')}`, 'bad') : ''}
${signedIn ? '' : `<section class="c4-reasons"><h2>Built for the way you work</h2><div class="c4-grid">${[['Any agent', 'Bring a key or plan. Auto picks a good fit and can hand over when one gets stuck.'], ['Your computer or ours', 'Pair your computer with one command. A hosted sandbox is coming soon.'], ['Proof you can inspect', 'See the changes, tests, screenshots and review before you approve.'], ['A cost you can follow', 'Track task costs and plan limits. Set caps that stop with a wrap-up.']].map(([a, b]) => `<article><h3>${a}</h3><p>${b}</p></article>`).join('')}</div></section>`}`;
-- page(req, res, null, wrap(content), signedIn ? {} : { index: true, styles: [showcase.STYLESHEET], jsonLd: homeJsonLd(config) });
++ if (!signedIn) return page(req, res, null, wrap(visitorHome()), { index: true, styles: [showcase.STYLESHEET], jsonLd: homeJsonLd(config) });
++ page(req, res, null, wrap(content), {});
});
/**
ddiff --git a/test/c4.test.js b/test/c4.test.js
iindex aef151d..48df7bb 100644
---- a/test/c4.test.js
++++ b/test/c4.test.js
@@@ -19,6 +19,19 @@ const { boot, check, done } = require('./helpers/boot');
assert.ok(task.text.includes('Sign in to see your work'));
});
++ await check('a visitor gets the front door, never empty panels (owner, 10-31)', async () => {
++ const home = await t.get('/');
++ assert.strictEqual(home.status, 200);
++ const h = home.text;
++ assert.ok(h.includes('Every coding agent.') && h.includes('One place.'), 'the hero');
++ assert.ok(h.includes('class="c4-demo"') && h.includes('Codex took over: Claude Code hit its plan limit'), 'a conversation that shows a hand-over');
++ assert.ok(h.includes('id="how"') && h.includes('Pick who works on it'), 'how it works');
++ assert.ok(h.includes('Your plans, your keys, your machine') && h.includes('A conductor for the big jobs'));
++ assert.ok(h.includes('action="/start"') && h.includes('id="c4-prompt"'), 'the composer is the start');
++ assert.ok(!h.includes('<h2>Running</h2>') && !h.includes('<h2>Recent tasks</h2>') && !h.includes('No tasks yet'), 'no empty panels for a visitor');
++ assert.match(require('fs').readFileSync(require('path').join(__dirname, '..', 'server/http/c4.js'), 'utf8'), /if \(!signedIn\) return page\(req, res, null, wrap\(visitorHome\(\)\)/, 'the route sends a visitor to the front door');
++ });
++
await check('task details are owner scoped and management pages honor sign-in', async () => {
const settings = await t.get('/settings', { as: ada });
assert.strictEqual(settings.status, 200);
ddiff --git a/test/home.test.js b/test/home.test.js
iindex 5b375bd..13b9a85 100644
---- a/test/home.test.js
++++ b/test/home.test.js
@@@ -35,17 +35,24 @@ const { boot, check, done } = require('./helpers/boot');
const t = await boot();
try {
-- await check('the home presents the Codes workspace and its composer', async () => {
-- let r = await t.get('/');
++ await check('a visitor gets the front door: the product shown, the composer, never empty panels', async () => {
++ const r = await t.get('/');
assert.strictEqual(r.status, 200);
-- assert.match(r.text, /Every coding agent\. One place\./);
++ assert.match(r.text, /<h1>Every coding agent\.<span class="sc-accent"> One place\.<\/span><\/h1>/);
assert.match(r.text, /What should an agent do\?/);
-- assert.match(r.text, /Needs you/);
-- assert.match(r.text, /Recent tasks/);
++ assert.ok(r.text.includes('class="c4-demo"') && r.text.includes('Codex took over'), 'a hand-over shown');
++ assert.ok(r.text.includes('id="how"') && r.text.includes('id="yours"') && r.text.includes('id="conductor"'), 'how it works, yours, the conductor');
++ assert.ok(!r.text.includes('Needs you') && !r.text.includes('Recent tasks'), 'no empty panels for a visitor');
const blocks = [...r.text.matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)].map((m) => JSON.parse(m[1]));
assert.ok(blocks.some((b) => b['@type'] === 'WebSite'), 'the home keeps its website metadata');
assert.strictEqual((r.text.match(/<h1[\s>]/g) || []).length, 1, 'one h1');
});
++ await check('a signed-in person gets the chat, not the front door', async () => {
++ const r = await t.get('/', { as: t.network.addUser('rosa') });
++ assert.strictEqual(r.status, 200);
++ assert.match(r.text, /What should we build/);
++ assert.ok(!r.text.includes('class="c4-demo"'), 'no front door once signed in');
++ });
} finally { await t.close(); }
done();
})().catch((e) => { console.error(e); process.exit(1); });
ddiff --git a/test/perf-budget.test.js b/test/perf-budget.test.js
iindex af1ba51..8559983 100644
---- a/test/perf-budget.test.js
++++ b/test/perf-budget.test.js
@@@ -18,7 +18,7 @@ const BUDGETS = {
jsRawKB: 343, // 342.2 with openvibe-shared 4.66.0: the mark with depth and motion, ov-fx loaded on request (2026-10-31); 341.4 with openvibe-shared 4.64.0: the look travels between sites, the Look panel in the menu (2026-10-31); 340.6 with openvibe-shared 4.59.0: the Pair mark beside the name, set in the brand face (2026-10-31); 330.9 after C9b: the conductor's message box with its images and its live turn (2.6 KB in js/c4.js); 326.2 after C4b: the home page loads the workspace script for its repository search (10.9 KB of its own); 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; 263.4 with shared 4.11.0's seasonal navbar; 254.1 with openvibe-shared 4.9.0 (status text twins in every theme, 2026-10-23); 246.2 with openvibe-shared 4.2.x (Ask Actor in the navbar, 2026-10-15); 239.1 (212.2 before openvibe-shared/shell's web-runtime.js)
jsBrotliKB: 84, // 82.9 after C9b's conductor script; 80.6 after C4b's repository search on the home page; 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; 61.7 with shared 4.11.0's seasonal navbar; 59.1 with openvibe-shared 4.4.0 (every site in the launcher, 2026-10-17); 56.3 (49.9 before web-runtime.js)
cssFiles: 3, // C4 adds its responsive workspace stylesheet alongside codes.css and run.css.
-- cssRawKB: 70, // 69.3 after the Memory M2b review: the quiet Remove link, the phone sources fold, the in-place note menu and the danger buttons (public/css/c4.css, 2.2 KB); 67.1 after Memory M2b: the Memory page's sources, notes, note menus, conflicts and tabs (public/css/c4.css, 2.4 KB); 63.0 after C9c: the fleet's rows and its meters on the Agents page (public/css/c4.css, 1.2 KB); 62.4 after C9b: the conductor's turns, approvals inbox and composer (public/css/c4.css); 58.4 after C4b: the proof rows, the fold-out panels, the shots, the forms that remember choices and their light/dark styles; 46 KB after adding the C4 phone-first workspace, cards, task timeline and light/dark styles.
++ cssRawKB: 72, // 70.7 after the front door: a visitor's example conversation, its hand-over line and result row (public/css/c4.css, 1.4 KB, 2026-10-31); 69.3 after the Memory M2b review: the quiet Remove link, the phone sources fold, the in-place note menu and the danger buttons (public/css/c4.css, 2.2 KB); 67.1 after Memory M2b: the Memory page's sources, notes, note menus, conflicts and tabs (public/css/c4.css, 2.4 KB); 63.0 after C9c: the fleet's rows and its meters on the Agents page (public/css/c4.css, 1.2 KB); 62.4 after C9b: the conductor's turns, approvals inbox and composer (public/css/c4.css); 58.4 after C4b: the proof rows, the fold-out panels, the shots, the forms that remember choices and their light/dark styles; 46 KB after adding the C4 phone-first workspace, cards, task timeline and light/dark styles.
cssBrotliKB: 15, // 14.2 after the Memory M2b review (the same quiet link, sources fold, in-place menu and danger buttons); 13.0 after C9b's conductor styles; 12.1 after C4b's rows, panels and forms; 9.9 KB after adding the C4 workspace styles.
externalFiles: 2, // 0
};
ddiff --git a/test/start-page.test.js b/test/start-page.test.js
iindex 7a375a2..ba77b08 100644
---- a/test/start-page.test.js
++++ b/test/start-page.test.js
@@@ -24,11 +24,12 @@ const { version } = require('../package.json');
assert.match(r.text, /<link rel="canonical" href="https:\/\/openvibe\.codes\/start">/);
});
-- await check('the workspace home leads to task creation and exposes its status sections', async () => {
++ await check('the home leads to task creation: the composer and Run for a visitor, the chat for a signed-in person', async () => {
const r = await t.get('/');
assert.ok(r.text.includes('href="/run"'));
assert.ok(r.text.includes('What should an agent do?'));
-- assert.ok(r.text.includes('Needs you') && r.text.includes('Running') && r.text.includes('Recent tasks'));
++ const me = await t.get('/', { as: t.network.addUser('ada') });
++ assert.match(me.text, /What should we build/);
});
await check('the command line prints this version and points at the guide', () => {