openvibers/openvibe.codes Commits
Codes: use the shared Link machine picker (#362)
Changes
@@@ -439,14 +439,10 @@ ${showcase.faq({ items: [const waiting = signedIn ? await approvals.inbox(req.viewer.subject) : [];const conductors = rows.filter((x) => x.kind === 'conductor');const repos = signedIn ? await recentRepos(req) : [];-- const mine = signedIn ? await machinesOf(req, res) : { ok: false, machines: [] };-- const used = signedIn ? await lastUsedMachines(req) : [];-- const machineList = orderMachines(mine.machines, used);const agentOptions = (harnesses?.list?.() || []).map((a) => `<option value="${esc(a.id)}"${settings && settings.default_agent === a.id ? ' selected' : ''}>${esc(a.name)}</option>`).join('');-- const machineOptions = machineList.map((m) => machineOption(m, settings)).join('');const saved = repos.map((repo) => `<a class="c4-chip" href="/?repo=${encodeURIComponent(repo.url)}#c4-prompt" data-c4-saved-repo="${esc(repo.url)}">${icon('<path d="M4 4h16v16l-8-4-8 4z"/>')}${esc(repo.full_name)}</a>`).join('');const content = `${header(signedIn ? 'Home' : 'Every coding agent. One place.', signedIn ? '' : 'Your keys, your machines. Tell an agent what to do and follow the work from your phone.')}--<section class="c4-card c4-composer"><form method="post" action="/start"><label for="c4-prompt">What should an agent do?</label><textarea id="c4-prompt" name="prompt" placeholder="Describe the change, question or fix" required></textarea><div class="c4-controls"><label class="c4-field-repo">Project<input type="url" name="repo" value="${esc(String(req.query.repo || ''))}" placeholder="Paste a repository URL" autocomplete="url" list="c4-repo-options" data-c4-repo-search data-c4-repo-url="/api/v1/me/repos"><datalist id="c4-repo-options">${repos.map((repo) => `<option value="${esc(repo.url)}">${esc(repo.full_name)}</option>`).join('')}</datalist></label><label>Agent <select name="agent"><option value="">Auto</option>${agentOptions}</select></label><label>Machine <select name="machine"><option value="">Auto</option>${machineOptions}</select></label><label>Cost cap <select name="cap"><option>$1.00</option><option>$5.00</option></select></label><button class="c4-button" type="submit">Start a task</button></div><p class="c4-hint">Run places the task on one of your own machines; the machine and agent you pick are saved as your defaults.<span class="c4-hint-status" data-c4-repo-status role="status"></span></p>${saved ? `<div class="c4-chips c4-saved" aria-label="Saved repositories">${saved}</div>` : ''}</form></section>++<section class="c4-card c4-composer"><form method="post" action="/start"><label for="c4-prompt">What should an agent do?</label><textarea id="c4-prompt" name="prompt" placeholder="Describe the change, question or fix" required></textarea><div class="c4-controls"><label class="c4-field-repo">Project<input type="url" name="repo" value="${esc(String(req.query.repo || ''))}" placeholder="Paste a repository URL" autocomplete="url" list="c4-repo-options" data-c4-repo-search data-c4-repo-url="/api/v1/me/repos"><datalist id="c4-repo-options">${repos.map((repo) => `<option value="${esc(repo.url)}">${esc(repo.full_name)}</option>`).join('')}</datalist></label><label>Agent <select name="agent"><option value="">Auto</option>${agentOptions}</select></label><div data-ov-machines data-needs="worker" data-name="machine" data-auto-label="Auto" data-value="${esc(req.query.machine || settings?.default_machine || '')}"></div><label>Cost cap <select name="cap"><option>$1.00</option><option>$5.00</option></select></label><button class="c4-button" type="submit">Start a task</button></div><p class="c4-hint">Run places the task on one of your own machines; the machine and agent you pick are saved as your defaults.<span class="c4-hint-status" data-c4-repo-status role="status"></span></p>${saved ? `<div class="c4-chips c4-saved" aria-label="Saved repositories">${saved}</div>` : ''}</form></section><div class="c4-grid"><section class="c4-card"><div class="c4-card-head"><h2>Running</h2><a href="/tasks?state=running">All tasks</a></div>${rows.filter((x) => !['done', 'succeeded', 'failed', 'cancelled'].includes(x.state)).slice(0, 4).map(rowHtml).join('') || '<div class="c4-emptyline">No tasks running yet. <a href="/#c4-prompt">Start a task</a></div>'}</section><section class="c4-card"><div class="c4-card-head"><h2>Needs you</h2>${waiting.length ? '<a href="/approvals">The inbox</a>' : ''}</div><p class="c4-sub">Approvals and questions</p>${waiting.length ? waiting.slice(0, 3).map((a) => `<a class="c4-task" href="/approvals"><span class="c4-status-dot c4-running" aria-label="Waiting"></span><span class="c4-task-title">${esc(a.tool || 'a tool')}</span><span class="c4-task-meta">${esc(String(a.summary || '').slice(0, 120))}</span></a>`).join('') : '<div class="c4-emptyline">Nothing needs your attention. <a href="/tasks">View tasks</a></div>'}</section></div>${signedIn ? `<section class="c4-card c4-conductor-cta"><div class="c4-card-head"><h2>Your conductor</h2><a href="/conductor">${conductors.length ? 'Open' : 'Set it up'}</a></div><p class="c4-muted">One long session on the agent you choose, on your own machine, carrying the plan forward and handing the work to your other agents. Each message you send is its next turn${conductors.length ? `: ${conductors.length} running or waiting` : ''}.</p><p><a class="c4-button" href="/conductor">${conductors.length ? 'Open your conductor' : 'Start your conductor'}</a></p></section>` : ''}<section class="c4-card"><div class="c4-card-head"><h2>Recent tasks</h2><a href="/tasks">View all</a></div>${rows.slice(0, 5).map(rowHtml).join('') || '<div class="c4-emptyline">No tasks yet. <a href="/#c4-prompt">Start a task</a></div>'}</section>@@@ -544,9 +540,6 @@ ${signedIn ? '' : `<section class="c4-reasons"><h2>Built for the way you work</hif (!viewer(req)) return page(req, res, 'Conductor', wrap(`${header('Conductor')}${signIn(req)}`));const settings = await userSettings.get(req.viewer.subject);const mine = await machinesOf(req, res);-- const used = await lastUsedMachines(req);-- const machineOptions = orderMachines((mine.machines || []).filter((m) => m.state !== 'revoked'), used)-- .map((m) => `<option value="${esc(m.id)}"${settings.default_machine === m.id ? ' selected' : ''}>${esc(m.name || m.node_id || m.id)}</option>`).join('');// The folders this person's machines have worked in, newest first: the choices beside the typed path.const folders = await db.many(`SELECT folder, machine, max(created_at) AS at, max(kind) AS last_kind FROM run_tasks@@@ -562,7 +555,7 @@ ${signedIn ? '' : `<section class="c4-reasons"><h2>Built for the way you work</hconst content = `${header('Conductor', 'Your orchestrator, running here: the agent you choose on your own machine, carrying the plan forward through the other agents.')}${flash(req)}<section class="c4-card c4-composer"><form method="post" action="/conductor"><label for="c4-folder">Working folder</label><div class="c4-folder-row"><input id="c4-folder" name="folder" placeholder="/home/you/project" value="${esc(String(req.query.folder || ''))}" list="c4-folders" autocomplete="off" spellcheck="false"><select name="folder_pick" data-c4-folder-pick aria-label="Folders this machine has worked in"><option value="">A recent folder…</option>${folderChoices}</select></div><datalist id="c4-folders">${folders.map((f) => `<option value="${esc(f.folder)}"></option>`).join('')}</datalist>--<div class="c4-controls"><label>Machine <select name="machine" data-c4-machine><option value="">Your default</option>${machineOptions}</select></label><label>Who leads this task <select name="who" data-c4-model><option value="">${esc(`Your setting: ${savedLeadName(settings)}`)}</option>${leadChoices(settings, aware, null, false)}</select></label><label>Safety <select name="safety">${[['ask', 'Ask me'], ['project', 'Auto in the project'], ['full', 'Full access']].map(([v, l]) => `<option value="${v}"${settings.safety === v ? ' selected' : ''}>${l}</option>`).join('')}</select></label></div>++<div class="c4-controls"><div data-ov-machines data-needs="worker" data-name="machine" data-auto-label="Your default" data-value="${esc(req.query.machine || '')}" data-c4-machine></div><label>Who leads this task <select name="who" data-c4-model><option value="">${esc(`Your setting: ${savedLeadName(settings)}`)}</option>${leadChoices(settings, aware, null, false)}</select></label><label>Safety <select name="safety">${[['ask', 'Ask me'], ['project', 'Auto in the project'], ['full', 'Full access']].map(([v, l]) => `<option value="${v}"${settings.safety === v ? ' selected' : ''}>${l}</option>`).join('')}</select></label></div><label class="c4-note-field" for="c4-conductor-note">What should it pick up? <small>Optional. It starts from the hand-over note and the plan; say here what you want first.</small></label><textarea id="c4-conductor-note" name="note" rows="2" placeholder="e.g. carry on with the next piece of the plan"></textarea><p class="c4-hint">It opens that folder, so it loads that project's memory and AGENTS.md. It runs with your own login on that machine, and never with a key of ours.${mine.problem ? ` ${esc(mine.problem.detail || '')}` : ''}</p><div class="c4-conductor-actions"><span class="c4-hint">One conductor is enough: it hands the work to the other agents through the Codes tools.</span><button class="c4-button" type="submit">Start your conductor</button></div>@@@ -767,14 +760,11 @@ ${report.days.length ? `<div class="table-wrap" tabindex="0" role="region" aria-];r.get('/settings', async (req, res) => {const settings = viewer(req) ? await userSettings.get(req.viewer.subject) : null;-- const mine = viewer(req) ? await machinesOf(req, res) : { machines: [] };-- const used = viewer(req) ? await lastUsedMachines(req) : [];// Each agent says what it is ready for (domain/readiness.js), drawn by Shared's select as a badge and a line.const fleetRows = viewer(req) && fleet ? await fleet.agents(req.viewer.subject, harnesses).catch(() => []) : [];const aware = agentReadiness(fleetRows);const agentOptions = (harnesses?.list?.() || []).map((a) => `<option value="${esc(a.id)}"${settings && settings.default_agent === a.id ? ' selected' : ''}${optionAttrs(aware.get(a.id))}>${esc(a.name)}</option>`).join('');// The same order the Home picker shows: the machines their work ran on last come first.-- const machineOptions = orderMachines((mine.machines || []).filter((m) => m.state !== 'revoked'), used).map((m) => machineOption(m, settings)).join('');// Settings → Limits (C9a): steps and minutes per task, for every agent at once; No limit means none.const limitSelect = (name, value, presets, unit) => {const choices = [[null, 'No limit'], ...presets];@@@ -798,7 +788,7 @@ ${report.days.length ? `<div class="table-wrap" tabindex="0" role="region" aria-const content = `${header('Settings', 'Your choices, remembered for your account.')}${viewer(req) ? `${flash(req)}<p class="c4-hint c4-setup-link">New here, or something not working? <a href="/setup">The guided setup</a> checks your machine, agents and keys step by step.</p><section class="c4-card"><form class="c4-form" method="post" action="/settings"><div class="c4-form-grid"><label>Default agent <select name="default_agent"><option value="">Auto</option>${agentOptions}</select></label>--<label>Default machine <select name="default_machine"><option value="">Auto</option>${machineOptions}</select></label>++<div data-ov-machines data-needs="worker" data-name="default_machine" data-auto-label="Auto" data-value="${esc(req.query.machine || settings.default_machine || '')}"></div></div><h3 class="c4-subhead" id="lead">Who leads your tasks</h3><p class="c4-hint">The agent that plans each task and hands its parts to the others. Each choice shows what your machines report about it and what it costs. One task can still pick another lead in its composer.</p>@@@ -83,13 +83,12 @@ function createSetupRoutes(ctx, kit) {const list = (d.machines.machines || []).filter((m) => m.state !== 'revoked');const sources = d.compute.sources || [];const sourceRows = sources.map((s) => `<li class="c4-row c4-row-flat"><span class="c4-row-dot${s.status === 'ready' ? ' c4-dot-ok' : ''}" aria-hidden="true"></span><span class="c4-row-label">${esc(s.name)}</span><span class="c4-row-note">${s.status === 'ready' ? 'Ready' : s.status === 'coming_soon' ? 'Coming soon' : 'Unavailable'}${s.price && s.price.amount_usd != null ? ` · $${esc(s.price.amount_usd)}/${esc(s.price.unit)}` : ''}${s.reason ? ` · ${esc(s.reason)}` : ''}</span></li>`).join('');-- const rows = list.map((m) => `<li class="c4-row c4-row-flat"><span class="c4-row-dot c4-dot-${m.state === 'online' ? 'ok' : 'skip'}" aria-hidden="true"></span><span class="c4-row-label">${esc(m.name || m.node_id || m.id)}</span><span class="c4-row-note">${m.state === 'online' ? 'Online: ready for your work' : 'Offline: start it, and it comes back here'}</span></li>`).join('');const problem = d.machines.ok === false && d.machines.problem ? note(esc(d.machines.problem.detail || 'Your machines could not be read.'), 'bad') : '';const how = `<ol class="c4-setup-how">--<li><b>Get a one-time code</b> on <a href="https://openvibe.link/machines" rel="noopener">openvibe.link/machines</a>. It lasts ten minutes.</li>++<li><b>Get a one-time code</b> on <a href="https://openvibe.link/add-computer?return_to=https%3A%2F%2Fopenvibe.codes%2Fsetup" rel="noopener">openvibe.link</a>. It lasts ten minutes.</li><li><b>Run the installer on that computer</b> and give it the code:${command('curl -fsSL https://openvibe.link/install | sh -s -- --worker', 'On the machine')}</li><li><b>It pairs itself</b> and shows up here as Online. It runs your work and nobody else's.</li></ol>`;-- return `${problem}${d.compute.ok === false && d.compute.problem ? note(esc(d.compute.problem.detail), 'bad') : ''}<p class="c4-hint">Choose a place for your work. Link checks readiness before a task can start. Rentals need a live price and a spending limit.</p>${sourceRows ? `<ul class="c4-rows">${sourceRows}</ul>` : ''}${rows ? `<ul class="c4-rows">${rows}</ul>` : ''}${list.length ? `<details class="c4-setup-more"><summary>Pair another machine</summary>${how}</details>` : how}++ return `${problem}${d.compute.ok === false && d.compute.problem ? note(esc(d.compute.problem.detail), 'bad') : ''}<p class="c4-hint">Choose a place for your work. Link checks readiness before a task can start. Rentals need a live price and a spending limit.</p>${sourceRows ? `<ul class="c4-rows">${sourceRows}</ul>` : ''}${how}<div class="c4-setup-actions"><a class="c4-button${list.length ? ' c4-button-light' : ''}" href="https://openvibe.link/machines#pair">${list.length ? 'Manage machines' : 'I have a code'}</a><a class="c4-opill" href="/setup#step-machine">Check again</a></div>`;}@@@ -131,13 +130,12 @@ ${rows ? `<ul class="c4-rows">${rows}</ul>` : d.keys ? '<div class="c4-emptyline: '<p class="c4-hint">Codes makes repositories on OpenVibe Git in your name with your OpenVibe account, and your tasks push their branches there. Nothing to link.</p><div class="c4-setup-actions"><a class="c4-button c4-button-light" href="/git">Make a repository</a></div>';}-- function choicesBody(d, machines) {++ function choicesBody(d, machines, pickedMachine = '') {const s = d.settings;const ready = readiness(d.rows);const offers = (harnesses && harnesses.list ? harnesses.list() : []).filter((a) => a && a.id);const agentOption = (id, name, selected) => `<option value="${esc(id)}"${selected ? ' selected' : ''}${optionAttrs(ready.get(id))}>${esc(name)}</option>`;const agents = offers.map((a) => agentOption(a.id, a.name || a.id, s.default_agent === a.id)).join('');-- const machineOptions = (machines.machines || []).filter((m) => m.state !== 'revoked').map((m) => `<option value="${esc(m.node_id || m.id)}"${s.default_machine && (s.default_machine === m.node_id || s.default_machine === m.id) ? ' selected' : ''} data-state="${m.state === 'online' ? 'ok' : 'off'}" data-badge="${m.state === 'online' ? 'Online' : 'Offline'}">${esc(m.name || m.node_id || m.id)}</option>`).join('');const fallbacks = s.conductor_fallbacks || [];const fallbackRow = (i) => {const f = fallbacks[i] || { agent: '', model: '' };@@@ -148,7 +146,7 @@ ${rows ? `<ul class="c4-rows">${rows}</ul>` : d.keys ? '<div class="c4-emptylinereturn `<form class="c4-form" method="post" action="/setup/choices"><div class="c4-form-grid"><label>Default agent <select name="default_agent" data-ov-title="Default agent"><option value="" data-desc="Codes picks the agent with room for each task">Auto</option>${agents}</select></label>--<label>Default machine <select name="default_machine" data-ov-title="Default machine"><option value="" data-desc="Your machine with room, else OpenVibe's">Auto</option>${machineOptions}</select></label>++<div data-ov-machines data-needs="worker" data-name="default_machine" data-auto-label="Auto" data-value="${esc(pickedMachine || s.default_machine || '')}"></div></div><h3 class="c4-subhead">When your conductor's agent runs low</h3><p class="c4-hint">Your conductor runs Claude Code. When it nears a usage limit, the next turn moves to the first fallback with room, and comes back once the window frees.</p>@@@ -175,7 +173,7 @@ ${rows ? `<ul class="c4-rows">${rows}</ul>` : d.keys ? '<div class="c4-emptylineaccounts: 'Where your work is pushed.',choices: 'What Codes uses when you do not say.',};-- const bodies = { machine: machineBody(d), agents: agentsBody(d), keys: keysBody(d), accounts: accountsBody(d), choices: choicesBody(d, d.machines) };++ const bodies = { machine: machineBody(d), agents: agentsBody(d), keys: keysBody(d), accounts: accountsBody(d), choices: choicesBody(d, d.machines, String(req.query.machine || '')) };const nav = steps.map((s, i) => `<li><a class="c4-setup-chip${s.done ? ' is-done' : ''}${s.id === next ? ' is-next' : ''}" href="#step-${s.id}"><span class="c4-setup-chip-n" aria-hidden="true">${s.done ? CHECK : i + 1}</span>${esc(s.short)}<span class="c4-sr-only">${s.done ? ', done' : s.optional ? ', optional' : ''}</span></a></li>`).join('');const ready = requiredLeft === 0? `<section class="c4-card c4-setup-ready"><h2>You are set</h2><p>Your machine, an agent and your choices are in place.${steps.some((s) => s.optional && !s.done) ? ' The optional steps can wait.' : ''}</p><div class="c4-setup-actions"><a class="c4-button" href="/">Start a task</a><a class="c4-opill" href="/conductor">Start your conductor</a></div></section>`@@@ -41,7 +41,6 @@ const WORKSPACE_NAV = [{ label: 'Tasks', href: '/tasks', icon: 'fa-list' },{ label: 'Conductor', href: '/conductor', icon: 'fa-diagram-project' },{ label: 'Agents', href: '/agents', icon: 'fa-robot' },-- { label: 'Machines', href: '/machines', icon: 'fa-display' },{ label: 'Memory', href: '/memory', icon: 'fa-brain' },{ label: 'Knowledge', href: '/knowledge', icon: 'fa-book' },{ label: 'Usage', href: '/usage', icon: 'fa-chart-line' },@@@ -117,7 +116,9 @@ function renderPage(o) {const crumbs = (o.crumbs || []).length? html`<nav class="crumbs" aria-label="Breadcrumbs">${o.crumbs.map((c, i) => html`${i ? ' / ' : ''}${c.href ? html`<a href="${c.href}">${c.label}</a>` : c.label}`)}</nav>`: '';-- const scripts = (o.sharedScripts || []).map((name) => html`<script src="${ovServe.url(name)}" defer></script>`).concat((o.scripts || []).map((rel) => html`<script src="${asset(rel)}" defer></script>`));++ const scripts = (o.sharedScripts || []).map((name) => html`<script src="${ovServe.url(name)}" defer></script>`);++ if (String(o.body || '').includes('data-ov-machines')) scripts.push(raw('<script src="/shared/machines.js" defer></script>'));++ scripts.push(...(o.scripts || []).map((rel) => html`<script src="${asset(rel)}" defer></script>`));return shell.page({name: SITE_NAME,lang: 'en',@@@ -45,20 +45,16 @@ const TASK3 = 'tsk_01ARZ3NDEKTSV4RRFFQ69G5FAX';assert.ok(!home.text.includes('<option value="' + NODE + '">Desk'), 'the chat offers no machine it could not honour');});-- await check('the machine picker is in the order of use, on Home and on Settings', async () => {++ await check('Settings mounts the Link machine picker with the saved default', async () => {// The Desk ran the work; the Laptop never did, and Network lists the Laptop first.await db.exec(`INSERT INTO codes_attempts (id, task_id, n, agent, machine, state, started_at, finished_at, created_at)VALUES ('att_01ARZ3NDEKTSV4RRFFQ69G5FAC',$1,2,'codex',$2,'done', now() - interval '2 hours', now() - interval '2 hours', now() - interval '2 hours')`, [TASK2, NODE]);await t.ctx.userSettings.save(ada.subject, { default_machine: OTHER });-- for (const page of ['/settings']) {-- const shown = await t.get(page, { as: ada });-- assert.strictEqual(shown.status, 200);-- const desk = shown.text.indexOf(`<option value="${NODE}">Desk`);-- const laptop = shown.text.indexOf(`<option value="${OTHER}"`);-- assert.ok(desk > -1 && laptop > -1, `${page} offers both machines`);-- assert.ok(desk < laptop, `${page} shows the machine used last first`);-- assert.ok(shown.text.includes('Laptop · your default'), `${page} marks the saved default`);-- }++ const shown = await t.get('/settings', { as: ada });++ assert.strictEqual(shown.status, 200);++ assert.ok(shown.text.includes('data-ov-machines') && shown.text.includes('data-name="default_machine"'), 'Settings mounts the shared picker');++ assert.ok(shown.text.includes(`data-value="${OTHER}"`), 'the saved default is the initial selection');++ assert.ok(!shown.text.includes(`<option value="${NODE}"`), 'Settings does not render its own machine list');});await check('the composer remembers the machine and agent a person picks, then opens Run’s form', async () => {@@@ -7,6 +7,10 @@const assert = require('assert');const { boot, check, done } = require('./helpers/boot');const { readiness, optionAttrs } = require('../server/domain/readiness');++const { createSetupRoutes } = require('../server/http/setup');++const { createC4Routes } = require('../server/http/c4');++assert.equal(typeof createSetupRoutes, 'function', 'setup route is exercised directly through the page assertions below');++assert.equal(typeof createC4Routes, 'function', 'Codes composer and conductor routes are exercised by the same app below');const NODE = 'lnk_01J8Z3Q4R5S6T7V8W9X0Y1Z2S1';const DEVICE = 'dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2S1';@@@ -63,6 +67,7 @@ const DEVICE = 'dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2S1';assert.match(res.text, /opencode auth login/, 'a signed-out agent says how to sign in');assert.match(res.text, /<option value="claude-code"[^>]*data-state="ok" data-badge="41% used"/, 'the default agent picker says what is ready');assert.match(res.text, /<option value="opencode"[^>]*data-state="bad" data-badge="Sign in"[^>]*data-href="\/setup#agent-opencode"/);++ assert.match(res.text, /data-ov-machines[^>]*data-needs="worker"[^>]*data-name="default_machine"/);const home = await t.get('/', { as: ada });assert.match(home.text, /<b>2\/3<\/b>[\s\S]*Next: Your choices/);assert.match(home.text, /<option value="claude-code\|[^"]*"[^>]*data-state="ok"/, 'the composer\'s agent picker knows too');
@@ -439,14 +439,10 @@ ${showcase.faq({ items: [
@@ -439,14 +439,10 @@ ${showcase.faq({ items: [
const waiting = signedIn ? await approvals.inbox(req.viewer.subject) : [];
const waiting = signedIn ? await approvals.inbox(req.viewer.subject) : [];
const conductors = rows.filter((x) => x.kind === 'conductor');
const conductors = rows.filter((x) => x.kind === 'conductor');
const repos = signedIn ? await recentRepos(req) : [];
const repos = signedIn ? await recentRepos(req) : [];
- const mine = signedIn ? await machinesOf(req, res) : { ok: false, machines: [] };
- const used = signedIn ? await lastUsedMachines(req) : [];
- const machineList = orderMachines(mine.machines, used);
const agentOptions = (harnesses?.list?.() || []).map((a) => `<option value="${esc(a.id)}"${settings && settings.default_agent === a.id ? ' selected' : ''}>${esc(a.name)}</option>`).join('');
const agentOptions = (harnesses?.list?.() || []).map((a) => `<option value="${esc(a.id)}"${settings && settings.default_agent === a.id ? ' selected' : ''}>${esc(a.name)}</option>`).join('');
- const machineOptions = machineList.map((m) => machineOption(m, settings)).join('');
const saved = repos.map((repo) => `<a class="c4-chip" href="/?repo=${encodeURIComponent(repo.url)}#c4-prompt" data-c4-saved-repo="${esc(repo.url)}">${icon('<path d="M4 4h16v16l-8-4-8 4z"/>')}${esc(repo.full_name)}</a>`).join('');
const saved = repos.map((repo) => `<a class="c4-chip" href="/?repo=${encodeURIComponent(repo.url)}#c4-prompt" data-c4-saved-repo="${esc(repo.url)}">${icon('<path d="M4 4h16v16l-8-4-8 4z"/>')}${esc(repo.full_name)}</a>`).join('');
const content = `${header(signedIn ? 'Home' : 'Every coding agent. One place.', signedIn ? '' : 'Your keys, your machines. Tell an agent what to do and follow the work from your phone.')}
const content = `${header(signedIn ? 'Home' : 'Every coding agent. One place.', signedIn ? '' : 'Your keys, your machines. Tell an agent what to do and follow the work from your phone.')}
-<section class="c4-card c4-composer"><form method="post" action="/start"><label for="c4-prompt">What should an agent do?</label><textarea id="c4-prompt" name="prompt" placeholder="Describe the change, question or fix" required></textarea><div class="c4-controls"><label class="c4-field-repo">Project<input type="url" name="repo" value="${esc(String(req.query.repo || ''))}" placeholder="Paste a repository URL" autocomplete="url" list="c4-repo-options" data-c4-repo-search data-c4-repo-url="/api/v1/me/repos"><datalist id="c4-repo-options">${repos.map((repo) => `<option value="${esc(repo.url)}">${esc(repo.full_name)}</option>`).join('')}</datalist></label><label>Agent <select name="agent"><option value="">Auto</option>${agentOptions}</select></label><label>Machine <select name="machine"><option value="">Auto</option>${machineOptions}</select></label><label>Cost cap <select name="cap"><option>$1.00</option><option>$5.00</option></select></label><button class="c4-button" type="submit">Start a task</button></div><p class="c4-hint">Run places the task on one of your own machines; the machine and agent you pick are saved as your defaults.<span class="c4-hint-status" data-c4-repo-status role="status"></span></p>${saved ? `<div class="c4-chips c4-saved" aria-label="Saved repositories">${saved}</div>` : ''}</form></section>
+<section class="c4-card c4-composer"><form method="post" action="/start"><label for="c4-prompt">What should an agent do?</label><textarea id="c4-prompt" name="prompt" placeholder="Describe the change, question or fix" required></textarea><div class="c4-controls"><label class="c4-field-repo">Project<input type="url" name="repo" value="${esc(String(req.query.repo || ''))}" placeholder="Paste a repository URL" autocomplete="url" list="c4-repo-options" data-c4-repo-search data-c4-repo-url="/api/v1/me/repos"><datalist id="c4-repo-options">${repos.map((repo) => `<option value="${esc(repo.url)}">${esc(repo.full_name)}</option>`).join('')}</datalist></label><label>Agent <select name="agent"><option value="">Auto</option>${agentOptions}</select></label><div data-ov-machines data-needs="worker" data-name="machine" data-auto-label="Auto" data-value="${esc(req.query.machine || settings?.default_machine || '')}"></div><label>Cost cap <select name="cap"><option>$1.00</option><option>$5.00</option></select></label><button class="c4-button" type="submit">Start a task</button></div><p class="c4-hint">Run places the task on one of your own machines; the machine and agent you pick are saved as your defaults.<span class="c4-hint-status" data-c4-repo-status role="status"></span></p>${saved ? `<div class="c4-chips c4-saved" aria-label="Saved repositories">${saved}</div>` : ''}</form></section>
<div class="c4-grid"><section class="c4-card"><div class="c4-card-head"><h2>Running</h2><a href="/tasks?state=running">All tasks</a></div>${rows.filter((x) => !['done', 'succeeded', 'failed', 'cancelled'].includes(x.state)).slice(0, 4).map(rowHtml).join('') || '<div class="c4-emptyline">No tasks running yet. <a href="/#c4-prompt">Start a task</a></div>'}</section><section class="c4-card"><div class="c4-card-head"><h2>Needs you</h2>${waiting.length ? '<a href="/approvals">The inbox</a>' : ''}</div><p class="c4-sub">Approvals and questions</p>${waiting.length ? waiting.slice(0, 3).map((a) => `<a class="c4-task" href="/approvals"><span class="c4-status-dot c4-running" aria-label="Waiting"></span><span class="c4-task-title">${esc(a.tool || 'a tool')}</span><span class="c4-task-meta">${esc(String(a.summary || '').slice(0, 120))}</span></a>`).join('') : '<div class="c4-emptyline">Nothing needs your attention. <a href="/tasks">View tasks</a></div>'}</section></div>
<div class="c4-grid"><section class="c4-card"><div class="c4-card-head"><h2>Running</h2><a href="/tasks?state=running">All tasks</a></div>${rows.filter((x) => !['done', 'succeeded', 'failed', 'cancelled'].includes(x.state)).slice(0, 4).map(rowHtml).join('') || '<div class="c4-emptyline">No tasks running yet. <a href="/#c4-prompt">Start a task</a></div>'}</section><section class="c4-card"><div class="c4-card-head"><h2>Needs you</h2>${waiting.length ? '<a href="/approvals">The inbox</a>' : ''}</div><p class="c4-sub">Approvals and questions</p>${waiting.length ? waiting.slice(0, 3).map((a) => `<a class="c4-task" href="/approvals"><span class="c4-status-dot c4-running" aria-label="Waiting"></span><span class="c4-task-title">${esc(a.tool || 'a tool')}</span><span class="c4-task-meta">${esc(String(a.summary || '').slice(0, 120))}</span></a>`).join('') : '<div class="c4-emptyline">Nothing needs your attention. <a href="/tasks">View tasks</a></div>'}</section></div>
${signedIn ? `<section class="c4-card c4-conductor-cta"><div class="c4-card-head"><h2>Your conductor</h2><a href="/conductor">${conductors.length ? 'Open' : 'Set it up'}</a></div><p class="c4-muted">One long session on the agent you choose, on your own machine, carrying the plan forward and handing the work to your other agents. Each message you send is its next turn${conductors.length ? `: ${conductors.length} running or waiting` : ''}.</p><p><a class="c4-button" href="/conductor">${conductors.length ? 'Open your conductor' : 'Start your conductor'}</a></p></section>` : ''}
${signedIn ? `<section class="c4-card c4-conductor-cta"><div class="c4-card-head"><h2>Your conductor</h2><a href="/conductor">${conductors.length ? 'Open' : 'Set it up'}</a></div><p class="c4-muted">One long session on the agent you choose, on your own machine, carrying the plan forward and handing the work to your other agents. Each message you send is its next turn${conductors.length ? `: ${conductors.length} running or waiting` : ''}.</p><p><a class="c4-button" href="/conductor">${conductors.length ? 'Open your conductor' : 'Start your conductor'}</a></p></section>` : ''}
<section class="c4-card"><div class="c4-card-head"><h2>Recent tasks</h2><a href="/tasks">View all</a></div>${rows.slice(0, 5).map(rowHtml).join('') || '<div class="c4-emptyline">No tasks yet. <a href="/#c4-prompt">Start a task</a></div>'}</section>
<section class="c4-card"><div class="c4-card-head"><h2>Recent tasks</h2><a href="/tasks">View all</a></div>${rows.slice(0, 5).map(rowHtml).join('') || '<div class="c4-emptyline">No tasks yet. <a href="/#c4-prompt">Start a task</a></div>'}</section>
@@ -544,9 +540,6 @@ ${signedIn ? '' : `<section class="c4-reasons"><h2>Built for the way you work</h
@@ -544,9 +540,6 @@ ${signedIn ? '' : `<section class="c4-reasons"><h2>Built for the way you work</h
if (!viewer(req)) return page(req, res, 'Conductor', wrap(`${header('Conductor')}${signIn(req)}`));
if (!viewer(req)) return page(req, res, 'Conductor', wrap(`${header('Conductor')}${signIn(req)}`));
const settings = await userSettings.get(req.viewer.subject);
const settings = await userSettings.get(req.viewer.subject);
const mine = await machinesOf(req, res);
const mine = await machinesOf(req, res);
- const used = await lastUsedMachines(req);
- const machineOptions = orderMachines((mine.machines || []).filter((m) => m.state !== 'revoked'), used)
- .map((m) => `<option value="${esc(m.id)}"${settings.default_machine === m.id ? ' selected' : ''}>${esc(m.name || m.node_id || m.id)}</option>`).join('');
// The folders this person's machines have worked in, newest first: the choices beside the typed path.
// The folders this person's machines have worked in, newest first: the choices beside the typed path.
const folders = await db.many(
const folders = await db.many(
`SELECT folder, machine, max(created_at) AS at, max(kind) AS last_kind FROM run_tasks
`SELECT folder, machine, max(created_at) AS at, max(kind) AS last_kind FROM run_tasks
@@ -562,7 +555,7 @@ ${signedIn ? '' : `<section class="c4-reasons"><h2>Built for the way you work</h
@@ -562,7 +555,7 @@ ${signedIn ? '' : `<section class="c4-reasons"><h2>Built for the way you work</h
const content = `${header('Conductor', 'Your orchestrator, running here: the agent you choose on your own machine, carrying the plan forward through the other agents.')}${flash(req)}
const content = `${header('Conductor', 'Your orchestrator, running here: the agent you choose on your own machine, carrying the plan forward through the other agents.')}${flash(req)}
<section class="c4-card c4-composer"><form method="post" action="/conductor">
<section class="c4-card c4-composer"><form method="post" action="/conductor">
<label for="c4-folder">Working folder</label><div class="c4-folder-row"><input id="c4-folder" name="folder" placeholder="/home/you/project" value="${esc(String(req.query.folder || ''))}" list="c4-folders" autocomplete="off" spellcheck="false"><select name="folder_pick" data-c4-folder-pick aria-label="Folders this machine has worked in"><option value="">A recent folder…</option>${folderChoices}</select></div><datalist id="c4-folders">${folders.map((f) => `<option value="${esc(f.folder)}"></option>`).join('')}</datalist>
<label for="c4-folder">Working folder</label><div class="c4-folder-row"><input id="c4-folder" name="folder" placeholder="/home/you/project" value="${esc(String(req.query.folder || ''))}" list="c4-folders" autocomplete="off" spellcheck="false"><select name="folder_pick" data-c4-folder-pick aria-label="Folders this machine has worked in"><option value="">A recent folder…</option>${folderChoices}</select></div><datalist id="c4-folders">${folders.map((f) => `<option value="${esc(f.folder)}"></option>`).join('')}</datalist>
-<div class="c4-controls"><label>Machine <select name="machine" data-c4-machine><option value="">Your default</option>${machineOptions}</select></label><label>Who leads this task <select name="who" data-c4-model><option value="">${esc(`Your setting: ${savedLeadName(settings)}`)}</option>${leadChoices(settings, aware, null, false)}</select></label><label>Safety <select name="safety">${[['ask', 'Ask me'], ['project', 'Auto in the project'], ['full', 'Full access']].map(([v, l]) => `<option value="${v}"${settings.safety === v ? ' selected' : ''}>${l}</option>`).join('')}</select></label></div>
+<div class="c4-controls"><div data-ov-machines data-needs="worker" data-name="machine" data-auto-label="Your default" data-value="${esc(req.query.machine || '')}" data-c4-machine></div><label>Who leads this task <select name="who" data-c4-model><option value="">${esc(`Your setting: ${savedLeadName(settings)}`)}</option>${leadChoices(settings, aware, null, false)}</select></label><label>Safety <select name="safety">${[['ask', 'Ask me'], ['project', 'Auto in the project'], ['full', 'Full access']].map(([v, l]) => `<option value="${v}"${settings.safety === v ? ' selected' : ''}>${l}</option>`).join('')}</select></label></div>
<label class="c4-note-field" for="c4-conductor-note">What should it pick up? <small>Optional. It starts from the hand-over note and the plan; say here what you want first.</small></label><textarea id="c4-conductor-note" name="note" rows="2" placeholder="e.g. carry on with the next piece of the plan"></textarea>
<label class="c4-note-field" for="c4-conductor-note">What should it pick up? <small>Optional. It starts from the hand-over note and the plan; say here what you want first.</small></label><textarea id="c4-conductor-note" name="note" rows="2" placeholder="e.g. carry on with the next piece of the plan"></textarea>
<p class="c4-hint">It opens that folder, so it loads that project's memory and AGENTS.md. It runs with your own login on that machine, and never with a key of ours.${mine.problem ? ` ${esc(mine.problem.detail || '')}` : ''}</p>
<p class="c4-hint">It opens that folder, so it loads that project's memory and AGENTS.md. It runs with your own login on that machine, and never with a key of ours.${mine.problem ? ` ${esc(mine.problem.detail || '')}` : ''}</p>
<div class="c4-conductor-actions"><span class="c4-hint">One conductor is enough: it hands the work to the other agents through the Codes tools.</span><button class="c4-button" type="submit">Start your conductor</button></div>
<div class="c4-conductor-actions"><span class="c4-hint">One conductor is enough: it hands the work to the other agents through the Codes tools.</span><button class="c4-button" type="submit">Start your conductor</button></div>
@@ -767,14 +760,11 @@ ${report.days.length ? `<div class="table-wrap" tabindex="0" role="region" aria-
@@ -767,14 +760,11 @@ ${report.days.length ? `<div class="table-wrap" tabindex="0" role="region" aria-
];
];
r.get('/settings', async (req, res) => {
r.get('/settings', async (req, res) => {
const settings = viewer(req) ? await userSettings.get(req.viewer.subject) : null;
const settings = viewer(req) ? await userSettings.get(req.viewer.subject) : null;
- const mine = viewer(req) ? await machinesOf(req, res) : { machines: [] };
- const used = viewer(req) ? await lastUsedMachines(req) : [];
// Each agent says what it is ready for (domain/readiness.js), drawn by Shared's select as a badge and a line.
// Each agent says what it is ready for (domain/readiness.js), drawn by Shared's select as a badge and a line.
const fleetRows = viewer(req) && fleet ? await fleet.agents(req.viewer.subject, harnesses).catch(() => []) : [];
const fleetRows = viewer(req) && fleet ? await fleet.agents(req.viewer.subject, harnesses).catch(() => []) : [];
const aware = agentReadiness(fleetRows);
const aware = agentReadiness(fleetRows);
const agentOptions = (harnesses?.list?.() || []).map((a) => `<option value="${esc(a.id)}"${settings && settings.default_agent === a.id ? ' selected' : ''}${optionAttrs(aware.get(a.id))}>${esc(a.name)}</option>`).join('');
const agentOptions = (harnesses?.list?.() || []).map((a) => `<option value="${esc(a.id)}"${settings && settings.default_agent === a.id ? ' selected' : ''}${optionAttrs(aware.get(a.id))}>${esc(a.name)}</option>`).join('');
// The same order the Home picker shows: the machines their work ran on last come first.
// The same order the Home picker shows: the machines their work ran on last come first.
- const machineOptions = orderMachines((mine.machines || []).filter((m) => m.state !== 'revoked'), used).map((m) => machineOption(m, settings)).join('');
// Settings → Limits (C9a): steps and minutes per task, for every agent at once; No limit means none.
// Settings → Limits (C9a): steps and minutes per task, for every agent at once; No limit means none.
const limitSelect = (name, value, presets, unit) => {
const limitSelect = (name, value, presets, unit) => {
const choices = [[null, 'No limit'], ...presets];
const choices = [[null, 'No limit'], ...presets];
@@ -798,7 +788,7 @@ ${report.days.length ? `<div class="table-wrap" tabindex="0" role="region" aria-
@@ -798,7 +788,7 @@ ${report.days.length ? `<div class="table-wrap" tabindex="0" role="region" aria-
const content = `${header('Settings', 'Your choices, remembered for your account.')}${viewer(req) ? `${flash(req)}<p class="c4-hint c4-setup-link">New here, or something not working? <a href="/setup">The guided setup</a> checks your machine, agents and keys step by step.</p><section class="c4-card"><form class="c4-form" method="post" action="/settings">
const content = `${header('Settings', 'Your choices, remembered for your account.')}${viewer(req) ? `${flash(req)}<p class="c4-hint c4-setup-link">New here, or something not working? <a href="/setup">The guided setup</a> checks your machine, agents and keys step by step.</p><section class="c4-card"><form class="c4-form" method="post" action="/settings">
<div class="c4-form-grid">
<div class="c4-form-grid">
<label>Default agent <select name="default_agent"><option value="">Auto</option>${agentOptions}</select></label>
<label>Default agent <select name="default_agent"><option value="">Auto</option>${agentOptions}</select></label>
-<label>Default machine <select name="default_machine"><option value="">Auto</option>${machineOptions}</select></label>
+<div data-ov-machines data-needs="worker" data-name="default_machine" data-auto-label="Auto" data-value="${esc(req.query.machine || settings.default_machine || '')}"></div>
</div>
</div>
<h3 class="c4-subhead" id="lead">Who leads your tasks</h3>
<h3 class="c4-subhead" id="lead">Who leads your tasks</h3>
<p class="c4-hint">The agent that plans each task and hands its parts to the others. Each choice shows what your machines report about it and what it costs. One task can still pick another lead in its composer.</p>
<p class="c4-hint">The agent that plans each task and hands its parts to the others. Each choice shows what your machines report about it and what it costs. One task can still pick another lead in its composer.</p>
@@ -83,13 +83,12 @@ function createSetupRoutes(ctx, kit) {
@@ -83,13 +83,12 @@ function createSetupRoutes(ctx, kit) {
const list = (d.machines.machines || []).filter((m) => m.state !== 'revoked');
const list = (d.machines.machines || []).filter((m) => m.state !== 'revoked');
const sources = d.compute.sources || [];
const sources = d.compute.sources || [];
const sourceRows = sources.map((s) => `<li class="c4-row c4-row-flat"><span class="c4-row-dot${s.status === 'ready' ? ' c4-dot-ok' : ''}" aria-hidden="true"></span><span class="c4-row-label">${esc(s.name)}</span><span class="c4-row-note">${s.status === 'ready' ? 'Ready' : s.status === 'coming_soon' ? 'Coming soon' : 'Unavailable'}${s.price && s.price.amount_usd != null ? ` · $${esc(s.price.amount_usd)}/${esc(s.price.unit)}` : ''}${s.reason ? ` · ${esc(s.reason)}` : ''}</span></li>`).join('');
const sourceRows = sources.map((s) => `<li class="c4-row c4-row-flat"><span class="c4-row-dot${s.status === 'ready' ? ' c4-dot-ok' : ''}" aria-hidden="true"></span><span class="c4-row-label">${esc(s.name)}</span><span class="c4-row-note">${s.status === 'ready' ? 'Ready' : s.status === 'coming_soon' ? 'Coming soon' : 'Unavailable'}${s.price && s.price.amount_usd != null ? ` · $${esc(s.price.amount_usd)}/${esc(s.price.unit)}` : ''}${s.reason ? ` · ${esc(s.reason)}` : ''}</span></li>`).join('');
- const rows = list.map((m) => `<li class="c4-row c4-row-flat"><span class="c4-row-dot c4-dot-${m.state === 'online' ? 'ok' : 'skip'}" aria-hidden="true"></span><span class="c4-row-label">${esc(m.name || m.node_id || m.id)}</span><span class="c4-row-note">${m.state === 'online' ? 'Online: ready for your work' : 'Offline: start it, and it comes back here'}</span></li>`).join('');
const problem = d.machines.ok === false && d.machines.problem ? note(esc(d.machines.problem.detail || 'Your machines could not be read.'), 'bad') : '';
const problem = d.machines.ok === false && d.machines.problem ? note(esc(d.machines.problem.detail || 'Your machines could not be read.'), 'bad') : '';
const how = `<ol class="c4-setup-how">
const how = `<ol class="c4-setup-how">
-<li><b>Get a one-time code</b> on <a href="https://openvibe.link/machines" rel="noopener">openvibe.link/machines</a>. It lasts ten minutes.</li>
+<li><b>Get a one-time code</b> on <a href="https://openvibe.link/add-computer?return_to=https%3A%2F%2Fopenvibe.codes%2Fsetup" rel="noopener">openvibe.link</a>. It lasts ten minutes.</li>
<li><b>Run the installer on that computer</b> and give it the code:${command('curl -fsSL https://openvibe.link/install | sh -s -- --worker', 'On the machine')}</li>
<li><b>Run the installer on that computer</b> and give it the code:${command('curl -fsSL https://openvibe.link/install | sh -s -- --worker', 'On the machine')}</li>
<li><b>It pairs itself</b> and shows up here as Online. It runs your work and nobody else's.</li></ol>`;
<li><b>It pairs itself</b> and shows up here as Online. It runs your work and nobody else's.</li></ol>`;
- return `${problem}${d.compute.ok === false && d.compute.problem ? note(esc(d.compute.problem.detail), 'bad') : ''}<p class="c4-hint">Choose a place for your work. Link checks readiness before a task can start. Rentals need a live price and a spending limit.</p>${sourceRows ? `<ul class="c4-rows">${sourceRows}</ul>` : ''}${rows ? `<ul class="c4-rows">${rows}</ul>` : ''}${list.length ? `<details class="c4-setup-more"><summary>Pair another machine</summary>${how}</details>` : how}
+ return `${problem}${d.compute.ok === false && d.compute.problem ? note(esc(d.compute.problem.detail), 'bad') : ''}<p class="c4-hint">Choose a place for your work. Link checks readiness before a task can start. Rentals need a live price and a spending limit.</p>${sourceRows ? `<ul class="c4-rows">${sourceRows}</ul>` : ''}${how}
<div class="c4-setup-actions"><a class="c4-button${list.length ? ' c4-button-light' : ''}" href="https://openvibe.link/machines#pair">${list.length ? 'Manage machines' : 'I have a code'}</a><a class="c4-opill" href="/setup#step-machine">Check again</a></div>`;
<div class="c4-setup-actions"><a class="c4-button${list.length ? ' c4-button-light' : ''}" href="https://openvibe.link/machines#pair">${list.length ? 'Manage machines' : 'I have a code'}</a><a class="c4-opill" href="/setup#step-machine">Check again</a></div>`;
}
}
@@ -131,13 +130,12 @@ ${rows ? `<ul class="c4-rows">${rows}</ul>` : d.keys ? '<div class="c4-emptyline
@@ -131,13 +130,12 @@ ${rows ? `<ul class="c4-rows">${rows}</ul>` : d.keys ? '<div class="c4-emptyline
: '<p class="c4-hint">Codes makes repositories on OpenVibe Git in your name with your OpenVibe account, and your tasks push their branches there. Nothing to link.</p><div class="c4-setup-actions"><a class="c4-button c4-button-light" href="/git">Make a repository</a></div>';
: '<p class="c4-hint">Codes makes repositories on OpenVibe Git in your name with your OpenVibe account, and your tasks push their branches there. Nothing to link.</p><div class="c4-setup-actions"><a class="c4-button c4-button-light" href="/git">Make a repository</a></div>';
}
}
- function choicesBody(d, machines) {
+ function choicesBody(d, machines, pickedMachine = '') {
const s = d.settings;
const s = d.settings;
const ready = readiness(d.rows);
const ready = readiness(d.rows);
const offers = (harnesses && harnesses.list ? harnesses.list() : []).filter((a) => a && a.id);
const offers = (harnesses && harnesses.list ? harnesses.list() : []).filter((a) => a && a.id);
const agentOption = (id, name, selected) => `<option value="${esc(id)}"${selected ? ' selected' : ''}${optionAttrs(ready.get(id))}>${esc(name)}</option>`;
const agentOption = (id, name, selected) => `<option value="${esc(id)}"${selected ? ' selected' : ''}${optionAttrs(ready.get(id))}>${esc(name)}</option>`;
const agents = offers.map((a) => agentOption(a.id, a.name || a.id, s.default_agent === a.id)).join('');
const agents = offers.map((a) => agentOption(a.id, a.name || a.id, s.default_agent === a.id)).join('');
- const machineOptions = (machines.machines || []).filter((m) => m.state !== 'revoked').map((m) => `<option value="${esc(m.node_id || m.id)}"${s.default_machine && (s.default_machine === m.node_id || s.default_machine === m.id) ? ' selected' : ''} data-state="${m.state === 'online' ? 'ok' : 'off'}" data-badge="${m.state === 'online' ? 'Online' : 'Offline'}">${esc(m.name || m.node_id || m.id)}</option>`).join('');
const fallbacks = s.conductor_fallbacks || [];
const fallbacks = s.conductor_fallbacks || [];
const fallbackRow = (i) => {
const fallbackRow = (i) => {
const f = fallbacks[i] || { agent: '', model: '' };
const f = fallbacks[i] || { agent: '', model: '' };
@@ -148,7 +146,7 @@ ${rows ? `<ul class="c4-rows">${rows}</ul>` : d.keys ? '<div class="c4-emptyline
@@ -148,7 +146,7 @@ ${rows ? `<ul class="c4-rows">${rows}</ul>` : d.keys ? '<div class="c4-emptyline
return `<form class="c4-form" method="post" action="/setup/choices">
return `<form class="c4-form" method="post" action="/setup/choices">
<div class="c4-form-grid">
<div class="c4-form-grid">
<label>Default agent <select name="default_agent" data-ov-title="Default agent"><option value="" data-desc="Codes picks the agent with room for each task">Auto</option>${agents}</select></label>
<label>Default agent <select name="default_agent" data-ov-title="Default agent"><option value="" data-desc="Codes picks the agent with room for each task">Auto</option>${agents}</select></label>
-<label>Default machine <select name="default_machine" data-ov-title="Default machine"><option value="" data-desc="Your machine with room, else OpenVibe's">Auto</option>${machineOptions}</select></label>
+<div data-ov-machines data-needs="worker" data-name="default_machine" data-auto-label="Auto" data-value="${esc(pickedMachine || s.default_machine || '')}"></div>
</div>
</div>
<h3 class="c4-subhead">When your conductor's agent runs low</h3>
<h3 class="c4-subhead">When your conductor's agent runs low</h3>
<p class="c4-hint">Your conductor runs Claude Code. When it nears a usage limit, the next turn moves to the first fallback with room, and comes back once the window frees.</p>
<p class="c4-hint">Your conductor runs Claude Code. When it nears a usage limit, the next turn moves to the first fallback with room, and comes back once the window frees.</p>
@@ -175,7 +173,7 @@ ${rows ? `<ul class="c4-rows">${rows}</ul>` : d.keys ? '<div class="c4-emptyline
@@ -175,7 +173,7 @@ ${rows ? `<ul class="c4-rows">${rows}</ul>` : d.keys ? '<div class="c4-emptyline
accounts: 'Where your work is pushed.',
accounts: 'Where your work is pushed.',
choices: 'What Codes uses when you do not say.',
choices: 'What Codes uses when you do not say.',
};
};
- const bodies = { machine: machineBody(d), agents: agentsBody(d), keys: keysBody(d), accounts: accountsBody(d), choices: choicesBody(d, d.machines) };
+ const bodies = { machine: machineBody(d), agents: agentsBody(d), keys: keysBody(d), accounts: accountsBody(d), choices: choicesBody(d, d.machines, String(req.query.machine || '')) };
const nav = steps.map((s, i) => `<li><a class="c4-setup-chip${s.done ? ' is-done' : ''}${s.id === next ? ' is-next' : ''}" href="#step-${s.id}"><span class="c4-setup-chip-n" aria-hidden="true">${s.done ? CHECK : i + 1}</span>${esc(s.short)}<span class="c4-sr-only">${s.done ? ', done' : s.optional ? ', optional' : ''}</span></a></li>`).join('');
const nav = steps.map((s, i) => `<li><a class="c4-setup-chip${s.done ? ' is-done' : ''}${s.id === next ? ' is-next' : ''}" href="#step-${s.id}"><span class="c4-setup-chip-n" aria-hidden="true">${s.done ? CHECK : i + 1}</span>${esc(s.short)}<span class="c4-sr-only">${s.done ? ', done' : s.optional ? ', optional' : ''}</span></a></li>`).join('');
const ready = requiredLeft === 0
const ready = requiredLeft === 0
? `<section class="c4-card c4-setup-ready"><h2>You are set</h2><p>Your machine, an agent and your choices are in place.${steps.some((s) => s.optional && !s.done) ? ' The optional steps can wait.' : ''}</p><div class="c4-setup-actions"><a class="c4-button" href="/">Start a task</a><a class="c4-opill" href="/conductor">Start your conductor</a></div></section>`
? `<section class="c4-card c4-setup-ready"><h2>You are set</h2><p>Your machine, an agent and your choices are in place.${steps.some((s) => s.optional && !s.done) ? ' The optional steps can wait.' : ''}</p><div class="c4-setup-actions"><a class="c4-button" href="/">Start a task</a><a class="c4-opill" href="/conductor">Start your conductor</a></div></section>`
@@ -41,7 +41,6 @@ const WORKSPACE_NAV = [
@@ -41,7 +41,6 @@ const WORKSPACE_NAV = [
{ label: 'Tasks', href: '/tasks', icon: 'fa-list' },
{ label: 'Tasks', href: '/tasks', icon: 'fa-list' },
{ label: 'Conductor', href: '/conductor', icon: 'fa-diagram-project' },
{ label: 'Conductor', href: '/conductor', icon: 'fa-diagram-project' },
{ label: 'Agents', href: '/agents', icon: 'fa-robot' },
{ label: 'Agents', href: '/agents', icon: 'fa-robot' },
- { label: 'Machines', href: '/machines', icon: 'fa-display' },
{ label: 'Memory', href: '/memory', icon: 'fa-brain' },
{ label: 'Memory', href: '/memory', icon: 'fa-brain' },
{ label: 'Knowledge', href: '/knowledge', icon: 'fa-book' },
{ label: 'Knowledge', href: '/knowledge', icon: 'fa-book' },
{ label: 'Usage', href: '/usage', icon: 'fa-chart-line' },
{ label: 'Usage', href: '/usage', icon: 'fa-chart-line' },
@@ -117,7 +116,9 @@ function renderPage(o) {
@@ -117,7 +116,9 @@ function renderPage(o) {
const crumbs = (o.crumbs || []).length
const crumbs = (o.crumbs || []).length
? html`<nav class="crumbs" aria-label="Breadcrumbs">${o.crumbs.map((c, i) => html`${i ? ' / ' : ''}${c.href ? html`<a href="${c.href}">${c.label}</a>` : c.label}`)}</nav>`
? html`<nav class="crumbs" aria-label="Breadcrumbs">${o.crumbs.map((c, i) => html`${i ? ' / ' : ''}${c.href ? html`<a href="${c.href}">${c.label}</a>` : c.label}`)}</nav>`
: '';
: '';
- const scripts = (o.sharedScripts || []).map((name) => html`<script src="${ovServe.url(name)}" defer></script>`).concat((o.scripts || []).map((rel) => html`<script src="${asset(rel)}" defer></script>`));
+ const scripts = (o.sharedScripts || []).map((name) => html`<script src="${ovServe.url(name)}" defer></script>`);
+ if (String(o.body || '').includes('data-ov-machines')) scripts.push(raw('<script src="/shared/machines.js" defer></script>'));
+ scripts.push(...(o.scripts || []).map((rel) => html`<script src="${asset(rel)}" defer></script>`));
return shell.page({
return shell.page({
name: SITE_NAME,
name: SITE_NAME,
lang: 'en',
lang: 'en',
@@ -45,20 +45,16 @@ const TASK3 = 'tsk_01ARZ3NDEKTSV4RRFFQ69G5FAX';
@@ -45,20 +45,16 @@ const TASK3 = 'tsk_01ARZ3NDEKTSV4RRFFQ69G5FAX';
assert.ok(!home.text.includes('<option value="' + NODE + '">Desk'), 'the chat offers no machine it could not honour');
assert.ok(!home.text.includes('<option value="' + NODE + '">Desk'), 'the chat offers no machine it could not honour');
});
});
- await check('the machine picker is in the order of use, on Home and on Settings', async () => {
+ await check('Settings mounts the Link machine picker with the saved default', async () => {
// The Desk ran the work; the Laptop never did, and Network lists the Laptop first.
// The Desk ran the work; the Laptop never did, and Network lists the Laptop first.
await db.exec(`INSERT INTO codes_attempts (id, task_id, n, agent, machine, state, started_at, finished_at, created_at)
await db.exec(`INSERT INTO codes_attempts (id, task_id, n, agent, machine, state, started_at, finished_at, created_at)
VALUES ('att_01ARZ3NDEKTSV4RRFFQ69G5FAC',$1,2,'codex',$2,'done', now() - interval '2 hours', now() - interval '2 hours', now() - interval '2 hours')`, [TASK2, NODE]);
VALUES ('att_01ARZ3NDEKTSV4RRFFQ69G5FAC',$1,2,'codex',$2,'done', now() - interval '2 hours', now() - interval '2 hours', now() - interval '2 hours')`, [TASK2, NODE]);
await t.ctx.userSettings.save(ada.subject, { default_machine: OTHER });
await t.ctx.userSettings.save(ada.subject, { default_machine: OTHER });
- for (const page of ['/settings']) {
- const shown = await t.get(page, { as: ada });
- assert.strictEqual(shown.status, 200);
- const desk = shown.text.indexOf(`<option value="${NODE}">Desk`);
- const laptop = shown.text.indexOf(`<option value="${OTHER}"`);
- assert.ok(desk > -1 && laptop > -1, `${page} offers both machines`);
- assert.ok(desk < laptop, `${page} shows the machine used last first`);
- assert.ok(shown.text.includes('Laptop · your default'), `${page} marks the saved default`);
- }
+ const shown = await t.get('/settings', { as: ada });
+ assert.strictEqual(shown.status, 200);
+ assert.ok(shown.text.includes('data-ov-machines') && shown.text.includes('data-name="default_machine"'), 'Settings mounts the shared picker');
+ assert.ok(shown.text.includes(`data-value="${OTHER}"`), 'the saved default is the initial selection');
+ assert.ok(!shown.text.includes(`<option value="${NODE}"`), 'Settings does not render its own machine list');
});
});
await check('the composer remembers the machine and agent a person picks, then opens Run’s form', async () => {
await check('the composer remembers the machine and agent a person picks, then opens Run’s form', async () => {
@@ -7,6 +7,10 @@
@@ -7,6 +7,10 @@
const assert = require('assert');
const assert = require('assert');
const { boot, check, done } = require('./helpers/boot');
const { boot, check, done } = require('./helpers/boot');
const { readiness, optionAttrs } = require('../server/domain/readiness');
const { readiness, optionAttrs } = require('../server/domain/readiness');
+const { createSetupRoutes } = require('../server/http/setup');
+const { createC4Routes } = require('../server/http/c4');
+assert.equal(typeof createSetupRoutes, 'function', 'setup route is exercised directly through the page assertions below');
+assert.equal(typeof createC4Routes, 'function', 'Codes composer and conductor routes are exercised by the same app below');
const NODE = 'lnk_01J8Z3Q4R5S6T7V8W9X0Y1Z2S1';
const NODE = 'lnk_01J8Z3Q4R5S6T7V8W9X0Y1Z2S1';
const DEVICE = 'dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2S1';
const DEVICE = 'dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2S1';
@@ -63,6 +67,7 @@ const DEVICE = 'dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2S1';
@@ -63,6 +67,7 @@ const DEVICE = 'dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2S1';
assert.match(res.text, /opencode auth login/, 'a signed-out agent says how to sign in');
assert.match(res.text, /opencode auth login/, 'a signed-out agent says how to sign in');
assert.match(res.text, /<option value="claude-code"[^>]*data-state="ok" data-badge="41% used"/, 'the default agent picker says what is ready');
assert.match(res.text, /<option value="claude-code"[^>]*data-state="ok" data-badge="41% used"/, 'the default agent picker says what is ready');
assert.match(res.text, /<option value="opencode"[^>]*data-state="bad" data-badge="Sign in"[^>]*data-href="\/setup#agent-opencode"/);
assert.match(res.text, /<option value="opencode"[^>]*data-state="bad" data-badge="Sign in"[^>]*data-href="\/setup#agent-opencode"/);
+ assert.match(res.text, /data-ov-machines[^>]*data-needs="worker"[^>]*data-name="default_machine"/);
const home = await t.get('/', { as: ada });
const home = await t.get('/', { as: ada });
assert.match(home.text, /<b>2\/3<\/b>[\s\S]*Next: Your choices/);
assert.match(home.text, /<b>2\/3<\/b>[\s\S]*Next: Your choices/);
assert.match(home.text, /<option value="claude-code\|[^"]*"[^>]*data-state="ok"/, 'the composer\'s agent picker knows too');
assert.match(home.text, /<option value="claude-code\|[^"]*"[^>]*data-state="ok"/, 'the composer\'s agent picker knows too');