openvibers/openvibe.space Commits

Space editor: your agent's line, with the preview left in view (#228)

OP OpenVibers committed 6 days ago · b2b0e400a3 · 4 files, +73 −17

Changes

ddiff --git a/public/css/spaces.css b/public/css/spaces.css
iindex cfd95aa..856300d 100644
---- a/public/css/spaces.css
++++ b/public/css/spaces.css
@@@ -104,6 +104,30 @@ body[data-page="spaces"] .page:has(.sp-editor) { padding-bottom: 0; }
.sp-ed-ai { padding: 10px 16px; border-bottom: 1px solid var(--border, #2c3d5c); background: color-mix(in srgb, var(--accent, #60a5fa) 6%, transparent); }
.sp-ed-ai form { display: flex; gap: 8px; align-items: center; }
.sp-ed-ai i { color: var(--accent, #60a5fa); }
++/* The agent's line (plan §Z, Z1): where the person's agent is, above the editor, the preview left in view. A live dot
++ while it builds (the only motion, and only then), amber while it asks to publish, green done, red stuck. */
++.sp-ed-agent { display: flex; align-items: center; gap: 10px; margin-top: 8px; padding: 7px 8px 7px 12px; min-width: 0; border: 1px solid color-mix(in srgb, var(--accent, #60a5fa) 35%, var(--border, #2c3d5c)); border-radius: 10px; background: var(--bg-card, #111a2b); font-size: 0.9rem; }
++.sp-ed-agent-words { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
++.sp-ed-agent-words b { font-weight: 650; }
++.sp-ed-agent-dot { width: 9px; height: 9px; flex: none; border-radius: 50%; background: var(--accent, #60a5fa); }
++.sp-ed-agent[data-state="running"] .sp-ed-agent-dot { animation: sp-agent-live 1.8s ease-out infinite; }
++@keyframes sp-agent-live { 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent, #60a5fa) 60%, transparent); } 70%, 100% { box-shadow: 0 0 0 6px transparent; } }
++.sp-ed-agent[data-state="waiting"] { border-color: color-mix(in srgb, var(--warning-text, #fbbf24) 60%, var(--border, #2c3d5c)); background: color-mix(in srgb, var(--warning-text, #fbbf24) 8%, var(--bg-card, #111a2b)); }
++.sp-ed-agent[data-state="waiting"] .sp-ed-agent-dot { background: var(--warning-text, #fbbf24); }
++.sp-ed-agent[data-state="done"] { border-color: color-mix(in srgb, var(--success-text, #4ade80) 45%, var(--border, #2c3d5c)); }
++.sp-ed-agent[data-state="done"] .sp-ed-agent-dot { background: var(--success-text, #4ade80); }
++.sp-ed-agent[data-state="failed"] { border-color: color-mix(in srgb, var(--danger-text, #f87171) 50%, var(--border, #2c3d5c)); }
++.sp-ed-agent[data-state="failed"] .sp-ed-agent-dot { background: var(--danger-text, #f87171); }
++.sp-ed-agent .sp-btn { flex: none; min-height: 36px; }
++.sp-ed-agent-x { position: relative; flex: none; width: 28px; height: 28px; border: 0; border-radius: 50%; background: transparent; color: var(--text-secondary, #9fb0c8); font-size: 1.1rem; line-height: 1; cursor: pointer; }
++.sp-ed-agent-x::after { content: ""; position: absolute; inset: -8px; }
++.sp-ed-agent-x:hover { color: var(--text-primary, #e6edf7); background: var(--bg-hover, rgba(127, 140, 160, 0.12)); }
++@media (prefers-reduced-motion: reduce) { .sp-ed-agent-dot { animation: none !important; } }
++@media (max-width: 560px) {
++ .sp-ed-agent { flex-wrap: wrap; row-gap: 6px; }
++ .sp-ed-agent-words { flex-basis: calc(100% - 60px); white-space: normal; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
++ .sp-ed-agent .sp-btn { margin-left: 19px; }
++}
.sp-ed-ai input { flex: 1; min-width: 0; border: 1px solid var(--border, #2c3d5c); background: var(--bg-primary, #0b1220); color: inherit; border-radius: 10px; padding: 9px 12px; font: inherit; }
.sp-ed-main { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); min-height: 0; }
.sp-ed-code { display: grid; grid-template-rows: auto 1fr; min-height: 0; border-right: 1px solid var(--border, #2c3d5c); }
ddiff --git a/public/js/space-editor.js b/public/js/space-editor.js
iindex 187bdbc..1b65268 100644
---- a/public/js/space-editor.js
++++ b/public/js/space-editor.js
@@@ -166,15 +166,16 @@
var go = function () {
return call('POST', '/ai', { prompt: prompt }).then(function (j) {
input.value = '';
++ // The answer names a task of the person's own agent (plan 2026-10-22 §Z stage Z1): follow it in the bar, with
++ // the preview left in view so the person watches the space change as the agent writes it.
++ if (j && j.task) { agent = { task: j.task, state: 'running' }; agentBar('running', prompt); openDock(); setTimeout(agentPoll, 1500); return; }
building(true, prompt);
-- // The answer names a task of the person's own agent (plan 2026-10-22 §Z stage Z1): follow it there.
-- if (j && j.task) { agent = { task: j.task, state: 'running' }; var title = $('ed-building-title'); if (title) title.textContent = 'Your agent is building this…'; openDock(); setTimeout(agentPoll, 1500); return; }
setTimeout(poll, 3000);
}, function (err) { status(err.message, 'bad'); });
};
if (dirty) save().then(go, function () {}); else go();
});
-- if (space.building) { building(true, space.building_prompt); setTimeout(poll, 1500); }
++ if (space.building && !boot.agent) { building(true, space.building_prompt); setTimeout(poll, 1500); }
// ── The person's own agent (plan 2026-10-22 §Z, stage Z1) ──
// The space is a task of the agent: the Dock opens at it, the page follows it, and the preview shows the
@@@ -196,26 +197,46 @@
run();
return true;
}
++ /**
++ * The agent's line above the editor: one sentence for where the task is, and the Dock for its steps. Building
++ * (a live dot), asking (amber: it wants to publish, nothing goes live until the person answers), done, or stuck.
++ */
++ var AGENT_WORDS = {
++ running: ['Your agent is building this', 'Watch in your Dock'],
++ waiting: ['Your agent asks to publish this space', 'Answer in your Dock'],
++ done: ['Your agent is done', 'See its steps'],
++ failed: ['Your agent couldn\u2019t finish', 'See what happened'],
++ };
++ var agentNote = '';
++ function agentBar(state, text) {
++ var bar = $('ed-agent'); if (!bar) return;
++ var words = AGENT_WORDS[state] || AGENT_WORDS.running;
++ if (text != null) agentNote = text;
++ bar.dataset.state = state;
++ $('ed-agent-title').textContent = words[0];
++ $('ed-agent-text').textContent = agentNote ? (state === 'running' || state === 'waiting' ? '\u201c' + agentNote + '\u201d' : agentNote) : '';
++ $('ed-agent-open-label').textContent = words[1];
++ $('ed-agent-open').classList.toggle('sp-btn-quiet', state !== 'waiting');
++ $('ed-agent-x').hidden = state === 'running' || state === 'waiting';
++ bar.hidden = false;
++ }
++ $('ed-agent-open').addEventListener('click', openDock);
++ $('ed-agent-x').addEventListener('click', function () { $('ed-agent').hidden = true; });
function agentPoll() {
if (!agent) return;
call('GET', '/agent').then(function (j) {
agentFiles(j.files);
-- if (j.publish && j.publish.state === 'pending') status('Your agent wants to publish this space — answer it in your Dock');
-- if (j.status === 'running' || j.status === 'waiting') { setTimeout(agentPoll, 3000); return; }
-- building(false);
-- if (j.status === 'done') status(j.message ? 'Your agent is done: ' + j.message : 'Your agent is done');
-- else if (j.status === 'failed') status('Your agent could not build this space', 'bad');
-- else status('');
++ var asking = j.publish && j.publish.state === 'pending';
++ if (j.status === 'running' || j.status === 'waiting') { agentBar(asking ? 'waiting' : 'running'); setTimeout(agentPoll, 3000); return; }
++ if (j.status === 'done') agentBar('done', j.message || '');
++ else if (j.status === 'failed') agentBar('failed', j.message || '');
++ else $('ed-agent').hidden = true;
}, function () { setTimeout(agentPoll, 5000); });
}
-- if (agent) {
-- var title = $('ed-building-title');
-- if (title) title.textContent = 'Your agent is building this…';
-- if (agent.state === 'running' || agent.state === 'waiting') {
-- building(true, '');
-- openDock();
-- setTimeout(agentPoll, 1500);
-- }
++ if (agent && (agent.state === 'running' || agent.state === 'waiting')) {
++ agentBar(agent.state === 'waiting' ? 'waiting' : 'running', space.building_prompt || '');
++ openDock();
++ setTimeout(agentPoll, 1500);
}
// ── Settings ──
ddiff --git a/server/spaces/render.js b/server/spaces/render.js
iindex 810349b..bfc7e5c 100644
---- a/server/spaces/render.js
++++ b/server/spaces/render.js
@@@ -211,6 +211,12 @@ function editorPage({ space, files, csrf, canPublish, url, aiOn, buildsLeft, tem
<i class="fa-solid fa-wand-magic-sparkles" aria-hidden="true"></i>
<input id="ed-ai" maxlength="4000" autocomplete="off" placeholder="${aiOn ? 'Ask AI to change it: “make the buttons bigger and add a dark mode toggle”' : 'OpenVibe AI is not available right now'}" ${aiOn ? '' : 'disabled'}>
<button class="sp-btn" ${aiOn ? '' : 'disabled'}>Change</button></form>
++ <div class="sp-ed-agent" id="ed-agent" role="status" aria-live="polite" data-state="running" hidden>
++ <span class="sp-ed-agent-dot" aria-hidden="true"></span>
++ <span class="sp-ed-agent-words"><b id="ed-agent-title">Your agent is building this</b> <span class="sp-muted" id="ed-agent-text"></span></span>
++ <button class="sp-btn sp-btn-quiet sp-btn-sm" id="ed-agent-open" type="button"><i class="fa-solid fa-list-check" aria-hidden="true"></i> <span id="ed-agent-open-label">Watch in your Dock</span></button>
++ <button class="sp-ed-agent-x" id="ed-agent-x" type="button" aria-label="Hide" hidden>×</button>
++ </div>
</div>
<div class="sp-ed-switch" role="tablist" aria-label="Show"><button type="button" role="tab" data-view="preview" aria-selected="true"><i class="fa-solid fa-eye" aria-hidden="true"></i> Preview</button><button type="button" role="tab" data-view="code" aria-selected="false"><i class="fa-solid fa-code" aria-hidden="true"></i> Code</button></div>
<div class="sp-ed-main">
ddiff --git a/test/spaces-agent.test.js b/test/spaces-agent.test.js
iindex 43d6fff..ac917e2 100644
---- a/test/spaces-agent.test.js
++++ b/test/spaces-agent.test.js
@@@ -74,6 +74,11 @@ const BUILD_CAPS = ['space.build.read', 'space.build.create', 'space.build.write
// The page opens the Dock at that task: the shared navbar's API (the Dock's Agent panel marks the step).
const js = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'space-editor.js'), 'utf8');
assert.ok(/OpenVibeNavbar\.openDock\('agent', \{ task: agent\.task \}\)/.test(js), 'the editor opens the Dock at the task');
++ // The agent's line, not the full-cover overlay: the preview stays in view while the agent writes the space, and
++ // asking to publish is its own state with the Dock one tap away.
++ assert.match(page.text, /<div class="sp-ed-agent" id="ed-agent" role="status" aria-live="polite" data-state="running" hidden>/, 'the agent bar is on the page');
++ assert.ok(/if \(space\.building && !boot\.agent\)/.test(js), 'the building overlay is only for the old builder');
++ for (const words of ['Your agent is building this', 'Your agent asks to publish this space', 'Answer in your Dock', 'Your agent is done']) assert.ok(js.includes(words), `the bar says "${words}"`);
});
await check('the agent works on the space through the builder API, and the page shows it', async () => {