openvibers/openvibe.codes Commits

Codes: code blocks are keyboard-focusable (axe scrollable-region-focusable) (#77)

OP OpenVibers committed 29 days ago · f9dd107d73 · 4 files, +12 −10

An axe scan (WCAG 2.1 AA) of every OpenVibe home page at 390 px found code blocks that scroll sideways but cannot be reached by keyboard (`scrollable-region-focusable`, serious): the home page's code sample and `/docs`.

Every `<pre>` now has `tabindex="0"`: the home page and docs samples, Markdown fences, and a run's summary, patch and prompt. `test/layout.test.js` checks the home page has no unfocusable `<pre>`.

Changes

ddiff --git a/server/http/pages.js b/server/http/pages.js
iindex 74826f9..10bc2a9 100644
---- a/server/http/pages.js
++++ b/server/http/pages.js
@@@ -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>`,
ddiff --git a/server/http/run.js b/server/http/run.js
iindex d3e4ad7..8460602 100644
---- a/server/http/run.js
++++ b/server/http/run.js
@@@ -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.durat
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>
<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 });
ddiff --git a/server/render/markdown.js b/server/render/markdown.js
iindex 076977b..0b58a74 100644
---- a/server/render/markdown.js
++++ b/server/render/markdown.js
@@@ -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');
}
ddiff --git a/test/layout.test.js b/test/layout.test.js
iindex af1a8c8..0e049a2 100644
---- a/test/layout.test.js
++++ b/test/layout.test.js
@@@ -40,6 +40,8 @@ const BOOST = /<script src="\/shared\/boost\.js\?v=[0-9a-f]{12}" data-main="#mai
assert.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');