openvibers/openvibe.codes Commits

The start page: Create, OpenVibe, My code; options behind one button; no keyboard by surprise (#333)

OP OpenVibers committed 4 days ago · 8015d054cf · 7 files, +63 −21

* The start page: Create, OpenVibe, My code; options behind one button; no keyboard by surprise

The owner, 11-09: too many options in the composer ("hidden under a cog"), "New project" sounds scary, "OpenVibe site"
should be "OpenVibe", the heading should follow the way you start, examples should rotate, and picking a mode put the
keyboard up and covered everything.
- The modes are Create, OpenVibe and My code, three equal parts with icons; the question follows the checked one
  ("What should we make?", "What should we improve on OpenVibe?", "What should we work on?").
- In the bar: attach and, for your own code, the repository. Everything else (what it is, the project, the agent,
  the effort) is behind Options (openvibe-shared 4.160.0's thread.composer options), each a full row; a changed one
  shows as a chip beside the button.
- The box's placeholder walks through the mode's ideas.
- On a touch screen nothing takes the focus by itself (a mode, an idea, a saved repository); with the keyboard up the
  mark and the line under the question fold away (data-ovt-fold).
- What Codes will do and where it runs stay under the box, as one quiet list with its action as a button.

* Merge remote-tracking branch 'origin/main' into codes-composer-1109

Changes

ddiff --git a/public/css/chat.css b/public/css/chat.css
iindex 66a9b9d..6946ea3 100644
---- a/public/css/chat.css
++++ b/public/css/chat.css
@@@ -201,3 +201,27 @@ html[data-ov-motion=reduced] .ch-spark-list[aria-hidden],html[data-ov-motion=red
.ch .ch-setup-text b{color:var(--ch-ink);font-size:14px;font-weight:650}
.ch .ch-setup-text small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12.5px;color:var(--ch-muted)}
.ch .ch-setup-nudge-go{flex:none;width:16px;height:16px;transform:rotate(-90deg)}
++/* ── 11-09: the composer's Options (openvibe-shared thread.composer options). Inside the panel each picker is a full row:
++ its name on the left in small type, the value as the control, the caret on the right; the kinds are a wrap of chips. ── */
++.ovt-opts-body .ch-pick{display:flex;width:100%;height:52px;padding:0 40px 0 14px;border:0;border-radius:14px;background:color-mix(in srgb,var(--ch-ink) 5%,transparent);color:var(--ch-ink);font-size:15px}
++.ovt-opts-body .ch-pick:hover{background:color-mix(in srgb,var(--ch-ink) 8%,transparent)}
++.ovt-opts-body .ch-pick .ch-sr,.ovt-opts-body .ch-kinds>.ch-sr{position:static;width:auto;height:auto;overflow:visible;clip:auto;flex:none;min-width:88px;color:var(--ch-muted);font-size:13px;font-weight:600}
++.ovt-opts-body .ch-pick select{flex:1;max-width:none;height:100%;font-size:15px;text-align:right;text-align-last:right;padding-right:6px}
++.ovt-opts-body .ch-pick .ch-i:not(.ch-caret){display:none}
++.ovt-opts-body .ch-pick .ch-caret{right:14px}
++.ovt-opts-body .ch-kinds{display:flex;flex-wrap:wrap;gap:6px;padding:10px 12px 12px;border-radius:14px;background:color-mix(in srgb,var(--ch-ink) 5%,transparent)}
++.ovt-opts-body .ch-kinds>.ch-sr{flex-basis:100%;margin-bottom:2px}
++.ovt-opts-body .ch-kind,.ovt-opts-body .ch-dim{height:40px}
++/* In the bar: the repository as one chip that takes the room left. */
++.ovt-c-tools .ch-pick-repo{flex:1 1 180px;min-width:0}
++.ovt-c-tools .ch-pick-repo input{width:100%}
++/* What Codes will do and where it runs: one quiet list under the box, each row an icon and a line, the same size. */
++.ch-picked,.ch-where>span{min-height:24px;align-items:center}
++.ch-where a{display:inline-flex;align-items:center;min-height:32px;padding:0 10px;margin-left:2px;border-radius:999px;background:color-mix(in srgb,var(--ch-accent) 12%,transparent);text-decoration:none}
++/* The modes: three equal parts, the question above follows the checked one. */
++.ch-modes{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));width:min(420px,100%)}
++.ch-mode{justify-content:center}
++@media (max-width:520px){.ch-mode{padding:0 8px}.ch-mode:not(:has(input:checked)) .ch-mode-i{display:inline-grid}}
++.ch-hello h1>span{animation:ch-ask-in .28s cubic-bezier(.2,.9,.3,1) both}
++@keyframes ch-ask-in{from{opacity:0;transform:translateY(4px)}}
++@media (prefers-reduced-motion:reduce){.ch-hello h1>span{animation:none}}
ddiff --git a/public/js/chat.js b/public/js/chat.js
iindex aa7b8cd..acf55a2 100644
---- a/public/js/chat.js
++++ b/public/js/chat.js
@@@ -58,12 +58,18 @@
lists.forEach((ul, n) => { ul.textContent = ''; sets[mode].forEach((i) => ul.appendChild(card(i, n > 0))); });
pace();
}
++ // A touch screen keeps its keyboard down until the box is tapped (11-09, the owner: picking a mode "puts up the
++ // keyboard automatically… takes up a lot of space… overrides everything").
++ const fine = !matchMedia('(pointer: coarse)').matches;
function choose(mode, { focus = false } = {}) {
const radio = modes.find((r) => r.value === mode);
if (radio && !radio.checked) radio.checked = true;
if (text && PLACEHOLDER[mode]) text.placeholder = PLACEHOLDER[mode];
++ // The box's placeholder walks through this mode's ideas (ov-thread.js reads data-ovt-examples).
++ if (text && sets[mode]) text.setAttribute('data-ovt-examples', JSON.stringify(sets[mode].slice(0, 12).map((i) => i.text)));
if (rail && rail.dataset.chShown !== mode) { rail.dataset.chShown = mode; fill(mode); }
-- if (focus && text) text.focus();
++ if (form) form.dispatchEvent(new Event('ovt:options'));
++ if (focus && text && fine) text.focus();
}
modes.forEach((r) => r.addEventListener('change', () => { if (r.checked) { choose(r.value, { focus: true }); pick(); } }));
@@@ -110,9 +116,9 @@
if (b.dataset.chIdeaKind) setKind(b.dataset.chIdeaKind);
text.value = b.dataset.chIdea;
grow();
-- text.focus();
-- // The words are the person's to change: the caret goes to the end.
-- text.setSelectionRange(text.value.length, text.value.length);
++ // The words are the person's to change: on a computer the caret goes to the end; a phone keeps the
++ // keyboard down so the filled box and Send stay in view.
++ if (fine) { text.focus(); text.setSelectionRange(text.value.length, text.value.length); }
});
const pause = $('[data-ch-ideas-pause]', rail);
if (pause) pause.addEventListener('click', () => {
@@@ -139,7 +145,7 @@
startPage(app, form, text);
// The repository: a recent one is a tap away, and typing searches every repository the person can reach.
const repo = $('[data-ch-repo]', form);
-- app.querySelectorAll('[data-ch-saved-repo]').forEach((b) => b.addEventListener('click', () => { if (repo) { repo.value = b.dataset.chSavedRepo; text.focus(); } }));
++ app.querySelectorAll('[data-ch-saved-repo]').forEach((b) => b.addEventListener('click', () => { if (repo) { repo.value = b.dataset.chSavedRepo; if (!matchMedia('(pointer: coarse)').matches) text.focus(); } }));
if (repo && repo.dataset.chRepoUrl) {
const list = document.getElementById(repo.getAttribute('list'));
let timer = 0, seq = 0;
@@@ -162,7 +168,7 @@
}
// The keyboard hint only where Enter sends (a phone keyboard's return is a new line).
if (status && !status.textContent && !matchMedia('(pointer: coarse)').matches) status.textContent = 'Enter sends · Shift+Enter for a new line';
-- if ($('.ovt.is-start', app)) text.focus({ preventScroll: true });
++ if ($('.ovt.is-start', app) && !matchMedia('(pointer: coarse)').matches) text.focus({ preventScroll: true });
}
if (form) {
// ── The composer's files: upload before send, with a chip, a thumbnail, a progress bar and a × ──
ddiff --git a/server/http/chat.js b/server/http/chat.js
iindex f1948df..02aa197 100644
---- a/server/http/chat.js
++++ b/server/http/chat.js
@@@ -420,22 +420,25 @@ function createChatRoutes(ctx) {
return `<option value="${esc(c.value)}"${chosen === c.value ? ' selected' : ''}${c.note ? ` title="${esc(c.note)}"` : ''}${attrs}>${esc(c.label)}</option>`;
};
const first = auto ? `<option value="">${esc(auto)}</option>` : opt(all[0]);
-- return `<label class="ch-pick${cls ? ` ${cls}` : ''}" title="${esc(label)}"><span class="ch-sr">${esc(label)}</span>${ico('agents')}<select name="who" data-ch-who data-ov-select="bare" data-ov-title="${esc(label)}">${first}${[...groups].map(([g, list]) => list.length > 1 ? `<optgroup label="${esc(g)}">${list.map(opt).join('')}</optgroup>` : opt(list[0])).join('')}</select>${ico('chevron', 'ch-caret')}</label>`;
++ // What the page opened with: a different pick shows as a chip beside the composer's Options (thread.js).
++ const opened = chosen && all.some((c) => c.value === chosen) ? chosen : auto ? '' : String(all[0].value);
++ return `<label class="ch-pick${cls ? ` ${cls}` : ''}" title="${esc(label)}"><span class="ch-sr">${esc(label)}</span>${ico('agents')}<select name="who" data-ch-who data-ov-select="bare" data-ov-title="${esc(label)}" data-ovt-default="${esc(opened)}">${first}${[...groups].map(([g, list]) => list.length > 1 ? `<optgroup label="${esc(g)}">${list.map(opt).join('')}</optgroup>` : opt(list[0])).join('')}</select>${ico('chevron', 'ch-caret')}</label>`;
}
// The effort in plain words (the shared ladder underneath; each agent gets the nearest level it accepts).
const EFFORT = [['', 'Effort: auto'], ['low', 'Quick'], ['medium', 'Balanced'], ['high', 'Thorough'], ['max', 'Deepest']];
const effortWord = (v) => (EFFORT.find(([k]) => k === v) || [null, v])[1];
function effortPicker(selected = '', cls = '') {
-- return `<label class="ch-pick${cls ? ` ${cls}` : ''}" title="How hard it thinks"><span class="ch-sr">Effort</span>${ico('sub')}<select name="effort" data-ch-effort data-ov-select="bare" data-ov-title="Effort">${EFFORT.map(([v, l]) => `<option value="${v}"${selected === v ? ' selected' : ''}>${l}</option>`).join('')}</select>${ico('chevron', 'ch-caret')}</label>`;
++ return `<label class="ch-pick${cls ? ` ${cls}` : ''}" title="How hard it thinks"><span class="ch-sr">Effort</span>${ico('sub')}<select name="effort" data-ch-effort data-ov-select="bare" data-ov-title="Effort" data-ovt-default="${esc(selected)}">${EFFORT.map(([v, l]) => `<option value="${v}"${selected === v ? ' selected' : ''}>${l}</option>`).join('')}</select>${ico('chevron', 'ch-caret')}</label>`;
}
/** Any file (not a program) up to 10 at once: the script adds chips and progress, the plain form posts multipart. */
function filePicker() {
return `<label class="ch-pick ch-pick-file ch-for-agents" title="Attach files">${ico('attach')}<span class="ch-sr">Attach files</span><input type="file" name="files" multiple data-ch-files></label>`;
}
/** The kit's composer with Codes' pickers in its tools, where the work runs under the bar, and its JSON route. */
-- function composer({ action, api = '', placeholder, pickers = '', live = false, stopAction = '', note = '', id = null, where = '', files = false }) {
++ function composer({ action, api = '', placeholder, pickers = '', options = '', examples = [], live = false, stopAction = '', note = '', id = null, where = '', files = false }) {
return thread.composer({
-- action, id, placeholder, label: placeholder, tools: pickers + (files ? filePicker() : ''),
++ action, id, placeholder, label: placeholder, tools: (files ? filePicker() : '') + pickers,
++ options: options ? { html: options, label: 'Options' } : null, examples,
foot: `${files ? '<div class="ch-files" data-ch-files-list role="list" aria-live="polite"></div>' : ''}${where}`, note,
attrs: { ...(api ? { 'data-ch-api': api } : {}), ...(files ? { enctype: 'multipart/form-data' } : {}) },
busy: live, stopAction: live && stopAction ? stopAction : null, maxlength: 16000,
@@@ -546,8 +549,10 @@ function createChatRoutes(ctx) {
// One switch (the owner, 11-07: the three tall cards "rework these"): a short name each; what it means is the line
// under the composer, which changes with the choice.
const MODE_CARDS = [
-- ['new', 'spark', 'New project', 'A website, game or app, built online: nothing to install'],
-- ['openvibe', 'ov', 'OpenVibe site', `Improve Live, Chat, Games or any of ${start.PROJECTS.length} OpenVibe projects`],
++ // 11-09, the owner: "project makes it sound a little scary… something cooler, that knows you can do different
++ // things"; and "instead of OpenVibe site, just OpenVibe: people work on OpenVibe, which lets you do any platform".
++ ['new', 'spark', 'Create', 'A website, game or app, built online: nothing to install'],
++ ['openvibe', 'ov', 'OpenVibe', `Improve Live, Chat, Games or any of ${start.PROJECTS.length} OpenVibe projects`],
['code', 'repo', 'My code', 'Any Git repository, private ones too; agents work on your computer'],
];
const modes = `<div class="ch-modes" role="radiogroup" aria-label="What are you making?">${MODE_CARDS.map(([v, icon, title, line]) => `<label class="ch-mode" title="${esc(line)}"><input type="radio" name="mode" value="${v}" form="ch-start"${mode === v ? ' checked' : ''} data-ch-mode><span class="ch-mode-i">${ico(icon)}</span><b>${esc(title)}</b></label>`).join('')}</div>`;
@@@ -556,13 +561,16 @@ function createChatRoutes(ctx) {
// What it is: icon chips from the design system's icons (the owner, 11-07: "switching from website to game should
// be easy … 2D or 3D … more icons"); a game asks 2D or 3D right beside it.
const dim = start.DIMS[req.query.dim] ? String(req.query.dim) : '2d';
-- const kindPick = `<div class="ch-kinds ch-for-new" role="radiogroup" aria-label="What it is">${Object.entries(start.KINDS).map(([v, k]) => `<label class="ch-kind"><input type="radio" name="kind" value="${v}" form="ch-start"${kind === v ? ' checked' : ''} data-ch-kind><span class="ov-icon" data-icon="${esc(k.icon || 'ov')}" data-size="22" aria-hidden="true"></span><span>${esc(k.label)}</span></label>`).join('')}<span class="ch-dims" role="radiogroup" aria-label="2D or 3D">${Object.keys(start.DIMS).map((d) => `<label class="ch-dim"><input type="radio" name="dim" value="${d}" form="ch-start"${dim === d ? ' checked' : ''}><span>${d.toUpperCase()}</span></label>`).join('')}</span></div>`;
++ const kindPick = `<div class="ch-kinds ch-for-new" role="radiogroup" aria-label="What it is" data-ovt-default="${esc(kind)}"><span class="ch-sr">What it is</span>${Object.entries(start.KINDS).map(([v, k]) => `<label class="ch-kind"><input type="radio" name="kind" value="${v}" form="ch-start"${kind === v ? ' checked' : ''} data-ch-kind><span class="ov-icon" data-icon="${esc(k.icon || 'ov')}" data-size="22" aria-hidden="true"></span><span>${esc(k.label)}</span></label>`).join('')}<span class="ch-dims" role="radiogroup" aria-label="2D or 3D" data-ovt-default="${esc(dim)}">${Object.keys(start.DIMS).map((d) => `<label class="ch-dim"><input type="radio" name="dim" value="${d}" form="ch-start"${dim === d ? ' checked' : ''}><span>${d.toUpperCase()}</span></label>`).join('')}</span></div>`;
const groups = [...new Set(start.PROJECTS.map((p) => p.group))];
-- const projectPick = `<label class="ch-pick ch-for-openvibe" title="Which OpenVibe project"><span class="ch-sr">Which OpenVibe project</span>${ico('ov')}<select name="project" data-ch-project data-ov-select="bare" data-ov-title="Which OpenVibe project"><option value="auto"${chosenProject === 'auto' ? ' selected' : ''}>Codes picks for you</option>${groups.map((g) => `<optgroup label="${esc(g)}">${start.PROJECTS.filter((p) => p.group === g).map((p) => `<option value="${esc(p.repository)}"${chosenProject === p.repository ? ' selected' : ''}${p.line ? ` title="${esc(p.line)}"` : ''}>${esc(p.name)}${p.domain ? ` · ${esc(p.domain)}` : ''}</option>`).join('')}</optgroup>`).join('')}</select>${ico('chevron', 'ch-caret')}</label>`;
++ const projectPick = `<label class="ch-pick ch-for-openvibe" title="Which OpenVibe project"><span class="ch-sr">Which OpenVibe project</span>${ico('ov')}<select name="project" data-ch-project data-ov-select="bare" data-ov-title="Which OpenVibe project" data-ovt-default="${esc(chosenProject)}"><option value="auto"${chosenProject === 'auto' ? ' selected' : ''}>Codes picks for you</option>${groups.map((g) => `<optgroup label="${esc(g)}">${start.PROJECTS.filter((p) => p.group === g).map((p) => `<option value="${esc(p.repository)}"${chosenProject === p.repository ? ' selected' : ''}${p.line ? ` title="${esc(p.line)}"` : ''}>${esc(p.name)}${p.domain ? ` · ${esc(p.domain)}` : ''}</option>`).join('')}</optgroup>`).join('')}</select>${ico('chevron', 'ch-caret')}</label>`;
const repoPick = `<label class="ch-pick ch-pick-repo ch-for-code" title="Repository"><span class="ch-sr">Repository: owner/name or any Git URL</span>${ico('repo')}<input name="repo" type="text" inputmode="url" spellcheck="false" autocapitalize="off" value="${esc(repo)}" placeholder="owner/name or any Git URL" list="ch-repos" autocomplete="off" data-ch-repo data-ch-repo-url="/api/v1/me/repos"><datalist id="ch-repos">${repos.map((x) => `<option value="${esc(x.url)}">${esc(x.full_name)}</option>`).join('')}</datalist></label>`;
const ready = await readyFor(req);
const catalog = await aiModels();
-- const pickers = kindPick + projectPick + repoPick + whoPicker(settings.default_agent ? `${settings.default_agent}|` : '', { cls: 'ch-for-agents', ready, catalog }) + effortPicker('', 'ch-for-agents');
++ // In the bar: the clip and, for your own code, the repository (the one choice that cannot be guessed). Everything
++ // else is behind Options (11-09, the owner: "too many options… hidden under a cog"), and a changed one shows as a chip.
++ const pickers = repoPick;
++ const options = kindPick + projectPick + whoPicker(settings.default_agent ? `${settings.default_agent}|` : '', { cls: 'ch-for-agents', ready, catalog }) + effortPicker('', 'ch-for-agents');
// The repositories a person used last are a tap away (the box also searches every one they can reach).
const newRepo = `<a class="ch-chip ch-chip-new" href="/git">${ico('new')}New on OpenVibe Git</a>`;
const recent = `<div class="ch-recent ch-for-code" role="group" aria-label="Your repositories">${newRepo}${repos.length > 1 ? repos.map((x) => `<button class="ch-chip" type="button" data-ch-saved-repo="${esc(x.url)}">${ico('repo')}${esc(x.full_name)}</button>`).join('') : ''}</div>`;
@@@ -584,10 +592,14 @@ function createChatRoutes(ctx) {
const pct = progress ? Math.round((progress.done / Math.max(1, progress.of)) * 100) : 0;
const nudge = progress ? `<a class="ch-setup-nudge" href="/setup"><span class="ch-setup-ring" style="--p:${pct}" aria-hidden="true"><b>${progress.done}/${progress.of}</b></span><span class="ch-setup-text"><b>Finish setting up Codes</b><small>Next: ${esc(progress.next)}</small></span>${ico('chevron', 'ch-setup-nudge-go')}</a>` : '';
// The greeting is one short line: the composer, the page's one action, stays on the first screen of a phone.
-- const hello = `<div class="ch-hello"><span class="ov-mark" data-size="40" aria-hidden="true"></span><h1>${name ? `What should we build, ${esc(name)}?` : 'What should we build?'}</h1><p>Say it the way you would to a friend.</p>${error ? `<p class="ch-error" role="alert">${esc(error)}</p>` : ''}${modes}</div>`;
++ // The question follows the way you start (11-09, the owner); the mark and the line under it fold away while the
++ // keyboard is up (thread.css, data-ovt-fold), so the box stays in view.
++ const you = name ? `, ${esc(name)}` : '';
++ const ask = `<span class="ch-for-new">What should we make${you}?</span><span class="ch-for-openvibe">What should we improve on OpenVibe${you}?</span><span class="ch-for-code">What should we work on${you}?</span>`;
++ const hello = `<div class="ch-hello"><span class="ov-mark" data-size="40" aria-hidden="true" data-ovt-fold></span><h1>${ask}</h1><p data-ovt-fold>Say it the way you would to a friend.</p>${error ? `<p class="ch-error" role="alert">${esc(error)}</p>` : ''}${modes}</div>`;
return frame(req, res, {
title: 'New chat', heading: 'New chat', start: true, empty: hello,
-- composerHtml: composer({ action: '/chat', id: 'ch-start', placeholder: mode === 'new' ? 'Describe your website, game or app' : 'What should change?', pickers, where, files: true }) + nudge + ideas,
++ composerHtml: composer({ action: '/chat', id: 'ch-start', placeholder: mode === 'new' ? 'Describe your website, game or app' : 'What should change?', pickers, options, examples: (sets[mode] || []).slice(0, 12).map((i) => i.text), where, files: true }) + nudge + ideas,
});
});
ddiff --git a/test/c4.test.js b/test/c4.test.js
iindex 6b92ec2..ae9309b 100644
---- a/test/c4.test.js
++++ b/test/c4.test.js
@@@ -58,7 +58,7 @@ const { boot, check, done } = require('./helpers/boot');
const home = await t.get('/', { as: ada });
assert.ok(!home.text.includes('Built for the way you work'));
assert.ok(!home.text.includes('Recently shipped on'));
-- assert.ok(home.text.includes('What should we build') && home.text.includes('owner/name or any Git URL') && home.text.includes('name="mode"'), 'the greeting, the three ways to start and the repository');
++ assert.ok(home.text.includes('What should we make') && home.text.includes('owner/name or any Git URL') && home.text.includes('name="mode"'), 'the greeting, the three ways to start and the repository');
assert.ok(home.text.includes('action="/chat"') && home.text.includes('data-ovt-composer'), 'one composer that starts the conversation');
const id = 'tsk_01ARZ3NDEKTSV4RRFFQ69G5FAV';
await t.ctx.store.db.exec(`INSERT INTO run_tasks (id, subject, project_id, repo, ref, prompt, agent, state, result, error, created_at, updated_at)
ddiff --git a/test/c4c.test.js b/test/c4c.test.js
iindex 02933d9..eb17941 100644
---- a/test/c4c.test.js
++++ b/test/c4c.test.js
@@@ -86,7 +86,7 @@ const split = (links) => ({
await check('a signed-in person&rsquo;s home is a new chat, not a page of cards (plan L31)', async () => {
const page = await t.get('/', { as: ada });
assert.strictEqual(page.status, 200);
-- assert.ok(page.text.includes('What should we build') && page.text.includes('action="/chat"'), 'the greeting and the composer');
++ assert.ok(page.text.includes('What should we make') && page.text.includes('action="/chat"'), 'the greeting and the composer');
assert.ok(!page.text.includes('class="c4-card'), 'no cards');
assert.ok(page.text.includes('aria-label="Your conversations"'), 'the rail of conversations is the navigation');
});
ddiff --git a/test/home.test.js b/test/home.test.js
iindex 13b9a85..85ca9b6 100644
---- a/test/home.test.js
++++ b/test/home.test.js
@@@ -50,7 +50,7 @@ const { boot, check, done } = require('./helpers/boot');
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.match(r.text, /What should we make/);
assert.ok(!r.text.includes('class="c4-demo"'), 'no front door once signed in');
});
} finally { await t.close(); }
ddiff --git a/test/start-page.test.js b/test/start-page.test.js
iindex ba77b08..e46bcf2 100644
---- a/test/start-page.test.js
++++ b/test/start-page.test.js
@@@ -29,7 +29,7 @@ const { version } = require('../package.json');
assert.ok(r.text.includes('href="/run"'));
assert.ok(r.text.includes('What should an agent do?'));
const me = await t.get('/', { as: t.network.addUser('ada') });
-- assert.match(me.text, /What should we build/);
++ assert.match(me.text, /What should we make/);
});
await check('the command line prints this version and points at the guide', () => {