openvibers/openvibe.actor Commits
Actor: the task page says it once — no dangling separator in the ledger, no repeated waiting box (#205)
Changes
@@@ -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 };});@@@ -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;}@@@ -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>`;}nnew file mode 100644@@@ -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); });
@@ -111,6 +111,21 @@
@@ -111,6 +111,21 @@
});
});
return out.steps.length || out.ledger ? out : null;
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. */
/** The divider a plan draws in the session: the step in hand, or the whole plan done. */
function planLine(p) {
function planLine(p) {
if (!p || !p.steps.length) return null;
if (!p || !p.steps.length) return null;
@@ -234,5 +249,5 @@
@@ -234,5 +249,5 @@
return out.replace(/[.?!]*$/, '') + '.';
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 };
});
});
@@ -81,13 +81,9 @@
@@ -81,13 +81,9 @@
labelled('To work out:', l.derive.length ? l.derive.join('; ') : 'none');
labelled('To work out:', l.derive.length ? l.derive.join('; ') : 'none');
}
}
if (p.progress) {
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;
ledger.hidden = !p.ledger && !p.progress;
}
}
@@ -993,6 +993,8 @@ ${existing ? html`<div class="notice" role="status">Already offered ${time(exist
@@ -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.
// The resume card (stage V2-9): the run was stopped inside an action, not before it started.
kind: questionEvent.question_kind === 'acting' ? 'acting' : 'question',
kind: questionEvent.question_kind === 'acting' ? 'acting' : 'question',
} : null;
} : 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 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 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+)"/);
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') : ''}
@@ -1040,7 +1042,7 @@ ${answerProblem ? notice(answerProblem, 'warn') : ''}
${assumeProblem ? notice(assumeProblem, 'warn') : ''}
${assumeProblem ? notice(assumeProblem, 'warn') : ''}
${forkProblem ? notice(forkProblem, 'warn') : ''}
${forkProblem ? notice(forkProblem, 'warn') : ''}
${question ? questionCard(row, question) : ''}
${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' ? benchBlock(row, offer) : ''}
${t.state === 'succeeded' ? followBlock(row, follows, followProblem) : ''}
${t.state === 'succeeded' ? followBlock(row, follows, followProblem) : ''}
${savedStepsBlock(row, saved, spans)}
${savedStepsBlock(row, saved, spans)}
@@ -1054,7 +1056,7 @@ ${open || look ? html`<figure class="last-look" id="last-look"${look ? '' : raw(
@@ -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>
<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>
<figcaption class="muted small">What your agent last saw on the screen${look ? html` · ${time(new Date(look.at).toISOString())}` : ''}</figcaption>
</figure>` : ''}
</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) : ''}
${!open && plan ? planCard(plan) : ''}
<div class="attempts" id="attempts">${attemptsOf(events).map(attemptBlock)}</div>
<div class="attempts" id="attempts">${attemptsOf(events).map(attemptBlock)}</div>
${traceBlock(spans, row.id)}
${traceBlock(spans, row.id)}
@@ -1121,7 +1123,7 @@ ${opts.editable ? html`<form method="post" action="/tasks/${opts.taskId}/assumpt
@@ -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>
${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 find:</b> ${ledger.lookup.length ? ledger.lookup.join('; ') : 'none'}</p>
<p><b>To work out:</b> ${ledger.derive.length ? ledger.derive.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>
</div>
</section>`;
</section>`;
}
}
@@ -1153,7 +1155,8 @@ ${problem ? html`<p class="notice warn" role="alert">${problem}</p>` : ''}
@@ -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>`;
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 === '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>`;
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>`;
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>`;
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1,112 @@
@@ -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); });