openvibers/openvibe.codes Commits
The start page: Create, OpenVibe, My code; options behind one button; no keyboard by surprise (#333)
Changes
@@@ -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}}@@@ -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 × ──@@@ -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,});});@@@ -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)@@@ -86,7 +86,7 @@ const split = (links) => ({await check('a signed-in person’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');});@@@ -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(); }@@@ -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', () => {
@@ -201,3 +201,27 @@ html[data-ov-motion=reduced] .ch-spark-list[aria-hidden],html[data-ov-motion=red
@@ -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 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-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)}
.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}}
@@ -58,12 +58,18 @@
@@ -58,12 +58,18 @@
lists.forEach((ul, n) => { ul.textContent = ''; sets[mode].forEach((i) => ul.appendChild(card(i, n > 0))); });
lists.forEach((ul, n) => { ul.textContent = ''; sets[mode].forEach((i) => ul.appendChild(card(i, n > 0))); });
pace();
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 } = {}) {
function choose(mode, { focus = false } = {}) {
const radio = modes.find((r) => r.value === mode);
const radio = modes.find((r) => r.value === mode);
if (radio && !radio.checked) radio.checked = true;
if (radio && !radio.checked) radio.checked = true;
if (text && PLACEHOLDER[mode]) text.placeholder = PLACEHOLDER[mode];
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 (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(); } }));
modes.forEach((r) => r.addEventListener('change', () => { if (r.checked) { choose(r.value, { focus: true }); pick(); } }));
@@ -110,9 +116,9 @@
@@ -110,9 +116,9 @@
if (b.dataset.chIdeaKind) setKind(b.dataset.chIdeaKind);
if (b.dataset.chIdeaKind) setKind(b.dataset.chIdeaKind);
text.value = b.dataset.chIdea;
text.value = b.dataset.chIdea;
grow();
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);
const pause = $('[data-ch-ideas-pause]', rail);
if (pause) pause.addEventListener('click', () => {
if (pause) pause.addEventListener('click', () => {
@@ -139,7 +145,7 @@
@@ -139,7 +145,7 @@
startPage(app, form, text);
startPage(app, form, text);
// The repository: a recent one is a tap away, and typing searches every repository the person can reach.
// The repository: a recent one is a tap away, and typing searches every repository the person can reach.
const repo = $('[data-ch-repo]', form);
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) {
if (repo && repo.dataset.chRepoUrl) {
const list = document.getElementById(repo.getAttribute('list'));
const list = document.getElementById(repo.getAttribute('list'));
let timer = 0, seq = 0;
let timer = 0, seq = 0;
@@ -162,7 +168,7 @@
@@ -162,7 +168,7 @@
}
}
// The keyboard hint only where Enter sends (a phone keyboard's return is a new line).
// 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 (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) {
if (form) {
// ── The composer's files: upload before send, with a chip, a thumbnail, a progress bar and a × ──
// ── The composer's files: upload before send, with a chip, a thumbnail, a progress bar and a × ──
@@ -420,22 +420,25 @@ function createChatRoutes(ctx) {
@@ -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>`;
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]);
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).
// 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 EFFORT = [['', 'Effort: auto'], ['low', 'Quick'], ['medium', 'Balanced'], ['high', 'Thorough'], ['max', 'Deepest']];
const effortWord = (v) => (EFFORT.find(([k]) => k === v) || [null, v])[1];
const effortWord = (v) => (EFFORT.find(([k]) => k === v) || [null, v])[1];
function effortPicker(selected = '', cls = '') {
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. */
/** Any file (not a program) up to 10 at once: the script adds chips and progress, the plain form posts multipart. */
function filePicker() {
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>`;
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. */
/** 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({
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,
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' } : {}) },
attrs: { ...(api ? { 'data-ch-api': api } : {}), ...(files ? { enctype: 'multipart/form-data' } : {}) },
busy: live, stopAction: live && stopAction ? stopAction : null, maxlength: 16000,
busy: live, stopAction: live && stopAction ? stopAction : null, maxlength: 16000,
@@ -546,8 +549,10 @@ function createChatRoutes(ctx) {
@@ -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
// 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.
// under the composer, which changes with the choice.
const MODE_CARDS = [
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'],
['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>`;
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) {
@@ -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
// 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.
// 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 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 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 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 ready = await readyFor(req);
const catalog = await aiModels();
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).
// 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 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>`;
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) {
@@ -584,10 +592,14 @@ function createChatRoutes(ctx) {
const pct = progress ? Math.round((progress.done / Math.max(1, progress.of)) * 100) : 0;
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>` : '';
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.
// 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, {
return frame(req, res, {
title: 'New chat', heading: 'New chat', start: true, empty: hello,
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,
});
});
});
});
@@ -58,7 +58,7 @@ const { boot, check, done } = require('./helpers/boot');
@@ -58,7 +58,7 @@ const { boot, check, done } = require('./helpers/boot');
const home = await t.get('/', { as: ada });
const home = await t.get('/', { as: ada });
assert.ok(!home.text.includes('Built for the way you work'));
assert.ok(!home.text.includes('Built for the way you work'));
assert.ok(!home.text.includes('Recently shipped on'));
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');
assert.ok(home.text.includes('action="/chat"') && home.text.includes('data-ovt-composer'), 'one composer that starts the conversation');
const id = 'tsk_01ARZ3NDEKTSV4RRFFQ69G5FAV';
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)
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)
@@ -86,7 +86,7 @@ const split = (links) => ({
@@ -86,7 +86,7 @@ const split = (links) => ({
await check('a signed-in person’s home is a new chat, not a page of cards (plan L31)', async () => {
await check('a signed-in person’s home is a new chat, not a page of cards (plan L31)', async () => {
const page = await t.get('/', { as: ada });
const page = await t.get('/', { as: ada });
assert.strictEqual(page.status, 200);
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('class="c4-card'), 'no cards');
assert.ok(page.text.includes('aria-label="Your conversations"'), 'the rail of conversations is the navigation');
assert.ok(page.text.includes('aria-label="Your conversations"'), 'the rail of conversations is the navigation');
});
});
@@ -50,7 +50,7 @@ const { boot, check, done } = require('./helpers/boot');
@@ -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 () => {
await check('a signed-in person gets the chat, not the front door', async () => {
const r = await t.get('/', { as: t.network.addUser('rosa') });
const r = await t.get('/', { as: t.network.addUser('rosa') });
assert.strictEqual(r.status, 200);
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');
assert.ok(!r.text.includes('class="c4-demo"'), 'no front door once signed in');
});
});
} finally { await t.close(); }
} finally { await t.close(); }
@@ -29,7 +29,7 @@ const { version } = require('../package.json');
@@ -29,7 +29,7 @@ const { version } = require('../package.json');
assert.ok(r.text.includes('href="/run"'));
assert.ok(r.text.includes('href="/run"'));
assert.ok(r.text.includes('What should an agent do?'));
assert.ok(r.text.includes('What should an agent do?'));
const me = await t.get('/', { as: t.network.addUser('ada') });
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', () => {
await check('the command line prints this version and points at the guide', () => {