openvibers/openvibe.codes Commits

Who leads: the composer names your own setting; Settings no longer pins Opus (#314)

OP OpenVibers committed 6 days ago · 092d1952b2 · 3 files, +45 −7

* Who leads: the composer names your own setting, and Settings no longer pins Opus for someone who never chose

* Composer file chips: one wrapping row, names cut with an ellipsis, the upload as a line along the chip, a failed file in red, a 44px remove

Changes

ddiff --git a/public/css/chat.css b/public/css/chat.css
iindex 6a77569..66a9b9d 100644
---- a/public/css/chat.css
++++ b/public/css/chat.css
@@@ -73,6 +73,26 @@ details[open]>summary .ch-caret{rotate:180deg}
.ch-pick-file{cursor:pointer;padding:0 9px}
.ch-pick-file input{position:absolute;inset:0;opacity:0;cursor:pointer}
.ch-pick-file.has-files{color:var(--ch-accent);border-color:color-mix(in srgb,var(--ch-accent) 50%,var(--ch-line))}
++/* Files waiting in the composer: one row that wraps, a chip per file that never outgrows a phone; the upload is a
++ 2px line along the chip's foot, and the remove keeps a 44px target inside a 32px chip. */
++.ch-files{display:flex;flex-wrap:wrap;gap:6px;margin:0 0 8px;min-width:0}
++.ch-files[hidden]{display:none}
++.ch-file{position:relative;overflow:hidden;max-width:100%;min-width:0;min-height:32px;padding:3px 2px 3px 6px;gap:7px;cursor:default;color:var(--ch-ink)}
++.ch-file:hover{color:var(--ch-ink);border-color:var(--ch-line)}
++.ch-file img{width:24px;height:24px;flex:none;border-radius:6px;object-fit:cover}
++.ch-file>svg{width:15px;height:15px;flex:none;color:var(--ch-soft)}
++.ch-file>span{min-width:0;max-width:24ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
++.ch-file small{flex:none;font-size:11.5px;color:var(--ch-muted);font-variant-numeric:tabular-nums}
++.ch-file progress{position:absolute;left:0;right:0;bottom:0;width:100%;height:2px;border:0;-webkit-appearance:none;appearance:none;background:transparent;color:var(--ch-accent)}
++.ch-file progress[hidden]{display:none}
++.ch-file progress::-webkit-progress-bar{background:transparent}
++.ch-file progress::-webkit-progress-value{background:var(--ch-accent)}
++.ch-file progress::-moz-progress-bar{background:var(--ch-accent)}
++.ch-file .ch-icon-btn{position:relative;width:26px;height:26px;border-radius:999px;font-size:17px;line-height:1}
++.ch-file .ch-icon-btn::after{content:"";position:absolute;inset:-9px}
++.ch-file.is-bad{border-color:color-mix(in srgb,var(--ch-bad) 55%,var(--ch-line));color:var(--ch-bad)}
++.ch-file.is-bad small{color:var(--ch-bad)}
++@media (max-width:520px){.ch-file>span{max-width:16ch}}
.ch .ovt-c-note.is-bad{color:var(--ch-bad)}
.ch .ovt-composer.is-sending textarea{opacity:.6}
ddiff --git a/server/http/c4.js b/server/http/c4.js
iindex 89f2008..dd91795 100644
---- a/server/http/c4.js
++++ b/server/http/c4.js
@@@ -289,14 +289,26 @@ ${pr.problem ? `<p class="c4-hint">${esc(pr.problem.detail)}</p>` : `<dl class="
if (amount > 0) return `$${amount} per ${offer.price.unit || 'run'}`;
return 'included in your plan';
}
-- /** Every harness and model a conductor may lead with, with what the person's machines say and its price. */
-- function leadChoices(settings, aware, catalog = null) {
-- const chosen = settings && settings.conductor_agent ? `${settings.conductor_agent}|${settings.conductor_model || ''}` : '';
++ /** The person's saved "Who leads your tasks" as `agent|model`, or '' for the default. */
++ const savedLead = (settings) => (settings && settings.conductor_agent ? `${settings.conductor_agent}|${settings.conductor_model || ''}` : '');
++ /** The saved lead's name, as the composer's first option says it ("Claude Code on Opus" for the default). */
++ function savedLeadName(settings, catalog = null) {
++ const chosen = savedLead(settings);
++ const c = chosen && models.choices(harnesses, catalog).find((x) => x.value === chosen);
++ return c ? c.label : 'Claude Code on Opus';
++ }
++ /**
++ * Every harness and model a conductor may lead with, with what the person's machines say and its price.
++ * `select` marks the saved choice (Settings); the composer passes false, because its first option already
++ * means "my setting". Nothing is preselected for the default, so saving Settings untouched keeps following it.
++ */
++ function leadChoices(settings, aware, catalog = null, select = true) {
++ const chosen = select ? savedLead(settings) : '';
return models.choices(harnesses, catalog).slice(1).map((c) => {
const info = aware.get(String(c.agent));
const price = c.detail || priceLine(c.agent, c.model);
const attrs = optionAttrs({ ...(info || {}), desc: [info && info.desc, price].filter(Boolean).join(' · ') });
-- return `<option value="${esc(c.value)}"${chosen === c.value || (!chosen && c.value === 'claude-code|opus') ? ' selected' : ''}${attrs}>${esc(`${c.label} — ${price}`)}</option>`;
++ return `<option value="${esc(c.value)}"${chosen && chosen === c.value ? ' selected' : ''}${attrs}>${esc(`${c.label} — ${price}`)}</option>`;
}).join('');
}
const conductorState = (row) => (['queued', 'placed', 'running'].includes(row.state)
@@@ -545,7 +557,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)}
<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="">Today's choice: Claude Code on Opus</option>${leadChoices(settings, aware)}</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"><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>
<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>
@@@ -821,8 +833,8 @@ ${report.days.length ? `<div class="table-wrap" tabindex="0" role="region" aria-
<label>Default machine <select name="default_machine"><option value="">Auto</option>${machineOptions}</select></label>
</div>
<h3 class="c4-subhead" id="lead">Who leads your tasks</h3>
--<p class="c4-hint">The agent that plans the work and hands the parts to the others. Every harness and model you can run is listed with what your machines say about it and what it costs. A single task can still pick another leader in its composer.</p>
--<div class="c4-form-grid"><label>Who leads your tasks <select name="conductor_who"><option value="">Today's choice: Claude Code on Opus</option>${leadChoices(settings, aware)}</select></label></div>
++<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>
++<div class="c4-form-grid"><label>Lead <select name="conductor_who"><option value="">The default (Claude Code on Opus today)</option>${leadChoices(settings, aware)}</select></label></div>
<h3 class="c4-subhead">Limits</h3>
<div class="c4-form-grid">
<label>Steps per task ${limitSelect('steps', settings.steps, [[200, '200'], [350, '350'], [500, '500'], [1000, '1,000']], 'steps')}</label>
ddiff --git a/test/conductor.test.js b/test/conductor.test.js
iindex d032e17..79ee775 100644
---- a/test/conductor.test.js
++++ b/test/conductor.test.js
@@@ -196,6 +196,12 @@ const DEVICE = 'dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2N1';
const res = await get('/conductor');
assert.strictEqual(res.status, 200);
assert.match(res.text, /<select name="who" data-c4-model>/, 'who leads is a list');
++ // The first option is the person's own setting, said by name, and it is the one selected: the composer never
++ // claims a default the person changed, and no other leader is preselected.
++ assert.match(res.text, /<select name="who" data-c4-model><option value="">Your setting: [^<]+<\/option>/, 'the first choice names the setting');
++ const whoList = res.text.split('<select name="who" data-c4-model>')[1].split('</select>')[0];
++ assert.ok(!/ selected/.test(whoList), 'nothing else is preselected');
++ assert.ok(!res.text.includes("Today's choice"), 'no "Today\'s choice" wording');
for (const label of ['Claude Code · Opus', 'Claude Code · Sonnet', 'Codex', 'OpenCode', 'DeepSeek']) assert.ok(res.text.includes(label), `${label} is offered`);
assert.match(res.text, /data-c4-folder-pick/, 'the recent folders are a choice beside the typed path');
assert.ok(res.text.includes(`data-machine="${NODE}"`), 'this conductor&rsquo;s folder is one, on its machine');