openvibers/openvibe.actor Commits

Actor: the task page says it once — no dangling separator in the ledger, no repeated waiting box (#205)

OP OpenVibers committed 14 days ago · a2b930ae63 · 4 files, +138 −12

The brain's ledger drew "Done: no ·" and "Stuck: no ·": the separator after the value
dangles on a phone, where every label sits on its own line. Each labelled line now shows
its value only, from one formatter that the server render and the live card share.

A task stopped mid-action said it twice: the acting card, then a "Waiting for you" box
that repeated it word for word. That box now keeps only the Cancel button and the one
fact that matters while the person decides — the task holds its place and its budget.
The other reasons to wait (the question before a task starts, a hand-off) keep their box
as they were.

The new test renders a waiting task's ledger and its boxes, and checks that no labelled
line ends in a separator and that the acting page says the task is waiting once.

Changes

ddiff --git a/public/js/task-events.js b/public/js/task-events.js
iindex cb1df8e..ac82085 100644
---- a/public/js/task-events.js
++++ b/public/js/task-events.js
@@@ -111,6 +111,21 @@
});
return out.steps.length || out.ledger ? out : null;
}
++ /**
++ * The progress row of the ledger (stage V2-8): each labelled line shows its value only. A separator after the value
++ * would dangle on a phone, where every label sits on its own line.
++ */
++ function progressRows(progress) {
++ if (!progress) return [];
++ var stuck = (progress.stuck === true || progress.progressing === false) ? 'yes' : 'no';
++ var rows = [
++ { label: 'Done:', value: progress.done ? 'yes' : 'no', cls: 'plan-progress' },
++ { label: 'Stuck:', value: stuck, cls: 'plan-progress' },
++ { label: 'Next:', value: progress.next || 'none', cls: 'plan-progress' },
++ ];
++ if (progress.stopped) rows.push({ label: 'Stopped:', value: String(progress.stopped), cls: 'plan-stopped' });
++ return rows;
++ }
/** The divider a plan draws in the session: the step in hand, or the whole plan done. */
function planLine(p) {
if (!p || !p.steps.length) return null;
@@@ -234,5 +249,5 @@
return out.replace(/[.?!]*$/, '') + '.';
}
-- return { lines: lines, firstAt: firstAt, duration: duration, attachments: attachments, plan: plan, planLine: planLine, assumingLine: assumingLine, lowerFirst: lowerFirst };
++ return { lines: lines, firstAt: firstAt, duration: duration, attachments: attachments, plan: plan, planLine: planLine, assumingLine: assumingLine, lowerFirst: lowerFirst, progressRows: progressRows };
});
ddiff --git a/public/js/task.js b/public/js/task.js
iindex c3977c0..0e42a08 100644
---- a/public/js/task.js
++++ b/public/js/task.js
@@@ -81,13 +81,9 @@
labelled('To work out:', l.derive.length ? l.derive.join('; ') : 'none');
}
if (p.progress) {
-- var stuck = (p.progress.stuck === true || p.progress.progressing === false) ? 'yes' : 'no';
-- var line = el('p', 'plan-progress');
-- line.appendChild(el('b', '', 'Done:')); line.appendChild(document.createTextNode(' ' + (p.progress.done ? 'yes' : 'no') + ' \u00b7 '));
-- line.appendChild(el('b', '', 'Stuck:')); line.appendChild(document.createTextNode(' ' + stuck + ' \u00b7 '));
-- line.appendChild(el('b', '', 'Next:')); line.appendChild(document.createTextNode(' ' + (p.progress.next || 'none')));
-- ledger.appendChild(line);
-- if (p.progress.stopped) labelled('Stopped:', p.progress.stopped, 'plan-stopped');
++ // Each labelled line shows its value only: a separator after the value would dangle on a phone.
++ var rows = (window.OpenVibeTaskEvents && window.OpenVibeTaskEvents.progressRows) ? window.OpenVibeTaskEvents.progressRows(p.progress) : [];
++ rows.forEach(function (row) { labelled(row.label, row.value, row.cls); });
}
ledger.hidden = !p.ledger && !p.progress;
}
ddiff --git a/server/http/pages.js b/server/http/pages.js
iindex b218fd3..5e347ef 100644
---- a/server/http/pages.js
++++ b/server/http/pages.js
@@@ -993,6 +993,8 @@ ${existing ? html`<div class="notice" role="status">Already offered ${time(exist
// The resume card (stage V2-9): the run was stopped inside an action, not before it started.
kind: questionEvent.question_kind === 'acting' ? 'acting' : 'question',
} : null;
++ // The acting card (stage V2-9) says the task is waiting once; the box under it must not say it again.
++ const actingCard = Boolean(question && question.kind === 'acting');
const answerProblem = req.query.answer === 'gone' ? 'That question is no longer waiting for an answer. The task may have finished or been restarted; ask again.' : null;
const assumeProblem = req.query.assume === 'gone' ? 'Those assumptions could not be changed: the task is no longer running.' : null;
const cls = (events.find((e) => e.step === 'plan' && !e.agent && /"(\w+)" task/.test(e.text || '')) || { text: '' }).text.match(/"(\w+)"/);
@@@ -1040,7 +1042,7 @@ ${answerProblem ? notice(answerProblem, 'warn') : ''}
${assumeProblem ? notice(assumeProblem, 'warn') : ''}
${forkProblem ? notice(forkProblem, 'warn') : ''}
${question ? questionCard(row, question) : ''}
--${outcome(t, row, { acting: question && question.kind === 'acting' })}
++${outcome(t, row, { acting: actingCard })}
${t.state === 'succeeded' ? benchBlock(row, offer) : ''}
${t.state === 'succeeded' ? followBlock(row, follows, followProblem) : ''}
${savedStepsBlock(row, saved, spans)}
@@@ -1054,7 +1056,7 @@ ${open || look ? html`<figure class="last-look" id="last-look"${look ? '' : raw(
<a id="last-look-link" href="${look ? `/tasks/${row.id}/look?at=${look.at}` : '#'}" target="_blank" rel="noopener" title="Open it full size"><img id="last-look-img" src="${look ? `/tasks/${row.id}/look?at=${look.at}` : ''}" alt="The screen your agent last looked at"${look && look.width ? raw(` width="${Number(look.width)}" height="${Number(look.height)}"`) : ''} loading="lazy"></a>
<figcaption class="muted small">What your agent last saw on the screen${look ? html` · ${time(new Date(look.at).toISOString())}` : ''}</figcaption>
</figure>` : ''}
--${waiting ? html`<p class="muted small live-note" id="waiting-note">Waiting for your answer. The task keeps its place and its budget while you decide.</p>` : html`<section class="how" aria-labelledby="h-how"><h2 id="h-how">How it went</h2>
++${waiting ? html`<p class="muted small live-note" id="waiting-note">Waiting for your answer.${actingCard ? '' : ' The task keeps its place and its budget while you decide.'}</p>` : html`<section class="how" aria-labelledby="h-how"><h2 id="h-how">How it went</h2>
${!open && plan ? planCard(plan) : ''}
<div class="attempts" id="attempts">${attemptsOf(events).map(attemptBlock)}</div>
${traceBlock(spans, row.id)}
@@@ -1121,7 +1123,7 @@ ${opts.editable ? html`<form method="post" action="/tasks/${opts.taskId}/assumpt
${ledger ? html`<p><b>Given:</b> ${ledger.given.length ? ledger.given.join('; ') : 'none'}</p>
<p><b>To find:</b> ${ledger.lookup.length ? ledger.lookup.join('; ') : 'none'}</p>
<p><b>To work out:</b> ${ledger.derive.length ? ledger.derive.join('; ') : 'none'}</p>` : ''}
--${progress ? html`<p class="plan-progress"><b>Done:</b> ${progress.done ? 'yes' : 'no'} · <b>Stuck:</b> ${progress.stuck === true || progress.progressing === false ? 'yes' : 'no'} · <b>Next:</b> ${progress.next || 'none'}</p>${progress.stopped ? html`<p class="plan-stopped"><b>Stopped:</b> ${progress.stopped}</p>` : ''}` : ''}
++${progress ? taskEvents.progressRows(progress).map((row) => html`<p class="${row.cls}"><b>${row.label}</b> ${row.value}</p>`) : ''}
</div>
</section>`;
}
@@@ -1153,7 +1155,8 @@ ${problem ? html`<p class="notice warn" role="alert">${problem}</p>` : ''}
return html`<section class="outcome outcome-bad" role="status"><h2>Actor couldn't finish this</h2><p>${friendlyError(t.error)}</p><div class="outcome-actions">${again}</div>${t.error ? html`<p class="muted small">Code <code>${t.error.code}</code></p>` : ''}</section>`;
}
if (t.state === 'cancelled') return html`<section class="outcome" role="status"><h2>Cancelled</h2><p>What was spent before the cancel stays spent.</p><div class="outcome-actions">${again}</div></section>`;
-- if (t.state === 'needs_you' && acting) return html`<section class="outcome outcome-wait" role="status"><h2>Waiting for you</h2><p>Actor was stopped while it was doing something that may have happened. Check it yourself, then choose above: run it again, record that it happened, or stop the task. The task keeps its place and its budget while you decide.</p><form method="post" action="/tasks/${row.id}/cancel" class="inline"><button class="sc-btn" type="submit">Cancel</button></form></section>`;
++ // The acting card above already says what was stopped; this box keeps only what the person can do here.
++ if (t.state === 'needs_you' && acting) return html`<section class="outcome outcome-wait" role="status"><h2>Waiting for you</h2><p>The task keeps its place and its budget while you decide.</p><form method="post" action="/tasks/${row.id}/cancel" class="inline"><button class="sc-btn" type="submit">Cancel</button></form></section>`;
if (t.state === 'needs_you') return html`<section class="outcome outcome-wait" role="status"><h2>Waiting for you</h2><p>Actor asked one question before spending anything on this task. Answer it above, and the same task continues with your words.</p><form method="post" action="/tasks/${row.id}/cancel" class="inline"><button class="sc-btn" type="submit">Cancel</button></form></section>`;
return html`<section class="outcome outcome-live" role="status" aria-live="polite"><h2><span class="dot-pulse" aria-hidden="true"></span> <span id="live-state">${STATE_TEXT[t.state]}…</span></h2><p class="muted">This page fills in as the agent works. You can leave and come back: the task keeps running.</p><form method="post" action="/tasks/${row.id}/cancel" class="inline"><button class="sc-btn" type="submit">Cancel</button></form></section>`;
}
ddiff --git a/test/task-page-polish.test.js b/test/task-page-polish.test.js
nnew file mode 100644
iindex 0000000..70693fa
---- /dev/null
++++ b/test/task-page-polish.test.js
@@@ -0,0 +1,112 @@
++'use strict';
++/**
++ * The task page, polished (Actor v2, after V2-9): the brain's ledger shows each labelled value on its own line, with no
++ * dangling separator after it, and a task stopped mid-action says it once — the acting card, then a short "Waiting for
++ * you" box that keeps only the Cancel button and what the task does while the person decides.
++ */
++const assert = require('assert');
++const { boot, check, done } = require('./helpers/boot');
++const store = require('../server/tasks/store');
++const taskEvents = require('../public/js/task-events');
++
++const PLAN = '→ 1. Move the rover forward one step\nTask ledger: given the rover · find nothing · derive the move\nProgress: not done; not looping; moving; next Move the rover forward one step';
++
++/** The ledger's lines as a person reads them: the label and its value, without the markup. */
++function ledgerLines(page) {
++ const block = /<div class="plan-ledger"[^>]*>([\s\S]*?)<\/div>/.exec(page);
++ assert.ok(block, 'the page has a ledger');
++ return block[1]
++ .split('</p>')
++ .map((chunk) => chunk.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim())
++ .filter(Boolean);
++}
++
++/** The "Waiting for you" box on its own. */
++function waitingBox(page) {
++ const box = /<section class="outcome outcome-wait"[^>]*>([\s\S]*?)<\/section>/.exec(page);
++ assert.ok(box, 'the page has a waiting box');
++ return box[1].replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
++}
++
++/** A task that waits for its person, with the brain's ledgers on its plan card. */
++async function waitingTask(s, requester, at, { acting = false } = {}) {
++ const id = s.newId('tsk');
++ await store.insertTask(s, { id, requester, task: 'Move my rover forward one step.', mode: 'balanced', budget_task: 0.05, budget_day: 0.25, created_at: at });
++ await store.updateTask(s, id, { state: 'needs_you', cost_usd: 0.0004 });
++ await store.appendEvent(s, id, { at, kind: 'output', step: 'plan', agent: 'openvibe-runtime', text: PLAN });
++ if (acting) {
++ await store.appendEvent(s, id, {
++ at, kind: 'output', step: 'question', agent: 'openvibe-runtime', question_kind: 'acting', tool: 'robot steps', input: 'rover: forward 1',
++ text: 'Actor was stopped while it was robot steps (rover: forward 1). It may or may not have happened. Check, then choose.',
++ options: ['Run it again', 'It happened, go on', 'Stop the task'],
++ });
++ } else {
++ await store.appendEvent(s, id, {
++ at, kind: 'output', step: 'question', agent: 'openvibe-runtime',
++ text: 'Which city?', options: ['Lisbon', 'Porto'],
++ });
++ }
++ await store.appendEvent(s, id, { at, kind: 'state', state: 'needs_you' });
++ return id;
++}
++
++(async () => {
++ const t = await boot();
++ try {
++ const u = t.network.addUser('polish');
++ const s = t.ctx.s;
++ const at = s.iso();
++ const requester = `user:${u.subject}`;
++ const actingId = await waitingTask(s, requester, at, { acting: true });
++ const askedId = await waitingTask(s, requester, at);
++
++ await check('the ledger of a waiting task shows each value only: no line ends in a separator', async () => {
++ const page = await t.get(`/tasks/${actingId}`, { as: u });
++ assert.strictEqual(page.status, 200);
++ const lines = ledgerLines(page.text);
++ assert.ok(lines.length >= 6, `the ledger has its lines (${lines.length})`);
++ for (const line of lines) {
++ assert.ok(!/\u00b7\s*$/.test(line), `no dangling separator: "${line}"`);
++ assert.ok(!/\u00b7/.test(line), `no separator at all: "${line}"`);
++ }
++ assert.ok(lines.some((l) => l === 'Done: no'), 'the ledger says whether it is done');
++ assert.ok(lines.some((l) => l === 'Stuck: no'), 'and whether it is stuck');
++ assert.ok(lines.some((l) => l === 'Next: Move the rover forward one step'), 'and what is next');
++ });
++
++ await check('the ledger formatter gives every label its own value', async () => {
++ const rows = taskEvents.progressRows({ done: false, looping: false, progressing: true, stuck: false, next: null, stopped: null });
++ assert.deepStrictEqual(rows.map((r) => `${r.label} ${r.value}`), ['Done: no', 'Stuck: no', 'Next: none']);
++ for (const row of rows) assert.ok(!/\u00b7/.test(row.value), 'a value carries no separator');
++ const stopped = taskEvents.progressRows({ done: true, progressing: true, stuck: false, next: null, stopped: 'the check failed' });
++ assert.deepStrictEqual(stopped.map((r) => `${r.label} ${r.value}`), ['Done: yes', 'Stuck: no', 'Next: none', 'Stopped: the check failed']);
++ });
++
++ await check('a task stopped mid-action says it once: the card, then a short waiting box', async () => {
++ const page = await t.get(`/tasks/${actingId}`, { as: u });
++ assert.strictEqual(page.status, 200);
++ assert.match(page.text, /Actor was stopped mid-action/);
++ for (const option of ['Run it again', 'It happened, go on', 'Stop the task']) {
++ assert.ok(page.text.includes(`value="${option}"`), `the card offers ${option}`);
++ }
++ const box = waitingBox(page.text);
++ assert.ok(/The task keeps its place and its budget while you decide\./.test(box), 'the box keeps the budget line');
++ assert.ok(/Cancel/.test(box), 'the box keeps the Cancel button');
++ assert.ok(!/doing something that may have happened/.test(box), 'the box does not repeat the acting card');
++ assert.ok(!/Check it yourself/.test(box), 'the box does not repeat the acting card');
++ assert.ok(!/choose above/.test(box), 'the box does not repeat the acting card');
++ });
++
++ await check('another reason to wait keeps its box as it was', async () => {
++ const page = await t.get(`/tasks/${askedId}`, { as: u });
++ assert.strictEqual(page.status, 200);
++ const box = waitingBox(page.text);
++ assert.ok(/Actor asked one question before spending anything on this task\./.test(box), 'the question box is as it was');
++ assert.ok(/Cancel/.test(box), 'the box keeps the Cancel button');
++ assert.ok(/The task keeps its place and its budget while you decide\./.test(page.text), 'the waiting note is as it was');
++ });
++ } finally {
++ await t.close();
++ }
++ done();
++})().catch((e) => { console.error(e); process.exit(1); });