openvibers/openvibe.codes Commits
Setup on a phone: nothing wider than the screen, commands wrap with Copy (#272)
Changes
@@@ -1,13 +1,12 @@/* Codes' guided setup (/setup, 2026-11-03): the progress, the step chips, and each step's card. Loaded on /setup only. */--.c4-setup{display:grid;gap:0}--.c4-setup-head{display:grid;gap:12px;margin:0 0 16px}++.c4-setup{display:grid;grid-template-columns:minmax(0,1fr);gap:0;min-width:0}++.c4-setup-head{display:grid;grid-template-columns:minmax(0,1fr);gap:12px;margin:0 0 16px;min-width:0}.c4-setup-head .c4-title{margin:0}.c4-setup-progress{display:flex;align-items:center;gap:12px}.c4-setup-bar{flex:1;max-width:420px;height:8px;border-radius:99px;background:var(--c4-quiet-bg);overflow:hidden}.c4-setup-bar>span{display:block;height:100%;border-radius:99px;background:var(--c4-accent);transition:width .4s cubic-bezier(.2,.8,.2,1)}.c4-setup-count{color:var(--c4-muted);font-size:13px;font-weight:600;white-space:nowrap}--.c4-setup-nav{display:flex;gap:8px;margin:0;padding:2px 0 4px;list-style:none;overflow-x:auto;scrollbar-width:none}--.c4-setup-nav::-webkit-scrollbar{display:none}++.c4-setup-nav{display:flex;flex-wrap:wrap;gap:8px;margin:0;padding:2px 0 4px;list-style:none}.c4-setup-chip{display:inline-flex;align-items:center;gap:8px;min-height:40px;padding:0 14px 0 6px;border:1px solid var(--c4-line);border-radius:999px;background:var(--c4-surface);color:var(--c4-ink);font-size:13px;font-weight:600;text-decoration:none;white-space:nowrap}.c4-setup-chip-n{display:grid;place-items:center;width:28px;height:28px;border-radius:50%;background:var(--c4-quiet-bg);color:var(--c4-muted);font-size:12px;font-weight:700}.c4-setup-chip-n svg,.c4-setup-n svg{width:15px;height:15px;fill:none;stroke:currentColor;stroke-width:2.6;stroke-linecap:round;stroke-linejoin:round}@@@ -22,14 +21,19 @@.c4-setup-step.is-done .c4-setup-n{background:var(--c4-ok-bg);color:var(--c4-ok-ink)}.c4-setup-step-title h2{margin:0;font-size:17px;letter-spacing:-.01em}.c4-setup-step-title p{margin:4px 0 0;color:var(--c4-muted);font-size:13px;line-height:1.45}--.c4-setup-body{margin:14px 0 0 54px;display:grid;gap:12px;min-width:0}++.c4-setup-body{margin:14px 0 0 54px;display:grid;grid-template-columns:minmax(0,1fr);gap:12px;min-width:0}.c4-setup-body .c4-hint{margin:0}.c4-setup-actions{display:flex;flex-wrap:wrap;gap:8px;align-items:center}--.c4-setup-how{margin:0;padding-left:20px;display:grid;gap:10px;font-size:13.5px;line-height:1.5}++.c4-setup-how{margin:0;padding-left:20px;display:grid;grid-template-columns:minmax(0,1fr);gap:10px;font-size:13.5px;line-height:1.5}++.c4-setup-how>li{min-width:0}.c4-setup-how a{color:var(--c4-accent)}.c4-setup-cmd{display:grid;gap:4px;margin-top:6px;min-width:0}.c4-setup-cmd-label{font-size:11px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:var(--c4-muted)}--.c4-setup-cmd code{display:block;padding:10px 12px;border-radius:9px;background:var(--c4-ghost);border:1px solid var(--c4-line);font:12.5px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--c4-ink);overflow-x:auto;white-space:pre;user-select:all;-webkit-user-select:all}++.c4-setup-cmd-box{display:flex;align-items:flex-start;gap:8px;min-width:0;padding:8px 8px 8px 12px;border-radius:10px;background:var(--c4-ghost);border:1px solid var(--c4-line)}++.c4-setup-cmd code{flex:1;min-width:0;padding:4px 0;font:12.5px/1.55 ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--c4-ink);white-space:pre-wrap;overflow-wrap:anywhere;user-select:all;-webkit-user-select:all}++.c4-setup-copy{flex:none;min-height:32px;padding:0 12px;border:1px solid var(--c4-line);border-radius:8px;background:var(--c4-surface);color:var(--c4-ink);font:inherit;font-size:12px;font-weight:650;cursor:pointer}++.c4-setup-copy:hover{border-color:var(--c4-accent);color:var(--c4-accent)}++.c4-setup-copy:focus-visible{outline:2px solid var(--c4-accent);outline-offset:2px}.c4-setup-cmd code:focus-visible{outline:2px solid var(--c4-accent);outline-offset:2px}.c4-setup-agent{scroll-margin-top:84px}.c4-setup-agent:target{background:var(--c4-open-bg);border-radius:10px}@@@ -40,10 +44,22 @@.c4-setup-ready p{margin:0 0 12px;color:var(--c4-muted)}.c4-setup-link{margin:0 0 14px}@media (max-width:640px){-- .c4-setup-step-head{grid-template-columns:34px minmax(0,1fr);gap:12px}-- .c4-setup-step-head>.c4-pill{grid-column:2;justify-self:start}++ .c4-setup .c4-card{padding:16px}++ .c4-setup-head{gap:10px}++ .c4-setup-head .c4-title h1{font-size:26px}++ .c4-setup-progress{gap:10px}++ .c4-setup-bar{max-width:none}++ /* Every step's chip on the screen at once: they wrap, none is cut at the edge. */++ .c4-setup-nav{gap:6px}++ .c4-setup-chip{min-height:36px;padding:0 12px 0 4px;font-size:12.5px}++ .c4-setup-chip-n{width:26px;height:26px}++ .c4-setup-step-head{grid-template-columns:34px minmax(0,1fr) auto;gap:10px}++ .c4-setup-step-title h2{font-size:16px}++ .c4-setup-step-title p{font-size:12.5px}.c4-setup-n{width:34px;height:34px;font-size:14px}-- .c4-setup-body{margin-left:0}++ .c4-setup-body{margin:12px 0 0;gap:10px}++ .c4-setup-how{padding-left:18px}.c4-setup-agent-help{padding-left:0}++ .c4-setup-actions>.c4-button,.c4-setup-actions>.c4-opill{flex:1 1 auto;justify-content:center}}@media (prefers-reduced-motion:reduce){.c4-setup-bar>span{transition:none}}nnew file mode 100644@@@ -0,0 +1,16 @@++// /setup's own script (loaded there only). Copy buttons (the guided setup's commands): the code beside the button goes to the clipboard, and the button says so.++// Where the clipboard is refused, the command is selected instead, for the person to copy.++(function () {++ 'use strict';++ document.addEventListener('click', function (e) {++ var btn = e.target && e.target.closest ? e.target.closest('[data-c4-copy]') : null;++ if (!btn) return;++ var code = btn.parentNode && btn.parentNode.querySelector('code');++ if (!code) return;++ var text = code.textContent || '';++ var said = function (word) { btn.textContent = word; window.setTimeout(function () { btn.textContent = 'Copy'; }, 1600); };++ var select = function () { var r = document.createRange(); r.selectNodeContents(code); var sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(r); said('Selected'); };++ if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(text).then(function () { said('Copied'); }, select);++ else select();++ });++})();@@@ -93,12 +93,13 @@ function createC4Routes(ctx) {const page = (req, res, title, body, extra = {}) => {// A page may add its own stylesheet (the guided setup's), so c4.css stays the size every page pays for.const css = ['css/c4.css', ...(Array.isArray(extra.css) ? extra.css : [])];++ const own = Array.isArray(extra.scripts) ? extra.scripts : [];if (viewer(req) && ctx.chatShell && !extra.index) {const here = '/' + (String(req.path || '/').split('/')[1] || '');-- return ctx.chatShell(req, res, { title: title || 'Codes', body: String(body), css, scripts: ['js/c4.js'], here });++ return ctx.chatShell(req, res, { title: title || 'Codes', body: String(body), css, scripts: ['js/c4.js', ...own], here });}const wantsScript = req.path === '/' || /^\/tasks\/tsk_/.test(req.path) || /^\/(agents|machines|conductor)/.test(req.path);-- return send(res, 200, { viewer: req.viewer, config, path: req.originalUrl, ...(title ? { title: `${title} · Codes` } : {}), scripts: wantsScript ? ['js/c4.js'] : [], body: raw(body), ...extra, css });++ return send(res, 200, { viewer: req.viewer, config, path: req.originalUrl, ...(title ? { title: `${title} · Codes` } : {}), body: raw(body), ...extra, css, scripts: [...(wantsScript ? ['js/c4.js'] : []), ...own] });};const viewer = (req) => req.viewer && req.viewer.kind === 'user' && req.viewer.subject;/**@@@ -847,7 +848,7 @@ ${conductorSection}// The guided setup (/setup): machine, agents, keys, accounts and choices, each step read live. Its home-page card// reads the database only, through ctx.setupProgress (the chat app's home asks it).const setupRoutes = require('./setup').createSetupRoutes(ctx, {-- page: (req, res, title, body) => page(req, res, title, body, { css: ['css/setup.css'] }),++ page: (req, res, title, body) => page(req, res, title, body, { css: ['css/setup.css'], scripts: ['js/setup.js'] }),wrap, header, pill, flash, sameOrigin, viewer, form, signIn, note, machinesOf, usageMeter, SAFETY_OPTIONS, YOUR_AI, PROVIDER_NAMES,});r.use(setupRoutes);@@@ -33,7 +33,8 @@ function createSetupRoutes(ctx, kit) {const db = store.db;const r = asyncRouter();-- const command = (text, label) => `<div class="c4-setup-cmd"><span class="c4-setup-cmd-label">${esc(label)}</span><code tabindex="0">${esc(text)}</code></div>`;++ // A command to type on the machine: it wraps on a phone (never a page wider than the screen), and Copy copies it.++ const command = (text, label) => `<div class="c4-setup-cmd"><span class="c4-setup-cmd-label">${esc(label)}</span><div class="c4-setup-cmd-box"><code tabindex="0">${esc(text)}</code><button type="button" class="c4-setup-copy" data-c4-copy>Copy</button></div></div>`;/** Everything the page says, read once per view. */async function readAll(req, res) {@@@ -38,6 +38,13 @@ const DEVICE = 'dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2S1';assert.match(res.text, /curl -fsSL https:\/\/openvibe\.bot\/install \| sh -s -- --worker/);assert.match(res.text, /npm install -g @openai\/codex/, 'an agent that is not there says how to install it');assert.match(res.text, /href="\/css\/setup\.css|css\/setup\.css/, 'the page brings its own stylesheet');++ assert.match(res.text, /js\/setup\.js/, 'and its own script (the Copy buttons), which no other page pays for');++ assert.match(res.text, /<code tabindex="0">curl -fsSL[^<]*<\/code><button type="button" class="c4-setup-copy" data-c4-copy>Copy<\/button>/, 'a command has its Copy button');++ // A phone never gets a page wider than its screen (2026-11-03: the installer's one long line widened the++ // whole grid to 595 px at 390): the page's columns may shrink, and a command wraps.++ const css = require('fs').readFileSync(require('path').join(__dirname, '..', 'public', 'css', 'setup.css'), 'utf8');++ for (const rule of ['.c4-setup{display:grid;grid-template-columns:minmax(0,1fr)', '.c4-setup-head{display:grid;grid-template-columns:minmax(0,1fr)', '.c4-setup-body{margin:14px 0 0 54px;display:grid;grid-template-columns:minmax(0,1fr)']) assert.ok(css.includes(rule), rule);++ assert.match(css, /\.c4-setup-cmd code\{[^}]*white-space:pre-wrap;overflow-wrap:anywhere/);const home = await t.get('/', { as: ada });assert.match(home.text, /class="ch-setup-nudge" href="\/setup"/);assert.match(home.text, /0 of 3 done · next: Your machine/);
@@ -1,13 +1,12 @@
@@ -1,13 +1,12 @@
/* Codes' guided setup (/setup, 2026-11-03): the progress, the step chips, and each step's card. Loaded on /setup only. */
/* Codes' guided setup (/setup, 2026-11-03): the progress, the step chips, and each step's card. Loaded on /setup only. */
-.c4-setup{display:grid;gap:0}
-.c4-setup-head{display:grid;gap:12px;margin:0 0 16px}
+.c4-setup{display:grid;grid-template-columns:minmax(0,1fr);gap:0;min-width:0}
+.c4-setup-head{display:grid;grid-template-columns:minmax(0,1fr);gap:12px;margin:0 0 16px;min-width:0}
.c4-setup-head .c4-title{margin:0}
.c4-setup-head .c4-title{margin:0}
.c4-setup-progress{display:flex;align-items:center;gap:12px}
.c4-setup-progress{display:flex;align-items:center;gap:12px}
.c4-setup-bar{flex:1;max-width:420px;height:8px;border-radius:99px;background:var(--c4-quiet-bg);overflow:hidden}
.c4-setup-bar{flex:1;max-width:420px;height:8px;border-radius:99px;background:var(--c4-quiet-bg);overflow:hidden}
.c4-setup-bar>span{display:block;height:100%;border-radius:99px;background:var(--c4-accent);transition:width .4s cubic-bezier(.2,.8,.2,1)}
.c4-setup-bar>span{display:block;height:100%;border-radius:99px;background:var(--c4-accent);transition:width .4s cubic-bezier(.2,.8,.2,1)}
.c4-setup-count{color:var(--c4-muted);font-size:13px;font-weight:600;white-space:nowrap}
.c4-setup-count{color:var(--c4-muted);font-size:13px;font-weight:600;white-space:nowrap}
-.c4-setup-nav{display:flex;gap:8px;margin:0;padding:2px 0 4px;list-style:none;overflow-x:auto;scrollbar-width:none}
-.c4-setup-nav::-webkit-scrollbar{display:none}
+.c4-setup-nav{display:flex;flex-wrap:wrap;gap:8px;margin:0;padding:2px 0 4px;list-style:none}
.c4-setup-chip{display:inline-flex;align-items:center;gap:8px;min-height:40px;padding:0 14px 0 6px;border:1px solid var(--c4-line);border-radius:999px;background:var(--c4-surface);color:var(--c4-ink);font-size:13px;font-weight:600;text-decoration:none;white-space:nowrap}
.c4-setup-chip{display:inline-flex;align-items:center;gap:8px;min-height:40px;padding:0 14px 0 6px;border:1px solid var(--c4-line);border-radius:999px;background:var(--c4-surface);color:var(--c4-ink);font-size:13px;font-weight:600;text-decoration:none;white-space:nowrap}
.c4-setup-chip-n{display:grid;place-items:center;width:28px;height:28px;border-radius:50%;background:var(--c4-quiet-bg);color:var(--c4-muted);font-size:12px;font-weight:700}
.c4-setup-chip-n{display:grid;place-items:center;width:28px;height:28px;border-radius:50%;background:var(--c4-quiet-bg);color:var(--c4-muted);font-size:12px;font-weight:700}
.c4-setup-chip-n svg,.c4-setup-n svg{width:15px;height:15px;fill:none;stroke:currentColor;stroke-width:2.6;stroke-linecap:round;stroke-linejoin:round}
.c4-setup-chip-n svg,.c4-setup-n svg{width:15px;height:15px;fill:none;stroke:currentColor;stroke-width:2.6;stroke-linecap:round;stroke-linejoin:round}
@@ -22,14 +21,19 @@
@@ -22,14 +21,19 @@
.c4-setup-step.is-done .c4-setup-n{background:var(--c4-ok-bg);color:var(--c4-ok-ink)}
.c4-setup-step.is-done .c4-setup-n{background:var(--c4-ok-bg);color:var(--c4-ok-ink)}
.c4-setup-step-title h2{margin:0;font-size:17px;letter-spacing:-.01em}
.c4-setup-step-title h2{margin:0;font-size:17px;letter-spacing:-.01em}
.c4-setup-step-title p{margin:4px 0 0;color:var(--c4-muted);font-size:13px;line-height:1.45}
.c4-setup-step-title p{margin:4px 0 0;color:var(--c4-muted);font-size:13px;line-height:1.45}
-.c4-setup-body{margin:14px 0 0 54px;display:grid;gap:12px;min-width:0}
+.c4-setup-body{margin:14px 0 0 54px;display:grid;grid-template-columns:minmax(0,1fr);gap:12px;min-width:0}
.c4-setup-body .c4-hint{margin:0}
.c4-setup-body .c4-hint{margin:0}
.c4-setup-actions{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
.c4-setup-actions{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
-.c4-setup-how{margin:0;padding-left:20px;display:grid;gap:10px;font-size:13.5px;line-height:1.5}
+.c4-setup-how{margin:0;padding-left:20px;display:grid;grid-template-columns:minmax(0,1fr);gap:10px;font-size:13.5px;line-height:1.5}
+.c4-setup-how>li{min-width:0}
.c4-setup-how a{color:var(--c4-accent)}
.c4-setup-how a{color:var(--c4-accent)}
.c4-setup-cmd{display:grid;gap:4px;margin-top:6px;min-width:0}
.c4-setup-cmd{display:grid;gap:4px;margin-top:6px;min-width:0}
.c4-setup-cmd-label{font-size:11px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:var(--c4-muted)}
.c4-setup-cmd-label{font-size:11px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:var(--c4-muted)}
-.c4-setup-cmd code{display:block;padding:10px 12px;border-radius:9px;background:var(--c4-ghost);border:1px solid var(--c4-line);font:12.5px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--c4-ink);overflow-x:auto;white-space:pre;user-select:all;-webkit-user-select:all}
+.c4-setup-cmd-box{display:flex;align-items:flex-start;gap:8px;min-width:0;padding:8px 8px 8px 12px;border-radius:10px;background:var(--c4-ghost);border:1px solid var(--c4-line)}
+.c4-setup-cmd code{flex:1;min-width:0;padding:4px 0;font:12.5px/1.55 ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--c4-ink);white-space:pre-wrap;overflow-wrap:anywhere;user-select:all;-webkit-user-select:all}
+.c4-setup-copy{flex:none;min-height:32px;padding:0 12px;border:1px solid var(--c4-line);border-radius:8px;background:var(--c4-surface);color:var(--c4-ink);font:inherit;font-size:12px;font-weight:650;cursor:pointer}
+.c4-setup-copy:hover{border-color:var(--c4-accent);color:var(--c4-accent)}
+.c4-setup-copy:focus-visible{outline:2px solid var(--c4-accent);outline-offset:2px}
.c4-setup-cmd code:focus-visible{outline:2px solid var(--c4-accent);outline-offset:2px}
.c4-setup-cmd code:focus-visible{outline:2px solid var(--c4-accent);outline-offset:2px}
.c4-setup-agent{scroll-margin-top:84px}
.c4-setup-agent{scroll-margin-top:84px}
.c4-setup-agent:target{background:var(--c4-open-bg);border-radius:10px}
.c4-setup-agent:target{background:var(--c4-open-bg);border-radius:10px}
@@ -40,10 +44,22 @@
@@ -40,10 +44,22 @@
.c4-setup-ready p{margin:0 0 12px;color:var(--c4-muted)}
.c4-setup-ready p{margin:0 0 12px;color:var(--c4-muted)}
.c4-setup-link{margin:0 0 14px}
.c4-setup-link{margin:0 0 14px}
@media (max-width:640px){
@media (max-width:640px){
- .c4-setup-step-head{grid-template-columns:34px minmax(0,1fr);gap:12px}
- .c4-setup-step-head>.c4-pill{grid-column:2;justify-self:start}
+ .c4-setup .c4-card{padding:16px}
+ .c4-setup-head{gap:10px}
+ .c4-setup-head .c4-title h1{font-size:26px}
+ .c4-setup-progress{gap:10px}
+ .c4-setup-bar{max-width:none}
+ /* Every step's chip on the screen at once: they wrap, none is cut at the edge. */
+ .c4-setup-nav{gap:6px}
+ .c4-setup-chip{min-height:36px;padding:0 12px 0 4px;font-size:12.5px}
+ .c4-setup-chip-n{width:26px;height:26px}
+ .c4-setup-step-head{grid-template-columns:34px minmax(0,1fr) auto;gap:10px}
+ .c4-setup-step-title h2{font-size:16px}
+ .c4-setup-step-title p{font-size:12.5px}
.c4-setup-n{width:34px;height:34px;font-size:14px}
.c4-setup-n{width:34px;height:34px;font-size:14px}
- .c4-setup-body{margin-left:0}
+ .c4-setup-body{margin:12px 0 0;gap:10px}
+ .c4-setup-how{padding-left:18px}
.c4-setup-agent-help{padding-left:0}
.c4-setup-agent-help{padding-left:0}
+ .c4-setup-actions>.c4-button,.c4-setup-actions>.c4-opill{flex:1 1 auto;justify-content:center}
}
}
@media (prefers-reduced-motion:reduce){.c4-setup-bar>span{transition:none}}
@media (prefers-reduced-motion:reduce){.c4-setup-bar>span{transition:none}}
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+// /setup's own script (loaded there only). Copy buttons (the guided setup's commands): the code beside the button goes to the clipboard, and the button says so.
+// Where the clipboard is refused, the command is selected instead, for the person to copy.
+(function () {
+ 'use strict';
+ document.addEventListener('click', function (e) {
+ var btn = e.target && e.target.closest ? e.target.closest('[data-c4-copy]') : null;
+ if (!btn) return;
+ var code = btn.parentNode && btn.parentNode.querySelector('code');
+ if (!code) return;
+ var text = code.textContent || '';
+ var said = function (word) { btn.textContent = word; window.setTimeout(function () { btn.textContent = 'Copy'; }, 1600); };
+ var select = function () { var r = document.createRange(); r.selectNodeContents(code); var sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(r); said('Selected'); };
+ if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(text).then(function () { said('Copied'); }, select);
+ else select();
+ });
+})();
@@ -93,12 +93,13 @@ function createC4Routes(ctx) {
@@ -93,12 +93,13 @@ function createC4Routes(ctx) {
const page = (req, res, title, body, extra = {}) => {
const page = (req, res, title, body, extra = {}) => {
// A page may add its own stylesheet (the guided setup's), so c4.css stays the size every page pays for.
// A page may add its own stylesheet (the guided setup's), so c4.css stays the size every page pays for.
const css = ['css/c4.css', ...(Array.isArray(extra.css) ? extra.css : [])];
const css = ['css/c4.css', ...(Array.isArray(extra.css) ? extra.css : [])];
+ const own = Array.isArray(extra.scripts) ? extra.scripts : [];
if (viewer(req) && ctx.chatShell && !extra.index) {
if (viewer(req) && ctx.chatShell && !extra.index) {
const here = '/' + (String(req.path || '/').split('/')[1] || '');
const here = '/' + (String(req.path || '/').split('/')[1] || '');
- return ctx.chatShell(req, res, { title: title || 'Codes', body: String(body), css, scripts: ['js/c4.js'], here });
+ return ctx.chatShell(req, res, { title: title || 'Codes', body: String(body), css, scripts: ['js/c4.js', ...own], here });
}
}
const wantsScript = req.path === '/' || /^\/tasks\/tsk_/.test(req.path) || /^\/(agents|machines|conductor)/.test(req.path);
const wantsScript = req.path === '/' || /^\/tasks\/tsk_/.test(req.path) || /^\/(agents|machines|conductor)/.test(req.path);
- return send(res, 200, { viewer: req.viewer, config, path: req.originalUrl, ...(title ? { title: `${title} · Codes` } : {}), scripts: wantsScript ? ['js/c4.js'] : [], body: raw(body), ...extra, css });
+ return send(res, 200, { viewer: req.viewer, config, path: req.originalUrl, ...(title ? { title: `${title} · Codes` } : {}), body: raw(body), ...extra, css, scripts: [...(wantsScript ? ['js/c4.js'] : []), ...own] });
};
};
const viewer = (req) => req.viewer && req.viewer.kind === 'user' && req.viewer.subject;
const viewer = (req) => req.viewer && req.viewer.kind === 'user' && req.viewer.subject;
/**
/**
@@ -847,7 +848,7 @@ ${conductorSection}
@@ -847,7 +848,7 @@ ${conductorSection}
// The guided setup (/setup): machine, agents, keys, accounts and choices, each step read live. Its home-page card
// The guided setup (/setup): machine, agents, keys, accounts and choices, each step read live. Its home-page card
// reads the database only, through ctx.setupProgress (the chat app's home asks it).
// reads the database only, through ctx.setupProgress (the chat app's home asks it).
const setupRoutes = require('./setup').createSetupRoutes(ctx, {
const setupRoutes = require('./setup').createSetupRoutes(ctx, {
- page: (req, res, title, body) => page(req, res, title, body, { css: ['css/setup.css'] }),
+ page: (req, res, title, body) => page(req, res, title, body, { css: ['css/setup.css'], scripts: ['js/setup.js'] }),
wrap, header, pill, flash, sameOrigin, viewer, form, signIn, note, machinesOf, usageMeter, SAFETY_OPTIONS, YOUR_AI, PROVIDER_NAMES,
wrap, header, pill, flash, sameOrigin, viewer, form, signIn, note, machinesOf, usageMeter, SAFETY_OPTIONS, YOUR_AI, PROVIDER_NAMES,
});
});
r.use(setupRoutes);
r.use(setupRoutes);
@@ -33,7 +33,8 @@ function createSetupRoutes(ctx, kit) {
@@ -33,7 +33,8 @@ function createSetupRoutes(ctx, kit) {
const db = store.db;
const db = store.db;
const r = asyncRouter();
const r = asyncRouter();
- const command = (text, label) => `<div class="c4-setup-cmd"><span class="c4-setup-cmd-label">${esc(label)}</span><code tabindex="0">${esc(text)}</code></div>`;
+ // A command to type on the machine: it wraps on a phone (never a page wider than the screen), and Copy copies it.
+ const command = (text, label) => `<div class="c4-setup-cmd"><span class="c4-setup-cmd-label">${esc(label)}</span><div class="c4-setup-cmd-box"><code tabindex="0">${esc(text)}</code><button type="button" class="c4-setup-copy" data-c4-copy>Copy</button></div></div>`;
/** Everything the page says, read once per view. */
/** Everything the page says, read once per view. */
async function readAll(req, res) {
async function readAll(req, res) {
@@ -38,6 +38,13 @@ const DEVICE = 'dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2S1';
@@ -38,6 +38,13 @@ const DEVICE = 'dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2S1';
assert.match(res.text, /curl -fsSL https:\/\/openvibe\.bot\/install \| sh -s -- --worker/);
assert.match(res.text, /curl -fsSL https:\/\/openvibe\.bot\/install \| sh -s -- --worker/);
assert.match(res.text, /npm install -g @openai\/codex/, 'an agent that is not there says how to install it');
assert.match(res.text, /npm install -g @openai\/codex/, 'an agent that is not there says how to install it');
assert.match(res.text, /href="\/css\/setup\.css|css\/setup\.css/, 'the page brings its own stylesheet');
assert.match(res.text, /href="\/css\/setup\.css|css\/setup\.css/, 'the page brings its own stylesheet');
+ assert.match(res.text, /js\/setup\.js/, 'and its own script (the Copy buttons), which no other page pays for');
+ assert.match(res.text, /<code tabindex="0">curl -fsSL[^<]*<\/code><button type="button" class="c4-setup-copy" data-c4-copy>Copy<\/button>/, 'a command has its Copy button');
+ // A phone never gets a page wider than its screen (2026-11-03: the installer's one long line widened the
+ // whole grid to 595 px at 390): the page's columns may shrink, and a command wraps.
+ const css = require('fs').readFileSync(require('path').join(__dirname, '..', 'public', 'css', 'setup.css'), 'utf8');
+ for (const rule of ['.c4-setup{display:grid;grid-template-columns:minmax(0,1fr)', '.c4-setup-head{display:grid;grid-template-columns:minmax(0,1fr)', '.c4-setup-body{margin:14px 0 0 54px;display:grid;grid-template-columns:minmax(0,1fr)']) assert.ok(css.includes(rule), rule);
+ assert.match(css, /\.c4-setup-cmd code\{[^}]*white-space:pre-wrap;overflow-wrap:anywhere/);
const home = await t.get('/', { as: ada });
const home = await t.get('/', { as: ada });
assert.match(home.text, /class="ch-setup-nudge" href="\/setup"/);
assert.match(home.text, /class="ch-setup-nudge" href="\/setup"/);
assert.match(home.text, /0 of 3 done · next: Your machine/);
assert.match(home.text, /0 of 3 done · next: Your machine/);