openvibers/openvibe.codes Commits
Codes: code blocks are keyboard-focusable (axe scrollable-region-focusable) (#77)
Changes
@@@ -101,7 +101,7 @@ Added test/date.test.js: three cases, all pass.lede: 'One open harness for Claude Code, Codex, OpenCode, Command Code, Aider, the DeepSeek API and models you host yourself: send a coding task to the agent that fits it, hand it over when one gets stuck, and keep the whole run in one place. Then point it at OpenVibe itself.',actions: [{ label: 'Install the harness', href: '/start', primary: true }, { label: 'Run on your machine from the web', href: '/run' }, { label: 'Meet the agents', href: '/harnesses' }],note: `Open source (AGPL-3.0). Version ${VERSION} runs on your machine with the agents and keys you already have, from your terminal or started from this site in any browser; the list below says exactly what works.`,-- aside: { html: `<pre class="codes-hero-code"><code>${showcase.esc(snippet)}</code></pre>` },++ aside: { html: `<pre tabindex="0" class="codes-hero-code"><code>${showcase.esc(snippet)}</code></pre>` },}))}${raw(showcase.features({id: 'agents', title: 'The agents', lede: `${offers.length} harnesses in the catalog, each with the models it runs. Routing picks one by what the task needs first, then by cost.`,@@@ -165,7 +165,7 @@ for await (const event of harness.run({ prompt: 'fix the failing test', cwd: '.'<p><code>openvibe-codes</code> runs a coding task on whichever agent fits it, hands it to the next one when an agent fails, hits its limit or goes quiet, and keeps the whole run. It runs on your machine with the agents and keys you already have: nothing goes through OpenVibe, and it needs no account.</p><h2 id="install">Install</h2><p>Node.js 22 or newer, then:</p>--<pre><code>${INSTALL}</code></pre>++<pre tabindex="0"><code>${INSTALL}</code></pre><p>Or from a clone: <code>git clone ${GITHUB}/OpenVibe.Codes</code>, <code>npm ci</code>, then <code>node bin/openvibe-codes.js</code>. Check what it found with <code>openvibe-codes agents</code>.</p><h2 id="agents">The agents it can use</h2><p>Install any of them; the harness uses what is there and says why the rest are not.</p>@@@ -180,7 +180,7 @@ ${table(['Agent', 'What it needs'], [[html`<a href="/harnesses#openai-compatible">Your own model</a>`, html`<code>OPENVIBE_CODES_BASE_URL</code> (an OpenAI-compatible server: llama.cpp, vLLM, Ollama, LM Studio…), <code>OPENVIBE_CODES_MODEL</code>, and <code>OPENVIBE_CODES_API_KEY</code> if it wants one`],])}<h2 id="use">Use it</h2>--<pre><code>${example}</code></pre>++<pre tabindex="0"><code>${example}</code></pre>${table(['Command', 'What it does'], [[html`<code>agents</code>`, 'Which agents this machine can run, and why not the others.'],[html`<code>route [--task edit] [--need harness:resume]</code>`, 'Which agent a task would go to, with the reason for every candidate.'],@@@ -201,11 +201,11 @@ ${table(['Level', 'What the agent may do'], [<p>When an attempt fails, hits a usage limit, crashes or prints nothing for a while, the harness stops it and gives the task to the next agent the router picks, leaving out the ones already tried. The next agent gets a note: the original task, which agent tried and why it stopped, the tools it used, the files it changed, its last message and <code>git status</code>. Every attempt is in the run.</p><h2 id="events">One event format</h2><p>Every agent's output becomes the same events: one JSON object per line with <code>--json</code>, the same shape as Claude Code's <code>stream-json</code>, ending with exactly one <code>result</code>. Keys and tokens in an agent's output are redacted before an event is printed or kept.</p>--<pre><code>${events}</code></pre>++<pre tabindex="0"><code>${events}</code></pre><h2 id="runs">Where runs are kept</h2><p>In <code>$OPENVIBE_CODES_HOME</code>, else <code>$XDG_STATE_HOME/openvibe-codes</code>, else <code>~/.local/state/openvibe-codes</code>: one directory per run with its attempts and events, readable only by you.</p><h2 id="code">From your own code</h2>--<pre><code>${fromCode}</code></pre>++<pre tabindex="0"><code>${fromCode}</code></pre><p><code>harness.agents()</code>, <code>harness.route({ task })</code> and <code>harness.run({ prompt, cwd, agent, permission, handoff })</code> are what the command line uses. The router is the same one behind <a href="/api/v1/harnesses"><code>/api/v1/harnesses/route</code></a>.</p><h2 id="next">What comes next</h2><p>Tasks from the web already run on your own machine (<a href="/run">Run on your machine</a>). Next: a hosted runner on OpenVibe.Run workers for people who would rather not run agents themselves, with budgets; OpenVibe.Actor using Codes for its coding work; and <a href="/improve">Improve OpenVibe</a> as a guided run: pick a repository, describe the change, get a pull request.</p>`,@@@ -167,10 +167,10 @@ ${tasksHtml(recent)}`;: d.ok ? html`<p class="run-notice run-notice-ok">The agents finished the task${d.agents && d.agents.length ? ` (${d.agents.join(' → ')})` : ''}.</p>`: html`<p class="run-notice run-notice-error">The task ended without success${d.error ? `: ${d.error}` : ''}.</p>`;return html`<section class="run-result">${verdict}--${d.summary ? html`<h2>What the agent said</h2><pre class="run-summary">${d.summary}</pre>` : ''}++${d.summary ? html`<h2>What the agent said</h2><pre tabindex="0" class="run-summary">${d.summary}</pre>` : ''}${push ? (push.pushed ? html`<p>Pushed <a href="${push.branch_url}">${push.branch}</a>${push.pull_request ? html` · <a href="${push.pull_request}">pull request</a>` : ''}${push.pull_request_error ? html` · the pull request was not opened (${push.pull_request_error})` : ''}</p>` : html`<p class="muted">Not pushed: ${push.reason}</p>`) : ''}${files.length ? html`<h2>Changed files</h2><table class="run-files"><tbody>${files.map((f) => html`<tr><td><code>${f.path}</code></td><td class="add">+${f.added == null ? '·' : f.added}</td><td class="del">−${f.removed == null ? '·' : f.removed}</td></tr>`)}</tbody></table>` : (row.state === 'succeeded' ? html`<p class="muted">No files changed.</p>` : '')}--${d.patch ? html`<h2>The patch${d.patch_truncated ? ' (cut)' : ''}</h2><pre class="run-patch" data-patch>${d.patch}</pre>` : ''}++${d.patch ? html`<h2>The patch${d.patch_truncated ? ' (cut)' : ''}</h2><pre tabindex="0" class="run-patch" data-patch>${d.patch}</pre>` : ''}${typeof d.duration_ms === 'number' ? html`<p class="muted">${Math.round(d.duration_ms / 1000)} s on your machine${d.cost_usd ? ` · about $${Number(d.cost_usd).toFixed(4)} of model use on your own keys` : ''}</p>` : ''}</section>`;}@@@ -184,7 +184,7 @@ ${typeof d.duration_ms === 'number' ? html`<p class="muted">${Math.round(d.duratconst body = html`<p class="crumbs"><a href="/run">Run on your machine</a></p><h1 class="run-repo">${repoName(row.repo)}${row.ref ? html` <small>${row.ref}</small>` : ''}</h1><p><span class="run-pill run-pill-${row.state}" data-state>${STATE_LABEL[row.state] || row.state}</span>${row.agent ? html` <span class="run-pill">${row.agent}</span>` : ''}</p>--<pre class="run-prompt">${row.prompt}</pre>++<pre tabindex="0" class="run-prompt">${row.prompt}</pre>${live ? html`<section class="run-live" data-events="/run/tasks/${row.id}/events" aria-live="polite"><h2>Live</h2><ol class="run-log" data-log></ol></section><form method="post" action="/run/tasks/${row.id}/cancel"><button class="run-btn" type="submit">Cancel</button></form>` : resultHtml(row)}`;return page(req, res, { title: 'Task', body });@@@ -30,7 +30,7 @@ function markdown(src, { headingOffset = 1 } = {}) {const flushList = () => { if (list) { out.push(`<${list.type}>${list.items.map((i) => `<li>${inline(i)}</li>`).join('')}</${list.type}>`); list = null; } };for (const line of lines) {if (fence) {-- if (/^```/.test(line)) { out.push(`<pre><code>${esc(fence.join('\n'))}</code></pre>`); fence = null; } else fence.push(line);++ if (/^```/.test(line)) { out.push(`<pre tabindex="0"><code>${esc(fence.join('\n'))}</code></pre>`); fence = null; } else fence.push(line);continue;}if (/^```/.test(line)) { flushPara(); flushList(); fence = []; continue; }@@@ -54,7 +54,7 @@ function markdown(src, { headingOffset = 1 } = {}) {flushList();para.push(line.trim());}-- if (fence) out.push(`<pre><code>${esc(fence.join('\n'))}</code></pre>`);++ if (fence) out.push(`<pre tabindex="0"><code>${esc(fence.join('\n'))}</code></pre>`);flushPara(); flushList();return out.join('\n');}@@@ -40,6 +40,8 @@ const BOOST = /<script src="\/shared\/boost\.js\?v=[0-9a-f]{12}" data-main="#maiassert.strictEqual(r.status, 200);const head = r.text.slice(0, r.text.indexOf('</head>'));assert.strictEqual((r.text.match(/<title>/g) || []).length, 1, 'exactly one <title>');++ // A code block that scrolls sideways on a phone is a region the keyboard must reach (axe scrollable-region-focusable).++ assert.ok(!/<pre(?![^>]*tabindex)[\s>]/.test(r.text), 'every <pre> is focusable');assert.ok(head.includes('<title>OpenVibe.Codes: code with any agent</title>'), 'home title');assert.ok(head.includes('<link rel="canonical" href="https://openvibe.codes/">'), 'canonical');assert.ok(head.includes('<meta name="robots" content="index, follow">'), 'robots');
@@ -101,7 +101,7 @@ Added test/date.test.js: three cases, all pass.
@@ -101,7 +101,7 @@ Added test/date.test.js: three cases, all pass.
lede: 'One open harness for Claude Code, Codex, OpenCode, Command Code, Aider, the DeepSeek API and models you host yourself: send a coding task to the agent that fits it, hand it over when one gets stuck, and keep the whole run in one place. Then point it at OpenVibe itself.',
lede: 'One open harness for Claude Code, Codex, OpenCode, Command Code, Aider, the DeepSeek API and models you host yourself: send a coding task to the agent that fits it, hand it over when one gets stuck, and keep the whole run in one place. Then point it at OpenVibe itself.',
actions: [{ label: 'Install the harness', href: '/start', primary: true }, { label: 'Run on your machine from the web', href: '/run' }, { label: 'Meet the agents', href: '/harnesses' }],
actions: [{ label: 'Install the harness', href: '/start', primary: true }, { label: 'Run on your machine from the web', href: '/run' }, { label: 'Meet the agents', href: '/harnesses' }],
note: `Open source (AGPL-3.0). Version ${VERSION} runs on your machine with the agents and keys you already have, from your terminal or started from this site in any browser; the list below says exactly what works.`,
note: `Open source (AGPL-3.0). Version ${VERSION} runs on your machine with the agents and keys you already have, from your terminal or started from this site in any browser; the list below says exactly what works.`,
- aside: { html: `<pre class="codes-hero-code"><code>${showcase.esc(snippet)}</code></pre>` },
+ aside: { html: `<pre tabindex="0" class="codes-hero-code"><code>${showcase.esc(snippet)}</code></pre>` },
}))}
}))}
${raw(showcase.features({
${raw(showcase.features({
id: 'agents', title: 'The agents', lede: `${offers.length} harnesses in the catalog, each with the models it runs. Routing picks one by what the task needs first, then by cost.`,
id: 'agents', title: 'The agents', lede: `${offers.length} harnesses in the catalog, each with the models it runs. Routing picks one by what the task needs first, then by cost.`,
@@ -165,7 +165,7 @@ for await (const event of harness.run({ prompt: 'fix the failing test', cwd: '.'
@@ -165,7 +165,7 @@ for await (const event of harness.run({ prompt: 'fix the failing test', cwd: '.'
<p><code>openvibe-codes</code> runs a coding task on whichever agent fits it, hands it to the next one when an agent fails, hits its limit or goes quiet, and keeps the whole run. It runs on your machine with the agents and keys you already have: nothing goes through OpenVibe, and it needs no account.</p>
<p><code>openvibe-codes</code> runs a coding task on whichever agent fits it, hands it to the next one when an agent fails, hits its limit or goes quiet, and keeps the whole run. It runs on your machine with the agents and keys you already have: nothing goes through OpenVibe, and it needs no account.</p>
<h2 id="install">Install</h2>
<h2 id="install">Install</h2>
<p>Node.js 22 or newer, then:</p>
<p>Node.js 22 or newer, then:</p>
-<pre><code>${INSTALL}</code></pre>
+<pre tabindex="0"><code>${INSTALL}</code></pre>
<p>Or from a clone: <code>git clone ${GITHUB}/OpenVibe.Codes</code>, <code>npm ci</code>, then <code>node bin/openvibe-codes.js</code>. Check what it found with <code>openvibe-codes agents</code>.</p>
<p>Or from a clone: <code>git clone ${GITHUB}/OpenVibe.Codes</code>, <code>npm ci</code>, then <code>node bin/openvibe-codes.js</code>. Check what it found with <code>openvibe-codes agents</code>.</p>
<h2 id="agents">The agents it can use</h2>
<h2 id="agents">The agents it can use</h2>
<p>Install any of them; the harness uses what is there and says why the rest are not.</p>
<p>Install any of them; the harness uses what is there and says why the rest are not.</p>
@@ -180,7 +180,7 @@ ${table(['Agent', 'What it needs'], [
@@ -180,7 +180,7 @@ ${table(['Agent', 'What it needs'], [
[html`<a href="/harnesses#openai-compatible">Your own model</a>`, html`<code>OPENVIBE_CODES_BASE_URL</code> (an OpenAI-compatible server: llama.cpp, vLLM, Ollama, LM Studio…), <code>OPENVIBE_CODES_MODEL</code>, and <code>OPENVIBE_CODES_API_KEY</code> if it wants one`],
[html`<a href="/harnesses#openai-compatible">Your own model</a>`, html`<code>OPENVIBE_CODES_BASE_URL</code> (an OpenAI-compatible server: llama.cpp, vLLM, Ollama, LM Studio…), <code>OPENVIBE_CODES_MODEL</code>, and <code>OPENVIBE_CODES_API_KEY</code> if it wants one`],
])}
])}
<h2 id="use">Use it</h2>
<h2 id="use">Use it</h2>
-<pre><code>${example}</code></pre>
+<pre tabindex="0"><code>${example}</code></pre>
${table(['Command', 'What it does'], [
${table(['Command', 'What it does'], [
[html`<code>agents</code>`, 'Which agents this machine can run, and why not the others.'],
[html`<code>agents</code>`, 'Which agents this machine can run, and why not the others.'],
[html`<code>route [--task edit] [--need harness:resume]</code>`, 'Which agent a task would go to, with the reason for every candidate.'],
[html`<code>route [--task edit] [--need harness:resume]</code>`, 'Which agent a task would go to, with the reason for every candidate.'],
@@ -201,11 +201,11 @@ ${table(['Level', 'What the agent may do'], [
@@ -201,11 +201,11 @@ ${table(['Level', 'What the agent may do'], [
<p>When an attempt fails, hits a usage limit, crashes or prints nothing for a while, the harness stops it and gives the task to the next agent the router picks, leaving out the ones already tried. The next agent gets a note: the original task, which agent tried and why it stopped, the tools it used, the files it changed, its last message and <code>git status</code>. Every attempt is in the run.</p>
<p>When an attempt fails, hits a usage limit, crashes or prints nothing for a while, the harness stops it and gives the task to the next agent the router picks, leaving out the ones already tried. The next agent gets a note: the original task, which agent tried and why it stopped, the tools it used, the files it changed, its last message and <code>git status</code>. Every attempt is in the run.</p>
<h2 id="events">One event format</h2>
<h2 id="events">One event format</h2>
<p>Every agent's output becomes the same events: one JSON object per line with <code>--json</code>, the same shape as Claude Code's <code>stream-json</code>, ending with exactly one <code>result</code>. Keys and tokens in an agent's output are redacted before an event is printed or kept.</p>
<p>Every agent's output becomes the same events: one JSON object per line with <code>--json</code>, the same shape as Claude Code's <code>stream-json</code>, ending with exactly one <code>result</code>. Keys and tokens in an agent's output are redacted before an event is printed or kept.</p>
-<pre><code>${events}</code></pre>
+<pre tabindex="0"><code>${events}</code></pre>
<h2 id="runs">Where runs are kept</h2>
<h2 id="runs">Where runs are kept</h2>
<p>In <code>$OPENVIBE_CODES_HOME</code>, else <code>$XDG_STATE_HOME/openvibe-codes</code>, else <code>~/.local/state/openvibe-codes</code>: one directory per run with its attempts and events, readable only by you.</p>
<p>In <code>$OPENVIBE_CODES_HOME</code>, else <code>$XDG_STATE_HOME/openvibe-codes</code>, else <code>~/.local/state/openvibe-codes</code>: one directory per run with its attempts and events, readable only by you.</p>
<h2 id="code">From your own code</h2>
<h2 id="code">From your own code</h2>
-<pre><code>${fromCode}</code></pre>
+<pre tabindex="0"><code>${fromCode}</code></pre>
<p><code>harness.agents()</code>, <code>harness.route({ task })</code> and <code>harness.run({ prompt, cwd, agent, permission, handoff })</code> are what the command line uses. The router is the same one behind <a href="/api/v1/harnesses"><code>/api/v1/harnesses/route</code></a>.</p>
<p><code>harness.agents()</code>, <code>harness.route({ task })</code> and <code>harness.run({ prompt, cwd, agent, permission, handoff })</code> are what the command line uses. The router is the same one behind <a href="/api/v1/harnesses"><code>/api/v1/harnesses/route</code></a>.</p>
<h2 id="next">What comes next</h2>
<h2 id="next">What comes next</h2>
<p>Tasks from the web already run on your own machine (<a href="/run">Run on your machine</a>). Next: a hosted runner on OpenVibe.Run workers for people who would rather not run agents themselves, with budgets; OpenVibe.Actor using Codes for its coding work; and <a href="/improve">Improve OpenVibe</a> as a guided run: pick a repository, describe the change, get a pull request.</p>`,
<p>Tasks from the web already run on your own machine (<a href="/run">Run on your machine</a>). Next: a hosted runner on OpenVibe.Run workers for people who would rather not run agents themselves, with budgets; OpenVibe.Actor using Codes for its coding work; and <a href="/improve">Improve OpenVibe</a> as a guided run: pick a repository, describe the change, get a pull request.</p>`,
@@ -167,10 +167,10 @@ ${tasksHtml(recent)}`;
@@ -167,10 +167,10 @@ ${tasksHtml(recent)}`;
: d.ok ? html`<p class="run-notice run-notice-ok">The agents finished the task${d.agents && d.agents.length ? ` (${d.agents.join(' → ')})` : ''}.</p>`
: d.ok ? html`<p class="run-notice run-notice-ok">The agents finished the task${d.agents && d.agents.length ? ` (${d.agents.join(' → ')})` : ''}.</p>`
: html`<p class="run-notice run-notice-error">The task ended without success${d.error ? `: ${d.error}` : ''}.</p>`;
: html`<p class="run-notice run-notice-error">The task ended without success${d.error ? `: ${d.error}` : ''}.</p>`;
return html`<section class="run-result">${verdict}
return html`<section class="run-result">${verdict}
-${d.summary ? html`<h2>What the agent said</h2><pre class="run-summary">${d.summary}</pre>` : ''}
+${d.summary ? html`<h2>What the agent said</h2><pre tabindex="0" class="run-summary">${d.summary}</pre>` : ''}
${push ? (push.pushed ? html`<p>Pushed <a href="${push.branch_url}">${push.branch}</a>${push.pull_request ? html` · <a href="${push.pull_request}">pull request</a>` : ''}${push.pull_request_error ? html` · the pull request was not opened (${push.pull_request_error})` : ''}</p>` : html`<p class="muted">Not pushed: ${push.reason}</p>`) : ''}
${push ? (push.pushed ? html`<p>Pushed <a href="${push.branch_url}">${push.branch}</a>${push.pull_request ? html` · <a href="${push.pull_request}">pull request</a>` : ''}${push.pull_request_error ? html` · the pull request was not opened (${push.pull_request_error})` : ''}</p>` : html`<p class="muted">Not pushed: ${push.reason}</p>`) : ''}
${files.length ? html`<h2>Changed files</h2><table class="run-files"><tbody>${files.map((f) => html`<tr><td><code>${f.path}</code></td><td class="add">+${f.added == null ? '·' : f.added}</td><td class="del">−${f.removed == null ? '·' : f.removed}</td></tr>`)}</tbody></table>` : (row.state === 'succeeded' ? html`<p class="muted">No files changed.</p>` : '')}
${files.length ? html`<h2>Changed files</h2><table class="run-files"><tbody>${files.map((f) => html`<tr><td><code>${f.path}</code></td><td class="add">+${f.added == null ? '·' : f.added}</td><td class="del">−${f.removed == null ? '·' : f.removed}</td></tr>`)}</tbody></table>` : (row.state === 'succeeded' ? html`<p class="muted">No files changed.</p>` : '')}
-${d.patch ? html`<h2>The patch${d.patch_truncated ? ' (cut)' : ''}</h2><pre class="run-patch" data-patch>${d.patch}</pre>` : ''}
+${d.patch ? html`<h2>The patch${d.patch_truncated ? ' (cut)' : ''}</h2><pre tabindex="0" class="run-patch" data-patch>${d.patch}</pre>` : ''}
${typeof d.duration_ms === 'number' ? html`<p class="muted">${Math.round(d.duration_ms / 1000)} s on your machine${d.cost_usd ? ` · about $${Number(d.cost_usd).toFixed(4)} of model use on your own keys` : ''}</p>` : ''}
${typeof d.duration_ms === 'number' ? html`<p class="muted">${Math.round(d.duration_ms / 1000)} s on your machine${d.cost_usd ? ` · about $${Number(d.cost_usd).toFixed(4)} of model use on your own keys` : ''}</p>` : ''}
</section>`;
</section>`;
}
}
@@ -184,7 +184,7 @@ ${typeof d.duration_ms === 'number' ? html`<p class="muted">${Math.round(d.durat
@@ -184,7 +184,7 @@ ${typeof d.duration_ms === 'number' ? html`<p class="muted">${Math.round(d.durat
const body = html`<p class="crumbs"><a href="/run">Run on your machine</a></p>
const body = html`<p class="crumbs"><a href="/run">Run on your machine</a></p>
<h1 class="run-repo">${repoName(row.repo)}${row.ref ? html` <small>${row.ref}</small>` : ''}</h1>
<h1 class="run-repo">${repoName(row.repo)}${row.ref ? html` <small>${row.ref}</small>` : ''}</h1>
<p><span class="run-pill run-pill-${row.state}" data-state>${STATE_LABEL[row.state] || row.state}</span>${row.agent ? html` <span class="run-pill">${row.agent}</span>` : ''}</p>
<p><span class="run-pill run-pill-${row.state}" data-state>${STATE_LABEL[row.state] || row.state}</span>${row.agent ? html` <span class="run-pill">${row.agent}</span>` : ''}</p>
-<pre class="run-prompt">${row.prompt}</pre>
+<pre tabindex="0" class="run-prompt">${row.prompt}</pre>
${live ? html`<section class="run-live" data-events="/run/tasks/${row.id}/events" aria-live="polite"><h2>Live</h2><ol class="run-log" data-log></ol></section>
${live ? html`<section class="run-live" data-events="/run/tasks/${row.id}/events" aria-live="polite"><h2>Live</h2><ol class="run-log" data-log></ol></section>
<form method="post" action="/run/tasks/${row.id}/cancel"><button class="run-btn" type="submit">Cancel</button></form>` : resultHtml(row)}`;
<form method="post" action="/run/tasks/${row.id}/cancel"><button class="run-btn" type="submit">Cancel</button></form>` : resultHtml(row)}`;
return page(req, res, { title: 'Task', body });
return page(req, res, { title: 'Task', body });
@@ -30,7 +30,7 @@ function markdown(src, { headingOffset = 1 } = {}) {
@@ -30,7 +30,7 @@ function markdown(src, { headingOffset = 1 } = {}) {
const flushList = () => { if (list) { out.push(`<${list.type}>${list.items.map((i) => `<li>${inline(i)}</li>`).join('')}</${list.type}>`); list = null; } };
const flushList = () => { if (list) { out.push(`<${list.type}>${list.items.map((i) => `<li>${inline(i)}</li>`).join('')}</${list.type}>`); list = null; } };
for (const line of lines) {
for (const line of lines) {
if (fence) {
if (fence) {
- if (/^```/.test(line)) { out.push(`<pre><code>${esc(fence.join('\n'))}</code></pre>`); fence = null; } else fence.push(line);
+ if (/^```/.test(line)) { out.push(`<pre tabindex="0"><code>${esc(fence.join('\n'))}</code></pre>`); fence = null; } else fence.push(line);
continue;
continue;
}
}
if (/^```/.test(line)) { flushPara(); flushList(); fence = []; continue; }
if (/^```/.test(line)) { flushPara(); flushList(); fence = []; continue; }
@@ -54,7 +54,7 @@ function markdown(src, { headingOffset = 1 } = {}) {
@@ -54,7 +54,7 @@ function markdown(src, { headingOffset = 1 } = {}) {
flushList();
flushList();
para.push(line.trim());
para.push(line.trim());
}
}
- if (fence) out.push(`<pre><code>${esc(fence.join('\n'))}</code></pre>`);
+ if (fence) out.push(`<pre tabindex="0"><code>${esc(fence.join('\n'))}</code></pre>`);
flushPara(); flushList();
flushPara(); flushList();
return out.join('\n');
return out.join('\n');
}
}
@@ -40,6 +40,8 @@ const BOOST = /<script src="\/shared\/boost\.js\?v=[0-9a-f]{12}" data-main="#mai
@@ -40,6 +40,8 @@ const BOOST = /<script src="\/shared\/boost\.js\?v=[0-9a-f]{12}" data-main="#mai
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);
const head = r.text.slice(0, r.text.indexOf('</head>'));
const head = r.text.slice(0, r.text.indexOf('</head>'));
assert.strictEqual((r.text.match(/<title>/g) || []).length, 1, 'exactly one <title>');
assert.strictEqual((r.text.match(/<title>/g) || []).length, 1, 'exactly one <title>');
+ // A code block that scrolls sideways on a phone is a region the keyboard must reach (axe scrollable-region-focusable).
+ assert.ok(!/<pre(?![^>]*tabindex)[\s>]/.test(r.text), 'every <pre> is focusable');
assert.ok(head.includes('<title>OpenVibe.Codes: code with any agent</title>'), 'home title');
assert.ok(head.includes('<title>OpenVibe.Codes: code with any agent</title>'), 'home title');
assert.ok(head.includes('<link rel="canonical" href="https://openvibe.codes/">'), 'canonical');
assert.ok(head.includes('<link rel="canonical" href="https://openvibe.codes/">'), 'canonical');
assert.ok(head.includes('<meta name="robots" content="index, follow">'), 'robots');
assert.ok(head.includes('<meta name="robots" content="index, follow">'), 'robots');