openvibers/openvibe.codes Commits
Conductor: the shared agent composer, the model as its chip (#428)
Changes
@@@ -9,11 +9,11 @@"domain": "openvibe.codes","contracts": "openvibe-contracts v0.406.0","sdk": "openvibe-sdk v0.150.0",-- "shared": "openvibe-shared v4.214.0",++ "shared": "openvibe-shared v4.215.0","packages": ["openvibe-contracts v0.406.0","openvibe-sdk v0.150.0",-- "openvibe-shared v4.214.0"++ "openvibe-shared v4.215.0"],"summary": "Public at https://openvibe.codes (sudo ovhost deploy codes; unit openvibe-codes on 127.0.0.1:4900). Since 2026-10-08 Codes is the open coding-agent harness: openvibe-codes (a command-line tool and Node.js module, v0.5.2) runs a coding task on Claude Code, Claude Code on another provider, Codex, OpenCode, Command Code, MiniMax Code, Aider, the DeepSeek API or a person's own OpenAI-compatible model on their machine, hands it to the next agent when one fails or goes quiet, and keeps every run; the site carries the agent catalog with a public routing API, getting started, Improve OpenVibe and the community documents. The developer console that used to live here moved to OpenVibe.Services (https://openvibe.services) and every one of its addresses answers a permanent redirect there.","works": [@@@ -17,7 +17,7 @@"iovalkey": "^0.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.406.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.150.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.214.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.215.0","pg": "^8.23.0","yaml": "^2.9.1"},@@@ -1275,9 +1275,9 @@}},"node_modules/openvibe-shared": {-- "version": "4.214.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.214.0",-- "integrity": "sha512-HvaqQOZLLFEgwDaCkrhJFfwQfsMIlOmV05PhO796JFmC7A1NScxOqp86djT6sYsK1pARMqeo0kTua5mAdXiQFw==",++ "version": "4.215.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.215.0",++ "integrity": "sha512-2hXCSsMQieDuYtdcr4KEhBmrM0rzw10T8TT/edGSsoXXQCf4bmqMWp7iPBR3NYaJgiGATcnNFdW931AbtGhfAw==","license": "MIT","dependencies": {"clean-css": "^5.3.3",@@@ -32,7 +32,7 @@"iovalkey": "^0.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.406.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.150.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.214.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.215.0","pg": "^8.23.0","yaml": "^2.9.1"},@@@ -232,7 +232,7 @@const text = (form.querySelector('textarea[name="text"]') || {}).value || '';const res = await fetch(form.dataset.conductorApi, {method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' },-- credentials: 'same-origin', body: JSON.stringify({ text, attachments: [] }),++ credentials: 'same-origin', body: JSON.stringify({ text, attachments: [], who: (form.querySelector('select[name="who"]') || {}).value || undefined }),});const doc = await res.json().catch(() => ({}));if (!res.ok) throw new Error(doc.detail || `the message was refused (${res.status})`);nnew file mode 100644@@@ -0,0 +1,38 @@++/* The conductor's message box on the shared composer (openvibe-shared 4.215.0, OpenVibeComposer.mount, preset++ agent): the session form keeps its action, upload picker, status lines and no-JS post; its model becomes the mode++ chip (the form's `who`), and c4.js still drives the send. Without ov-composer.js the server's box stays. */++(function () {++ 'use strict';++ function upgrade(form) {++ if (!form || form.__ovcComposer || !window.OpenVibeComposer || !window.OpenVibeComposer.mount) return;++ var ta = form.querySelector('#c4-message, textarea[name="text"], textarea');++ if (!ta) return;++ var options = [];++ try { options = JSON.parse(form.getAttribute('data-c4-model-options') || '[]'); } catch (e) { options = []; }++ var value = form.getAttribute('data-c4-model-value') || '';++ var c = window.OpenVibeComposer.mount(document.createElement('div'), {++ preset: 'agent', name: ta.name || 'text', maxLength: 16000,++ placeholder: ta.placeholder || 'Tell your conductor what to do next', label: 'Message the conductor',++ submitLabel: 'Send', tools: [],++ mode: options.length ? { name: 'who', label: 'Model', options: options, value: value } : null,++ });++ if (!c) return;++ c.text.id = 'c4-message';++ var anchor = ta.closest('label') || ta;++ while (c.form.firstChild) anchor.parentNode.insertBefore(c.form.firstChild, anchor);++ anchor.remove();++ var old = form.querySelector('.c4-conductor-actions button[type="submit"]');++ if (old) old.remove();++ form.classList.add('ovt-composer', 'ovc-v2');++ form.dataset.preset = 'agent';++ c.text.addEventListener('keydown', function (e) {++ if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); if (form.requestSubmit) form.requestSubmit(); else form.submit(); }++ });++ form.__ovcComposer = c;++ }++ function scan() {++ Array.prototype.forEach.call(document.querySelectorAll('form[data-ovc-compose]'), upgrade);++ }++ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', scan, { once: true }); else scan();++ window.OVCodesComposer = { scan: scan, upgrade: upgrade };++})();@@@ -381,9 +381,15 @@ ${pr.problem ? `<p class="c4-hint">${esc(pr.problem.detail)}</p>` : `<dl class="const outcome = row.outcome === 'unfinished' || row.outcome === 'stopped'? `<div class="c4-outcome"><span class="c4-obadge" aria-hidden="true">${icon('<path d="m18 6-12 12M6 6l12 12"/>')}</span><div class="c4-outcome-copy"><b>${row.outcome === 'stopped' ? 'Stopped' : 'The last turn did not finish'}</b><span>${esc(row.outcome_note || row.error || 'Write a message to carry on.')}</span></div></div>` : '';const transcript = `<section class="c4-card c4-transcript" data-c4-conductor-stream="${esc(`${api}/events?last_event_id=${lastSeq}`)}" data-task="${esc(row.id)}" data-started-at="${esc(row.started_at || row.created_at || '')}" data-c4-conductor="${live ? 'live' : 'idle'}"><div class="c4-card-head"><h2>${live ? 'This turn, live' : 'The last turn'}</h2><span class="c4-muted" data-live-label>${live ? 'Connecting…' : 'Nothing is running'}</span></div><pre class="c4-log" data-task-log aria-live="polite" tabindex="0">${esc(lastLines)}</pre></section>`;-- const composer = row.outcome === 'stopped' ? '' : `<section class="c4-card c4-conductor-composer"><form method="post" action="/tasks/${encodeURIComponent(row.id)}/messages" enctype="multipart/form-data" data-c4-conductor-form data-conductor-api="/api/v1/conductor/${encodeURIComponent(row.id)}/messages"><label for="c4-message">${live ? 'Say something: it is the next turn' : 'What next?'}</label><textarea id="c4-message" name="text" rows="3" placeholder="Tell your conductor what to do next, or ask what it is doing"></textarea><div class="c4-conductor-actions"><div data-c4-upload></div><noscript><label class="c4-file">Attach files<input type="file" name="files" multiple></label></noscript><button class="c4-button" type="submit">Send</button></div><p class="c4-hint">Up to 10 files, 20 MB each, 50 MB together. Programs cannot be attached.</p><div data-c4-upload-progress></div><p class="c4-hint" data-c4-message-status role="status">${live ? 'A message waits until the current turn ends; it is not lost.' : ''}</p></form></section>`;++ // The session's message box is the shared composer (openvibe-shared 4.215.0, preset agent): its model is the++ // mode chip, so the picker's who goes with the form and the JSON send, and the turn runs on the model chosen.++ const modelOptions = row.agent === 'claude-code'++ ? MODELS.map(([id, label]) => [`${row.agent}|${id}`, label])++ : [[`${row.agent}|${row.model || ''}`, row.model || 'its default model']];++ const whoValue = `${row.agent}|${row.model || ''}`;++ const composer = row.outcome === 'stopped' ? '' : `<section class="c4-card c4-conductor-composer"><form method="post" action="/tasks/${encodeURIComponent(row.id)}/messages" enctype="multipart/form-data" data-c4-conductor-form data-ovc-compose data-ovc-preset="agent" data-c4-model-options="${esc(JSON.stringify(modelOptions))}" data-c4-model-value="${esc(whoValue)}" data-conductor-api="/api/v1/conductor/${encodeURIComponent(row.id)}/messages"><label for="c4-message">${live ? 'Say something: it is the next turn' : 'What next?'}</label><textarea id="c4-message" name="text" rows="3" maxlength="16000" placeholder="Tell your conductor what to do next, or ask what it is doing"></textarea><div class="c4-conductor-actions"><div data-c4-upload></div><noscript><label class="c4-file">Attach files<input type="file" name="files" multiple></label></noscript><button class="c4-button" type="submit">Send</button></div><p class="c4-hint">Up to 10 files, 20 MB each, 50 MB together. Programs cannot be attached.</p><div data-c4-upload-progress></div><p class="c4-hint" data-c4-message-status role="status">${live ? 'A message waits until the current turn ends; it is not lost.' : ''}</p></form></section>`;const content = `${outcome}<section class="c4-card c4-conductor-head"><div class="c4-task-heading"><div class="c4-task-title-wrap"><h1>Conductor</h1></div><span class="c4-status c4-${esc(row.state)}">${esc(conductorState(row))}</span></div><div class="c4-chips" aria-label="Conductor details"><span class="c4-chip">${esc(row.folder || 'a folder')}</span><span class="c4-chip">${esc(row.agent === 'claude-code' ? modelLabel(row.model) : (row.model || 'its default model'))}</span><span class="c4-chip">safety: ${esc(row.safety || 'ask')}</span><span class="c4-chip">${esc(machineLabel || 'your machine')}</span></div><p class="c4-hint">One long session on your own machine, on the agent you chose and your own login. Each message is the next turn of the same conversation; it hands the work to your other agents through the Codes tools.</p></section>${approvals}${usageCard}<section class="c4-card c4-turns"><div class="c4-card-head"><h2>Turns</h2><span class="c4-muted">${(view.messages || []).length} so far</span></div>${turns ? `<ol class="c4-turn-list">${turns}</ol>` : '<div class="c4-emptyline">Its first turn is starting.</div>'}</section>${transcript}${composer}`;-- page(req, res, 'Conductor', wrap(content), { sharedScripts: ['agent-session.js'] });++ page(req, res, 'Conductor', wrap(content), { sharedScripts: ['agent-session.js', 'ov-composer.js'], scripts: ['js/composer.js'] });}// ── The front door (a visitor): what Codes is, shown rather than told ──nnew file mode 100644@@@ -0,0 +1,55 @@++'use strict';++// The conductor's message box on the shared composer (openvibe-shared 4.215.0, OpenVibeComposer.mount, preset++// agent): the session form keeps its action, uploader and status line; the model is the mode chip; the box sends.++// Real Chrome at 390 px; skipped without Chrome.++const assert = require('assert');++const fs = require('fs');++const os = require('os');++const path = require('path');++const http = require('http');++const h = require('openvibe-shared/browser-harness');++if (process.env.OV_SKIP_BROWSER === '1' || !h.findChrome()) { console.log('composer: skipped (no Chrome; set CHROME_BIN)'); process.exit(0); }++const SHARED = process.env.OV_SHARED_DIR || path.join(__dirname, '..', 'node_modules', 'openvibe-shared');++const SITE = path.join(__dirname, '..');++const form = `<section class="c4-card c4-conductor-composer"><form method="post" action="/tasks/t1/messages" enctype="multipart/form-data" data-c4-conductor-form data-ovc-compose data-ovc-preset="agent" data-c4-model-options='[["claude-code|opus","Opus 5.5"],["claude-code|sonnet","Sonnet 5.5"]]' data-c4-model-value="claude-code|opus"><label for="c4-message">What next?</label><textarea id="c4-message" name="text" rows="3" maxlength="16000" placeholder="Tell your conductor what to do next"></textarea><div class="c4-conductor-actions"><div data-c4-upload></div><button class="c4-button" type="submit">Send</button></div><p class="c4-hint" data-c4-message-status role="status"></p></form></section>`;++let posted = '';++const server = http.createServer((req, res) => {++ if (req.method === 'POST') {++ let body = '';++ req.on('data', (d) => { body += d; });++ req.on('end', () => { posted = body; res.writeHead(200, { 'content-type': 'text/html' }); res.end('<!doctype html><p id="posted">posted</p>'); });++ return;++ }++ if (req.url === '/') {++ res.writeHead(200, { 'content-type': 'text/html' });++ return res.end(`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Conductor</title><link rel="icon" href="data:,"></head><body style="margin:0;padding:12px;background:#0b1220;color:#e6edf7;font-family:sans-serif">${form}<script src="/ov-composer.js"></script><script src="/composer.js"></script></body></html>`);++ }++ if (req.url === '/ov-composer.js') { res.writeHead(200, { 'content-type': 'application/javascript' }); return res.end(fs.readFileSync(path.join(SHARED, 'ov-composer.js'))); }++ if (req.url === '/composer.js') { res.writeHead(200, { 'content-type': 'application/javascript' }); return res.end(fs.readFileSync(path.join(SITE, 'public/js/composer.js'))); }++ res.writeHead(404); res.end();++});++(async () => {++ await new Promise((ok) => server.listen(0, '127.0.0.1', ok));++ const base = `http://127.0.0.1:${server.address().port}`;++ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codes-composer-'));++ const browser = await h.launch({ tmpDir });++ const wait = (ms) => new Promise((r) => setTimeout(r, ms));++ const page = await h.openPage(browser, await browser.newContext(), { width: 390, height: 900 });++ try {++ await page.goto(base + '/', { maxMs: 6000 });++ for (let i = 0; i < 100 && !(await page.evaluate(`!!document.querySelector('form.ovc-v2')`)); i++) await wait(40);++ const shape = await page.evaluate(`(() => { const f = document.querySelector('form[data-c4-conductor-form]'); return { preset: f.dataset.preset, action: f.getAttribute('action'), text: f.querySelector('#c4-message[data-ovt-text]') ? f.querySelector('#c4-message').value : null, chip: f.querySelector('.ovc-chip-v') ? f.querySelector('.ovc-chip-v').textContent : null, who: f.querySelector('select[name=who]').value, upload: !!f.querySelector('[data-c4-upload]'), status: !!f.querySelector('[data-c4-message-status]'), old: !!f.querySelector('.c4-conductor-actions button[type=submit]') }; })()`);++ assert.deepEqual(shape, { preset: 'agent', action: '/tasks/t1/messages', text: '', chip: 'Opus 5.5', who: 'claude-code|opus', upload: true, status: true, old: false });++ assert.equal(await page.evaluate(`document.documentElement.scrollWidth`), 390, 'nothing is wider than a phone');++ await page.evaluate(`(() => { const f = document.querySelector('form.ovc-v2'); const t = f.querySelector('textarea'); t.value = 'Carry on'; t.dispatchEvent(new Event('input', { bubbles: true })); f.querySelector('select[name=who]').value = 'claude-code|sonnet'; f.querySelector('[data-ovt-send]').click(); })()`);++ for (let i = 0; i < 100 && !posted; i++) await wait(40);++ assert.match(posted, /Carry on/);++ assert.match(posted, /claude-code\|sonnet/);++ console.log('composer: the conductor is the shared agent composer, its model is the chip, sends, 390 px');++ } finally {++ await page.close().catch(() => {});++ await browser.close();++ server.close();++ fs.rmSync(tmpDir, { recursive: true, force: true });++ }++})().catch((e) => { console.error(e); process.exit(1); });
@@ -9,11 +9,11 @@
@@ -9,11 +9,11 @@
"domain": "openvibe.codes",
"domain": "openvibe.codes",
"contracts": "openvibe-contracts v0.406.0",
"contracts": "openvibe-contracts v0.406.0",
"sdk": "openvibe-sdk v0.150.0",
"sdk": "openvibe-sdk v0.150.0",
- "shared": "openvibe-shared v4.214.0",
+ "shared": "openvibe-shared v4.215.0",
"packages": [
"packages": [
"openvibe-contracts v0.406.0",
"openvibe-contracts v0.406.0",
"openvibe-sdk v0.150.0",
"openvibe-sdk v0.150.0",
- "openvibe-shared v4.214.0"
+ "openvibe-shared v4.215.0"
],
],
"summary": "Public at https://openvibe.codes (sudo ovhost deploy codes; unit openvibe-codes on 127.0.0.1:4900). Since 2026-10-08 Codes is the open coding-agent harness: openvibe-codes (a command-line tool and Node.js module, v0.5.2) runs a coding task on Claude Code, Claude Code on another provider, Codex, OpenCode, Command Code, MiniMax Code, Aider, the DeepSeek API or a person's own OpenAI-compatible model on their machine, hands it to the next agent when one fails or goes quiet, and keeps every run; the site carries the agent catalog with a public routing API, getting started, Improve OpenVibe and the community documents. The developer console that used to live here moved to OpenVibe.Services (https://openvibe.services) and every one of its addresses answers a permanent redirect there.",
"summary": "Public at https://openvibe.codes (sudo ovhost deploy codes; unit openvibe-codes on 127.0.0.1:4900). Since 2026-10-08 Codes is the open coding-agent harness: openvibe-codes (a command-line tool and Node.js module, v0.5.2) runs a coding task on Claude Code, Claude Code on another provider, Codex, OpenCode, Command Code, MiniMax Code, Aider, the DeepSeek API or a person's own OpenAI-compatible model on their machine, hands it to the next agent when one fails or goes quiet, and keeps every run; the site carries the agent catalog with a public routing API, getting started, Improve OpenVibe and the community documents. The developer console that used to live here moved to OpenVibe.Services (https://openvibe.services) and every one of its addresses answers a permanent redirect there.",
"works": [
"works": [
@@ -17,7 +17,7 @@
@@ -17,7 +17,7 @@
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.406.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.406.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.150.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.150.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.214.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.215.0",
"pg": "^8.23.0",
"pg": "^8.23.0",
"yaml": "^2.9.1"
"yaml": "^2.9.1"
},
},
@@ -1275,9 +1275,9 @@
@@ -1275,9 +1275,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "4.214.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.214.0",
- "integrity": "sha512-HvaqQOZLLFEgwDaCkrhJFfwQfsMIlOmV05PhO796JFmC7A1NScxOqp86djT6sYsK1pARMqeo0kTua5mAdXiQFw==",
+ "version": "4.215.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.215.0",
+ "integrity": "sha512-2hXCSsMQieDuYtdcr4KEhBmrM0rzw10T8TT/edGSsoXXQCf4bmqMWp7iPBR3NYaJgiGATcnNFdW931AbtGhfAw==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"clean-css": "^5.3.3",
"clean-css": "^5.3.3",
@@ -32,7 +32,7 @@
@@ -32,7 +32,7 @@
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.406.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.406.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.150.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.150.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.214.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.215.0",
"pg": "^8.23.0",
"pg": "^8.23.0",
"yaml": "^2.9.1"
"yaml": "^2.9.1"
},
},
@@ -232,7 +232,7 @@
@@ -232,7 +232,7 @@
const text = (form.querySelector('textarea[name="text"]') || {}).value || '';
const text = (form.querySelector('textarea[name="text"]') || {}).value || '';
const res = await fetch(form.dataset.conductorApi, {
const res = await fetch(form.dataset.conductorApi, {
method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
- credentials: 'same-origin', body: JSON.stringify({ text, attachments: [] }),
+ credentials: 'same-origin', body: JSON.stringify({ text, attachments: [], who: (form.querySelector('select[name="who"]') || {}).value || undefined }),
});
});
const doc = await res.json().catch(() => ({}));
const doc = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(doc.detail || `the message was refused (${res.status})`);
if (!res.ok) throw new Error(doc.detail || `the message was refused (${res.status})`);
new file mode 100644
new file mode 100644
@@ -0,0 +1,38 @@
@@ -0,0 +1,38 @@
+/* The conductor's message box on the shared composer (openvibe-shared 4.215.0, OpenVibeComposer.mount, preset
+ agent): the session form keeps its action, upload picker, status lines and no-JS post; its model becomes the mode
+ chip (the form's `who`), and c4.js still drives the send. Without ov-composer.js the server's box stays. */
+(function () {
+ 'use strict';
+ function upgrade(form) {
+ if (!form || form.__ovcComposer || !window.OpenVibeComposer || !window.OpenVibeComposer.mount) return;
+ var ta = form.querySelector('#c4-message, textarea[name="text"], textarea');
+ if (!ta) return;
+ var options = [];
+ try { options = JSON.parse(form.getAttribute('data-c4-model-options') || '[]'); } catch (e) { options = []; }
+ var value = form.getAttribute('data-c4-model-value') || '';
+ var c = window.OpenVibeComposer.mount(document.createElement('div'), {
+ preset: 'agent', name: ta.name || 'text', maxLength: 16000,
+ placeholder: ta.placeholder || 'Tell your conductor what to do next', label: 'Message the conductor',
+ submitLabel: 'Send', tools: [],
+ mode: options.length ? { name: 'who', label: 'Model', options: options, value: value } : null,
+ });
+ if (!c) return;
+ c.text.id = 'c4-message';
+ var anchor = ta.closest('label') || ta;
+ while (c.form.firstChild) anchor.parentNode.insertBefore(c.form.firstChild, anchor);
+ anchor.remove();
+ var old = form.querySelector('.c4-conductor-actions button[type="submit"]');
+ if (old) old.remove();
+ form.classList.add('ovt-composer', 'ovc-v2');
+ form.dataset.preset = 'agent';
+ c.text.addEventListener('keydown', function (e) {
+ if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); if (form.requestSubmit) form.requestSubmit(); else form.submit(); }
+ });
+ form.__ovcComposer = c;
+ }
+ function scan() {
+ Array.prototype.forEach.call(document.querySelectorAll('form[data-ovc-compose]'), upgrade);
+ }
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', scan, { once: true }); else scan();
+ window.OVCodesComposer = { scan: scan, upgrade: upgrade };
+})();
@@ -381,9 +381,15 @@ ${pr.problem ? `<p class="c4-hint">${esc(pr.problem.detail)}</p>` : `<dl class="
@@ -381,9 +381,15 @@ ${pr.problem ? `<p class="c4-hint">${esc(pr.problem.detail)}</p>` : `<dl class="
const outcome = row.outcome === 'unfinished' || row.outcome === 'stopped'
const outcome = row.outcome === 'unfinished' || row.outcome === 'stopped'
? `<div class="c4-outcome"><span class="c4-obadge" aria-hidden="true">${icon('<path d="m18 6-12 12M6 6l12 12"/>')}</span><div class="c4-outcome-copy"><b>${row.outcome === 'stopped' ? 'Stopped' : 'The last turn did not finish'}</b><span>${esc(row.outcome_note || row.error || 'Write a message to carry on.')}</span></div></div>` : '';
? `<div class="c4-outcome"><span class="c4-obadge" aria-hidden="true">${icon('<path d="m18 6-12 12M6 6l12 12"/>')}</span><div class="c4-outcome-copy"><b>${row.outcome === 'stopped' ? 'Stopped' : 'The last turn did not finish'}</b><span>${esc(row.outcome_note || row.error || 'Write a message to carry on.')}</span></div></div>` : '';
const transcript = `<section class="c4-card c4-transcript" data-c4-conductor-stream="${esc(`${api}/events?last_event_id=${lastSeq}`)}" data-task="${esc(row.id)}" data-started-at="${esc(row.started_at || row.created_at || '')}" data-c4-conductor="${live ? 'live' : 'idle'}"><div class="c4-card-head"><h2>${live ? 'This turn, live' : 'The last turn'}</h2><span class="c4-muted" data-live-label>${live ? 'Connecting…' : 'Nothing is running'}</span></div><pre class="c4-log" data-task-log aria-live="polite" tabindex="0">${esc(lastLines)}</pre></section>`;
const transcript = `<section class="c4-card c4-transcript" data-c4-conductor-stream="${esc(`${api}/events?last_event_id=${lastSeq}`)}" data-task="${esc(row.id)}" data-started-at="${esc(row.started_at || row.created_at || '')}" data-c4-conductor="${live ? 'live' : 'idle'}"><div class="c4-card-head"><h2>${live ? 'This turn, live' : 'The last turn'}</h2><span class="c4-muted" data-live-label>${live ? 'Connecting…' : 'Nothing is running'}</span></div><pre class="c4-log" data-task-log aria-live="polite" tabindex="0">${esc(lastLines)}</pre></section>`;
- const composer = row.outcome === 'stopped' ? '' : `<section class="c4-card c4-conductor-composer"><form method="post" action="/tasks/${encodeURIComponent(row.id)}/messages" enctype="multipart/form-data" data-c4-conductor-form data-conductor-api="/api/v1/conductor/${encodeURIComponent(row.id)}/messages"><label for="c4-message">${live ? 'Say something: it is the next turn' : 'What next?'}</label><textarea id="c4-message" name="text" rows="3" placeholder="Tell your conductor what to do next, or ask what it is doing"></textarea><div class="c4-conductor-actions"><div data-c4-upload></div><noscript><label class="c4-file">Attach files<input type="file" name="files" multiple></label></noscript><button class="c4-button" type="submit">Send</button></div><p class="c4-hint">Up to 10 files, 20 MB each, 50 MB together. Programs cannot be attached.</p><div data-c4-upload-progress></div><p class="c4-hint" data-c4-message-status role="status">${live ? 'A message waits until the current turn ends; it is not lost.' : ''}</p></form></section>`;
+ // The session's message box is the shared composer (openvibe-shared 4.215.0, preset agent): its model is the
+ // mode chip, so the picker's who goes with the form and the JSON send, and the turn runs on the model chosen.
+ const modelOptions = row.agent === 'claude-code'
+ ? MODELS.map(([id, label]) => [`${row.agent}|${id}`, label])
+ : [[`${row.agent}|${row.model || ''}`, row.model || 'its default model']];
+ const whoValue = `${row.agent}|${row.model || ''}`;
+ const composer = row.outcome === 'stopped' ? '' : `<section class="c4-card c4-conductor-composer"><form method="post" action="/tasks/${encodeURIComponent(row.id)}/messages" enctype="multipart/form-data" data-c4-conductor-form data-ovc-compose data-ovc-preset="agent" data-c4-model-options="${esc(JSON.stringify(modelOptions))}" data-c4-model-value="${esc(whoValue)}" data-conductor-api="/api/v1/conductor/${encodeURIComponent(row.id)}/messages"><label for="c4-message">${live ? 'Say something: it is the next turn' : 'What next?'}</label><textarea id="c4-message" name="text" rows="3" maxlength="16000" placeholder="Tell your conductor what to do next, or ask what it is doing"></textarea><div class="c4-conductor-actions"><div data-c4-upload></div><noscript><label class="c4-file">Attach files<input type="file" name="files" multiple></label></noscript><button class="c4-button" type="submit">Send</button></div><p class="c4-hint">Up to 10 files, 20 MB each, 50 MB together. Programs cannot be attached.</p><div data-c4-upload-progress></div><p class="c4-hint" data-c4-message-status role="status">${live ? 'A message waits until the current turn ends; it is not lost.' : ''}</p></form></section>`;
const content = `${outcome}<section class="c4-card c4-conductor-head"><div class="c4-task-heading"><div class="c4-task-title-wrap"><h1>Conductor</h1></div><span class="c4-status c4-${esc(row.state)}">${esc(conductorState(row))}</span></div><div class="c4-chips" aria-label="Conductor details"><span class="c4-chip">${esc(row.folder || 'a folder')}</span><span class="c4-chip">${esc(row.agent === 'claude-code' ? modelLabel(row.model) : (row.model || 'its default model'))}</span><span class="c4-chip">safety: ${esc(row.safety || 'ask')}</span><span class="c4-chip">${esc(machineLabel || 'your machine')}</span></div><p class="c4-hint">One long session on your own machine, on the agent you chose and your own login. Each message is the next turn of the same conversation; it hands the work to your other agents through the Codes tools.</p></section>${approvals}${usageCard}<section class="c4-card c4-turns"><div class="c4-card-head"><h2>Turns</h2><span class="c4-muted">${(view.messages || []).length} so far</span></div>${turns ? `<ol class="c4-turn-list">${turns}</ol>` : '<div class="c4-emptyline">Its first turn is starting.</div>'}</section>${transcript}${composer}`;
const content = `${outcome}<section class="c4-card c4-conductor-head"><div class="c4-task-heading"><div class="c4-task-title-wrap"><h1>Conductor</h1></div><span class="c4-status c4-${esc(row.state)}">${esc(conductorState(row))}</span></div><div class="c4-chips" aria-label="Conductor details"><span class="c4-chip">${esc(row.folder || 'a folder')}</span><span class="c4-chip">${esc(row.agent === 'claude-code' ? modelLabel(row.model) : (row.model || 'its default model'))}</span><span class="c4-chip">safety: ${esc(row.safety || 'ask')}</span><span class="c4-chip">${esc(machineLabel || 'your machine')}</span></div><p class="c4-hint">One long session on your own machine, on the agent you chose and your own login. Each message is the next turn of the same conversation; it hands the work to your other agents through the Codes tools.</p></section>${approvals}${usageCard}<section class="c4-card c4-turns"><div class="c4-card-head"><h2>Turns</h2><span class="c4-muted">${(view.messages || []).length} so far</span></div>${turns ? `<ol class="c4-turn-list">${turns}</ol>` : '<div class="c4-emptyline">Its first turn is starting.</div>'}</section>${transcript}${composer}`;
- page(req, res, 'Conductor', wrap(content), { sharedScripts: ['agent-session.js'] });
+ page(req, res, 'Conductor', wrap(content), { sharedScripts: ['agent-session.js', 'ov-composer.js'], scripts: ['js/composer.js'] });
}
}
// ── The front door (a visitor): what Codes is, shown rather than told ──
// ── The front door (a visitor): what Codes is, shown rather than told ──
new file mode 100644
new file mode 100644
@@ -0,0 +1,55 @@
@@ -0,0 +1,55 @@
+'use strict';
+// The conductor's message box on the shared composer (openvibe-shared 4.215.0, OpenVibeComposer.mount, preset
+// agent): the session form keeps its action, uploader and status line; the model is the mode chip; the box sends.
+// Real Chrome at 390 px; skipped without Chrome.
+const assert = require('assert');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const http = require('http');
+const h = require('openvibe-shared/browser-harness');
+if (process.env.OV_SKIP_BROWSER === '1' || !h.findChrome()) { console.log('composer: skipped (no Chrome; set CHROME_BIN)'); process.exit(0); }
+const SHARED = process.env.OV_SHARED_DIR || path.join(__dirname, '..', 'node_modules', 'openvibe-shared');
+const SITE = path.join(__dirname, '..');
+const form = `<section class="c4-card c4-conductor-composer"><form method="post" action="/tasks/t1/messages" enctype="multipart/form-data" data-c4-conductor-form data-ovc-compose data-ovc-preset="agent" data-c4-model-options='[["claude-code|opus","Opus 5.5"],["claude-code|sonnet","Sonnet 5.5"]]' data-c4-model-value="claude-code|opus"><label for="c4-message">What next?</label><textarea id="c4-message" name="text" rows="3" maxlength="16000" placeholder="Tell your conductor what to do next"></textarea><div class="c4-conductor-actions"><div data-c4-upload></div><button class="c4-button" type="submit">Send</button></div><p class="c4-hint" data-c4-message-status role="status"></p></form></section>`;
+let posted = '';
+const server = http.createServer((req, res) => {
+ if (req.method === 'POST') {
+ let body = '';
+ req.on('data', (d) => { body += d; });
+ req.on('end', () => { posted = body; res.writeHead(200, { 'content-type': 'text/html' }); res.end('<!doctype html><p id="posted">posted</p>'); });
+ return;
+ }
+ if (req.url === '/') {
+ res.writeHead(200, { 'content-type': 'text/html' });
+ return res.end(`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Conductor</title><link rel="icon" href="data:,"></head><body style="margin:0;padding:12px;background:#0b1220;color:#e6edf7;font-family:sans-serif">${form}<script src="/ov-composer.js"></script><script src="/composer.js"></script></body></html>`);
+ }
+ if (req.url === '/ov-composer.js') { res.writeHead(200, { 'content-type': 'application/javascript' }); return res.end(fs.readFileSync(path.join(SHARED, 'ov-composer.js'))); }
+ if (req.url === '/composer.js') { res.writeHead(200, { 'content-type': 'application/javascript' }); return res.end(fs.readFileSync(path.join(SITE, 'public/js/composer.js'))); }
+ res.writeHead(404); res.end();
+});
+(async () => {
+ await new Promise((ok) => server.listen(0, '127.0.0.1', ok));
+ const base = `http://127.0.0.1:${server.address().port}`;
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codes-composer-'));
+ const browser = await h.launch({ tmpDir });
+ const wait = (ms) => new Promise((r) => setTimeout(r, ms));
+ const page = await h.openPage(browser, await browser.newContext(), { width: 390, height: 900 });
+ try {
+ await page.goto(base + '/', { maxMs: 6000 });
+ for (let i = 0; i < 100 && !(await page.evaluate(`!!document.querySelector('form.ovc-v2')`)); i++) await wait(40);
+ const shape = await page.evaluate(`(() => { const f = document.querySelector('form[data-c4-conductor-form]'); return { preset: f.dataset.preset, action: f.getAttribute('action'), text: f.querySelector('#c4-message[data-ovt-text]') ? f.querySelector('#c4-message').value : null, chip: f.querySelector('.ovc-chip-v') ? f.querySelector('.ovc-chip-v').textContent : null, who: f.querySelector('select[name=who]').value, upload: !!f.querySelector('[data-c4-upload]'), status: !!f.querySelector('[data-c4-message-status]'), old: !!f.querySelector('.c4-conductor-actions button[type=submit]') }; })()`);
+ assert.deepEqual(shape, { preset: 'agent', action: '/tasks/t1/messages', text: '', chip: 'Opus 5.5', who: 'claude-code|opus', upload: true, status: true, old: false });
+ assert.equal(await page.evaluate(`document.documentElement.scrollWidth`), 390, 'nothing is wider than a phone');
+ await page.evaluate(`(() => { const f = document.querySelector('form.ovc-v2'); const t = f.querySelector('textarea'); t.value = 'Carry on'; t.dispatchEvent(new Event('input', { bubbles: true })); f.querySelector('select[name=who]').value = 'claude-code|sonnet'; f.querySelector('[data-ovt-send]').click(); })()`);
+ for (let i = 0; i < 100 && !posted; i++) await wait(40);
+ assert.match(posted, /Carry on/);
+ assert.match(posted, /claude-code\|sonnet/);
+ console.log('composer: the conductor is the shared agent composer, its model is the chip, sends, 390 px');
+ } finally {
+ await page.close().catch(() => {});
+ await browser.close();
+ server.close();
+ fs.rmSync(tmpDir, { recursive: true, force: true });
+ }
+})().catch((e) => { console.error(e); process.exit(1); });