openvibers/openvibe.surf Commits
The web task page: what, where, what you can do; then the answer; then every step (#96)
Changes
@@@ -305,3 +305,45 @@ html[data-ov-motion=reduced] .sf-desk-page[data-sf-desk-open="1"] .sf-desk-state.sf-tiles::-webkit-scrollbar { display: none; }.sf-tiles > li { flex: 0 0 72%; min-width: 0; scroll-snap-align: start; }}++++/* The web task (11-07): one card first — what, where, what you can do — then the answer, then every step. */++.sf-task { gap: 16px; }++.sf-task > * { margin: 0; }++.sf-task-head { padding-top: 6px; }++.sf-task-q { margin: 0; font-size: clamp(1.5rem, 3.4vw, 2.1rem); line-height: 1.2; letter-spacing: -.02em; overflow-wrap: anywhere; text-wrap: balance; }++.sf-task-card { display: grid; gap: 10px; padding: 18px; border-radius: 22px; border: 1px solid color-mix(in srgb, var(--room-strong, var(--accent, #3b82f6)) 35%, var(--border, #1f2d47));++ background: radial-gradient(120% 140% at 0% 0%, color-mix(in srgb, var(--room-strong, var(--accent, #3b82f6)) 14%, transparent), transparent 60%), var(--bg-secondary, #101828); }++.sf-task-card p { margin: 0; }++.sf-task-now { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; font-size: 1.05rem; }++.sf-task-now b { font-weight: 800; }++.sf-task-latest { min-width: 0; color: var(--text-secondary, #96a7c2); font-size: .95rem; overflow-wrap: anywhere; }++.sf-task-dot { align-self: center; width: 10px; height: 10px; border-radius: 50%; background: var(--text-muted, #7386a3); }++.sf-task[data-sf-desk-open="1"] .sf-task-dot { background: #22c55e; animation: sf-desk-pulse 1.6s ease-out infinite; }++.sf-task[data-state="done"] .sf-task-dot, .sf-task[data-state="succeeded"] .sf-task-dot { background: #22c55e; }++.sf-task[data-state="failed"] .sf-task-dot, .sf-task[data-state="stopped"] .sf-task-dot { background: #ef4444; }++.sf-task-where { display: flex; align-items: center; gap: 8px; color: var(--text-secondary, #96a7c2); font-size: .92rem; }++.sf-task-where .ovk-i { width: 16px; height: 16px; flex: none; color: var(--room-strong, var(--accent, #3b82f6)); }++.sf-task-needs { padding: 10px 12px; border-radius: 12px; border: 1px solid color-mix(in srgb, #f59e0b 45%, transparent); background: color-mix(in srgb, #f59e0b 14%, transparent); font-weight: 650; }++.sf-task-acts { display: flex; flex-wrap: wrap; gap: 10px; }++.sf-task-acts .ovk-btn { min-height: 44px; }++.sf-task-more { display: flex; flex-wrap: wrap; gap: 8px; font-size: .9rem; }++.sf-task-more span { color: var(--text-muted, #7386a3); }++.sf-task [hidden] { display: none !important; }++.sf-task-answer h2 { margin: 0 0 10px; font-size: .76rem; letter-spacing: .08em; text-transform: uppercase; color: var(--text-muted, #7386a3); }++.sf-task-answer .sf-desk-report-text { font-size: 1rem; }++.sf-task-answer h3 { margin: 16px 0 8px; font-size: .76rem; letter-spacing: .08em; text-transform: uppercase; color: var(--text-muted, #7386a3); }++.sf-task-wait { margin: 0; color: var(--text-secondary, #96a7c2); }++.sf-task-sources { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 8px; }++.sf-task-sources a { display: inline-grid; gap: 1px; max-width: 100%; padding: 8px 12px; border-radius: 12px; border: 1px solid var(--border, #1f2d47); background: color-mix(in srgb, var(--text-primary, #e6edf7) 4%, transparent); text-decoration: none; }++.sf-task-sources a:hover { border-color: color-mix(in srgb, var(--room-strong, var(--accent, #3b82f6)) 50%, var(--border, #1f2d47)); }++.sf-task-sources b { font-size: .88rem; color: var(--text-primary, #e6edf7); }++.sf-task-sources span { font-size: .78rem; color: var(--text-muted, #7386a3); overflow-wrap: anywhere; }++.sf-task-steps summary { display: flex; align-items: center; gap: 6px; min-height: 44px; cursor: pointer; font-weight: 750; list-style: none; }++.sf-task-steps summary::-webkit-details-marker { display: none; }++.sf-task-steps summary::before { content: '›'; display: inline-block; width: 14px; font-size: 1.2em; transition: transform .2s; }++.sf-task-steps[open] summary::before { transform: rotate(90deg); }++.sf-task-count { color: var(--text-muted, #7386a3); font-weight: 600; }++.sf-task-steps ol { margin-top: 8px; }++.sf-task-rule { margin: 0; text-align: center; }++@media (prefers-reduced-motion: reduce) { .sf-task[data-sf-desk-open="1"] .sf-task-dot { animation: none; } .sf-task-steps summary::before { transition: none; } }++html[data-ov-motion=reduced] .sf-task[data-sf-desk-open="1"] .sf-task-dot { animation: none; }@@@ -363,10 +363,15 @@var u = safeUrl(value);return u && new URL(u).origin === 'https://openvibe.bot' && /^\/machines\/dev_[0-9A-HJKMNP-TV-Z]{26}\/screen$/.test(new URL(u).pathname) ? u : null;}++ var WORDS = { queued: 'Getting ready', running: 'Working', verifying: 'Checking the answer', succeeded: 'Done', done: 'Done', failed: 'Did not finish', stopped: 'Stopped', cancelled: 'Stopped' };++ var count = root.querySelector('[data-sf-desk-count]');++ var latest = root.querySelector('[data-sf-desk-latest]');function snapshot(t) {-- root.querySelector('[data-sf-desk-state]').textContent = t.state || 'Working';++ var st = t.state || 'running';++ root.setAttribute('data-state', st);++ root.querySelector('[data-sf-desk-state]').textContent = WORDS[st] || st;var desk = botDesk(t.desk && t.desk.url);-- root.querySelector('[data-sf-desk-note]').textContent = desk ? 'Your agent is working on its private desk. Open the live viewer to watch or take over.' : 'The agent has not opened a desk, or its desk has ended.';++ root.querySelector('[data-sf-desk-note]').textContent = desk ? 'On a private desk: a real browser of its own' : ['queued', 'running', 'verifying'].includes(st) ? 'Searching and reading; no desk open' : 'Searched and read the web';var links = root.querySelector('[data-sf-desk-links]');links.hidden = !desk;if (desk) {@@@ -382,8 +387,11 @@sources.forEach(function (value) {var url = safeUrl(value);if (!url) return;-- var li = document.createElement('li'), a = document.createElement('a');-- a.href = url; a.textContent = url; a.target = '_blank'; a.rel = 'nofollow noopener noreferrer';++ var li = document.createElement('li'), a = document.createElement('a'), u = new URL(url);++ a.href = url; a.target = '_blank'; a.rel = 'nofollow noopener noreferrer';++ var host = document.createElement('b'); host.textContent = u.hostname.replace(/^www\./, ''); a.appendChild(host);++ var path = (u.pathname + u.search).replace(/\/$/, '');++ if (path) { var sp = document.createElement('span'); sp.textContent = path.length > 48 ? path.slice(0, 47) + '…' : path; a.appendChild(sp); }li.appendChild(a); list.appendChild(li);});root.querySelector('[data-sf-desk-sources-empty]').hidden = !!list.children.length;@@@ -394,6 +402,7 @@if (timer) clearInterval(timer);if (stream) stream.close();root.setAttribute('data-sf-desk-open', '0'); root.querySelector('[data-sf-desk-steps-note]').textContent = 'The task has ended.';++ if (latest) latest.textContent = '';}}function refresh() {@@@ -412,6 +421,8 @@line.className = 'sf-desk-step';line.textContent = [item.step === 'tool_call' ? 'Used ' + (item.tool || 'a tool') : item.step === 'tool_result' ? 'Evidence' : 'Step', item.text || item.input || ''].filter(Boolean).join(': ').slice(0, 1000);steps.appendChild(line);++ if (count) count.textContent = '(' + steps.children.length + ')';++ if (latest) latest.textContent = line.textContent.slice(0, 140);root.querySelector('[data-sf-desk-steps-note]').textContent = 'Updating live as the agent works.';} catch (_) { /* ignore a malformed event */ }}@@@ -299,21 +299,43 @@ const deskUrl = (value) => {return url && new URL(url).origin === 'https://openvibe.bot' && /^\/machines\/dev_[0-9A-HJKMNP-TV-Z]{26}\/screen$/.test(new URL(url).pathname) ? url : null;};++// The words a person reads for each state (the same in surf.js).++const TASK_STATE = { queued: 'Getting ready', running: 'Working', verifying: 'Checking the answer', succeeded: 'Done', done: 'Done', failed: 'Did not finish', stopped: 'Stopped', cancelled: 'Stopped' };++const sourceChip = (url) => {++ const u = new URL(url);++ const path = `${u.pathname}${u.search}`.replace(/\/$/, '');++ return `<li><a href="${esc(url)}" target="_blank" rel="nofollow noopener noreferrer"><b>${esc(u.hostname.replace(/^www\./, ''))}</b>${path ? `<span>${esc(path.length > 48 ? `${path.slice(0, 47)}…` : path)}</span>` : ''}</a></li>`;++};++++/**++ * One web task (plan Z, 11-07: "Surf and computer and actor all kinda go hand in hand … clear interfaces"). The same++ * three things every agent surface shows, in the same order: what it is doing (one line), where it works, and what you++ * can do (take over, watch). Then the answer with its sources; every step sits behind a count. surf.js keeps it live.++ */function webTask({ task, actorUrl } = {}) {const id = String(task.id);const actor = `${String(actorUrl).replace(/\/+$/, '')}/tasks/${encodeURIComponent(id)}`;const desk = deskUrl(task.desk && task.desk.url);const sources = task.result && Array.isArray(task.result.sources) ? task.result.sources.map(safeWebUrl).filter(Boolean) : [];const report = task.result && typeof task.result.answer === 'string' ? task.result.answer : '';-- const body = `<div class="sf-wrap sf-page-pad sf-narrow sf-desk-page" data-sf-desk-task="${esc(id)}" data-sf-desk-open="${['queued', 'running', 'verifying'].includes(task.state) ? '1' : '0'}">--<header class="sf-page-h sf-desk-head"><p class="sf-kicker">Web · Surf with Vibe</p><h1>Surf report</h1><p class="sf-desk-query">${esc(task.task || 'Your web task')}</p><p class="sf-desk-state" data-sf-desk-state role="status">${esc(task.state || 'queued')}</p></header>--<section class="sf-card sf-desk-view" aria-labelledby="sf-desk-view-h"><h2 id="sf-desk-view-h">Agent desk</h2><p data-sf-desk-note>${desk ? 'Your agent is working on its private desk. Open the live viewer to watch or take over.' : 'The agent has not opened a desk, or its desk has ended.'}</p>--<p data-sf-desk-links${desk ? '' : ' hidden'}><a class="sf-desk-watch" data-sf-desk-watch href="${esc(desk || '#')}" target="_blank" rel="noopener noreferrer">Watch live</a> · <a class="sf-desk-takeover" data-sf-desk-takeover href="${esc(desk || '#')}" target="_blank" rel="noopener noreferrer">Take over</a></p><p data-sf-desk-needs${task.needs_you ? '' : ' hidden'}>${esc(task.needs_you && task.needs_you.what || '')}</p></section>--<section class="sf-card sf-desk-steps" aria-labelledby="sf-desk-steps-h"><h2 id="sf-desk-steps-h">Live steps</h2><ol data-sf-desk-steps></ol><p class="sf-fine" data-sf-desk-steps-note>Connecting to the agent’s live steps…</p></section>--<section class="sf-card sf-desk-evidence" aria-labelledby="sf-desk-evidence-h"><h2 id="sf-desk-evidence-h">Sources cited</h2><ul data-sf-desk-sources>${sources.map((url) => `<li><a href="${esc(url)}" target="_blank" rel="nofollow noopener noreferrer">${esc(url)}</a></li>`).join('')}</ul><p data-sf-desk-sources-empty${sources.length ? ' hidden' : ''}>Sources will appear when the report is ready.</p></section>--<section class="sf-card sf-desk-report" aria-labelledby="sf-desk-report-h"><h2 id="sf-desk-report-h">Final report</h2><div class="sf-desk-report-text" data-sf-desk-report>${esc(report)}</div><p data-sf-desk-report-empty${report ? ' hidden' : ''}>The agent is still working.</p></section>--<p class="sf-fine sf-desk-actor"><a href="${esc(actor)}" target="_blank" rel="noopener noreferrer">Open full task in Actor</a> · <a href="/web">Start another surf</a></p></div>`;-- return layout({ title: 'Surf report', path: `/web/t/${encodeURIComponent(id)}`, robots: 'noindex, nofollow', body, active: '/web' });++ const state = String(task.state || 'queued');++ const open = ['queued', 'running', 'verifying'].includes(state);++ const body = `<div class="sf-wrap sf-page-pad sf-narrow sf-desk-page sf-task" data-sf-desk-task="${esc(id)}" data-sf-desk-open="${open ? '1' : '0'}" data-state="${esc(state)}">++<header class="sf-task-head"><p class="sf-kicker">Surf with Vibe</p><h1 class="sf-task-q">${esc(task.task || 'Your web task')}</h1></header>++<section class="sf-task-card" aria-label="What your agent is doing">++<p class="sf-task-now"><span class="sf-task-dot" aria-hidden="true"></span><b data-sf-desk-state role="status">${esc(TASK_STATE[state] || state)}</b><span class="sf-task-latest" data-sf-desk-latest>${open ? 'Your agent is on it.' : ''}</span></p>++<p class="sf-task-where">${kit.icon('globe')}<span data-sf-desk-note>${desk ? 'On a private desk: a real browser of its own' : open ? 'Searching and reading; no desk open' : 'Searched and read the web'}</span></p>++<p class="sf-task-needs" data-sf-desk-needs${task.needs_you ? '' : ' hidden'}>${esc(task.needs_you && task.needs_you.what || '')}</p>++<p class="sf-task-acts" data-sf-desk-links${desk ? '' : ' hidden'}><a class="ovk-btn ovk-btn--primary sf-desk-takeover" data-sf-desk-takeover href="${esc(desk || '#')}" target="_blank" rel="noopener noreferrer">Take over</a><a class="ovk-btn sf-desk-watch" data-sf-desk-watch href="${esc(desk || '#')}" target="_blank" rel="noopener noreferrer">Watch the desk</a></p>++<p class="sf-task-more"><a href="${esc(actor)}" target="_blank" rel="noopener noreferrer">Open in Actor</a><span aria-hidden="true">·</span><a href="/web">Ask another</a></p>++</section>++<section class="sf-card sf-desk-report sf-task-answer" aria-labelledby="sf-desk-report-h"><h2 id="sf-desk-report-h">The answer</h2>++<div class="sf-desk-report-text" data-sf-desk-report>${esc(report)}</div><p class="sf-task-wait" data-sf-desk-report-empty${report ? ' hidden' : ''}>It appears here when your agent finishes, with every source it used.</p>++<div class="sf-task-cites"><h3>Sources</h3><ul class="sf-task-sources" data-sf-desk-sources>${sources.map(sourceChip).join('')}</ul><p class="sf-task-wait" data-sf-desk-sources-empty${sources.length ? ' hidden' : ''}>None yet.</p></div>++</section>++<details class="sf-card sf-desk-steps sf-task-steps"${open ? ' open' : ''}><summary>Every step <span class="sf-task-count" data-sf-desk-count></span></summary><ol data-sf-desk-steps></ol><p class="sf-fine" data-sf-desk-steps-note>Connecting to the agent’s live steps…</p></details>++<p class="sf-fine sf-task-rule">It never signs in or pays for you: at a login, a captcha or a checkout it stops and hands over.</p></div>`;++ return layout({ title: 'Surf task', path: `/web/t/${encodeURIComponent(id)}`, robots: 'noindex, nofollow', body, active: '/web' });}function webTaskError({ notFound = false } = {}) {
@@ -305,3 +305,45 @@ html[data-ov-motion=reduced] .sf-desk-page[data-sf-desk-open="1"] .sf-desk-state
@@ -305,3 +305,45 @@ html[data-ov-motion=reduced] .sf-desk-page[data-sf-desk-open="1"] .sf-desk-state
.sf-tiles::-webkit-scrollbar { display: none; }
.sf-tiles::-webkit-scrollbar { display: none; }
.sf-tiles > li { flex: 0 0 72%; min-width: 0; scroll-snap-align: start; }
.sf-tiles > li { flex: 0 0 72%; min-width: 0; scroll-snap-align: start; }
}
}
+
+/* The web task (11-07): one card first — what, where, what you can do — then the answer, then every step. */
+.sf-task { gap: 16px; }
+.sf-task > * { margin: 0; }
+.sf-task-head { padding-top: 6px; }
+.sf-task-q { margin: 0; font-size: clamp(1.5rem, 3.4vw, 2.1rem); line-height: 1.2; letter-spacing: -.02em; overflow-wrap: anywhere; text-wrap: balance; }
+.sf-task-card { display: grid; gap: 10px; padding: 18px; border-radius: 22px; border: 1px solid color-mix(in srgb, var(--room-strong, var(--accent, #3b82f6)) 35%, var(--border, #1f2d47));
+ background: radial-gradient(120% 140% at 0% 0%, color-mix(in srgb, var(--room-strong, var(--accent, #3b82f6)) 14%, transparent), transparent 60%), var(--bg-secondary, #101828); }
+.sf-task-card p { margin: 0; }
+.sf-task-now { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; font-size: 1.05rem; }
+.sf-task-now b { font-weight: 800; }
+.sf-task-latest { min-width: 0; color: var(--text-secondary, #96a7c2); font-size: .95rem; overflow-wrap: anywhere; }
+.sf-task-dot { align-self: center; width: 10px; height: 10px; border-radius: 50%; background: var(--text-muted, #7386a3); }
+.sf-task[data-sf-desk-open="1"] .sf-task-dot { background: #22c55e; animation: sf-desk-pulse 1.6s ease-out infinite; }
+.sf-task[data-state="done"] .sf-task-dot, .sf-task[data-state="succeeded"] .sf-task-dot { background: #22c55e; }
+.sf-task[data-state="failed"] .sf-task-dot, .sf-task[data-state="stopped"] .sf-task-dot { background: #ef4444; }
+.sf-task-where { display: flex; align-items: center; gap: 8px; color: var(--text-secondary, #96a7c2); font-size: .92rem; }
+.sf-task-where .ovk-i { width: 16px; height: 16px; flex: none; color: var(--room-strong, var(--accent, #3b82f6)); }
+.sf-task-needs { padding: 10px 12px; border-radius: 12px; border: 1px solid color-mix(in srgb, #f59e0b 45%, transparent); background: color-mix(in srgb, #f59e0b 14%, transparent); font-weight: 650; }
+.sf-task-acts { display: flex; flex-wrap: wrap; gap: 10px; }
+.sf-task-acts .ovk-btn { min-height: 44px; }
+.sf-task-more { display: flex; flex-wrap: wrap; gap: 8px; font-size: .9rem; }
+.sf-task-more span { color: var(--text-muted, #7386a3); }
+.sf-task [hidden] { display: none !important; }
+.sf-task-answer h2 { margin: 0 0 10px; font-size: .76rem; letter-spacing: .08em; text-transform: uppercase; color: var(--text-muted, #7386a3); }
+.sf-task-answer .sf-desk-report-text { font-size: 1rem; }
+.sf-task-answer h3 { margin: 16px 0 8px; font-size: .76rem; letter-spacing: .08em; text-transform: uppercase; color: var(--text-muted, #7386a3); }
+.sf-task-wait { margin: 0; color: var(--text-secondary, #96a7c2); }
+.sf-task-sources { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 8px; }
+.sf-task-sources a { display: inline-grid; gap: 1px; max-width: 100%; padding: 8px 12px; border-radius: 12px; border: 1px solid var(--border, #1f2d47); background: color-mix(in srgb, var(--text-primary, #e6edf7) 4%, transparent); text-decoration: none; }
+.sf-task-sources a:hover { border-color: color-mix(in srgb, var(--room-strong, var(--accent, #3b82f6)) 50%, var(--border, #1f2d47)); }
+.sf-task-sources b { font-size: .88rem; color: var(--text-primary, #e6edf7); }
+.sf-task-sources span { font-size: .78rem; color: var(--text-muted, #7386a3); overflow-wrap: anywhere; }
+.sf-task-steps summary { display: flex; align-items: center; gap: 6px; min-height: 44px; cursor: pointer; font-weight: 750; list-style: none; }
+.sf-task-steps summary::-webkit-details-marker { display: none; }
+.sf-task-steps summary::before { content: '›'; display: inline-block; width: 14px; font-size: 1.2em; transition: transform .2s; }
+.sf-task-steps[open] summary::before { transform: rotate(90deg); }
+.sf-task-count { color: var(--text-muted, #7386a3); font-weight: 600; }
+.sf-task-steps ol { margin-top: 8px; }
+.sf-task-rule { margin: 0; text-align: center; }
+@media (prefers-reduced-motion: reduce) { .sf-task[data-sf-desk-open="1"] .sf-task-dot { animation: none; } .sf-task-steps summary::before { transition: none; } }
+html[data-ov-motion=reduced] .sf-task[data-sf-desk-open="1"] .sf-task-dot { animation: none; }
@@ -363,10 +363,15 @@
@@ -363,10 +363,15 @@
var u = safeUrl(value);
var u = safeUrl(value);
return u && new URL(u).origin === 'https://openvibe.bot' && /^\/machines\/dev_[0-9A-HJKMNP-TV-Z]{26}\/screen$/.test(new URL(u).pathname) ? u : null;
return u && new URL(u).origin === 'https://openvibe.bot' && /^\/machines\/dev_[0-9A-HJKMNP-TV-Z]{26}\/screen$/.test(new URL(u).pathname) ? u : null;
}
}
+ var WORDS = { queued: 'Getting ready', running: 'Working', verifying: 'Checking the answer', succeeded: 'Done', done: 'Done', failed: 'Did not finish', stopped: 'Stopped', cancelled: 'Stopped' };
+ var count = root.querySelector('[data-sf-desk-count]');
+ var latest = root.querySelector('[data-sf-desk-latest]');
function snapshot(t) {
function snapshot(t) {
- root.querySelector('[data-sf-desk-state]').textContent = t.state || 'Working';
+ var st = t.state || 'running';
+ root.setAttribute('data-state', st);
+ root.querySelector('[data-sf-desk-state]').textContent = WORDS[st] || st;
var desk = botDesk(t.desk && t.desk.url);
var desk = botDesk(t.desk && t.desk.url);
- root.querySelector('[data-sf-desk-note]').textContent = desk ? 'Your agent is working on its private desk. Open the live viewer to watch or take over.' : 'The agent has not opened a desk, or its desk has ended.';
+ root.querySelector('[data-sf-desk-note]').textContent = desk ? 'On a private desk: a real browser of its own' : ['queued', 'running', 'verifying'].includes(st) ? 'Searching and reading; no desk open' : 'Searched and read the web';
var links = root.querySelector('[data-sf-desk-links]');
var links = root.querySelector('[data-sf-desk-links]');
links.hidden = !desk;
links.hidden = !desk;
if (desk) {
if (desk) {
@@ -382,8 +387,11 @@
@@ -382,8 +387,11 @@
sources.forEach(function (value) {
sources.forEach(function (value) {
var url = safeUrl(value);
var url = safeUrl(value);
if (!url) return;
if (!url) return;
- var li = document.createElement('li'), a = document.createElement('a');
- a.href = url; a.textContent = url; a.target = '_blank'; a.rel = 'nofollow noopener noreferrer';
+ var li = document.createElement('li'), a = document.createElement('a'), u = new URL(url);
+ a.href = url; a.target = '_blank'; a.rel = 'nofollow noopener noreferrer';
+ var host = document.createElement('b'); host.textContent = u.hostname.replace(/^www\./, ''); a.appendChild(host);
+ var path = (u.pathname + u.search).replace(/\/$/, '');
+ if (path) { var sp = document.createElement('span'); sp.textContent = path.length > 48 ? path.slice(0, 47) + '…' : path; a.appendChild(sp); }
li.appendChild(a); list.appendChild(li);
li.appendChild(a); list.appendChild(li);
});
});
root.querySelector('[data-sf-desk-sources-empty]').hidden = !!list.children.length;
root.querySelector('[data-sf-desk-sources-empty]').hidden = !!list.children.length;
@@ -394,6 +402,7 @@
@@ -394,6 +402,7 @@
if (timer) clearInterval(timer);
if (timer) clearInterval(timer);
if (stream) stream.close();
if (stream) stream.close();
root.setAttribute('data-sf-desk-open', '0'); root.querySelector('[data-sf-desk-steps-note]').textContent = 'The task has ended.';
root.setAttribute('data-sf-desk-open', '0'); root.querySelector('[data-sf-desk-steps-note]').textContent = 'The task has ended.';
+ if (latest) latest.textContent = '';
}
}
}
}
function refresh() {
function refresh() {
@@ -412,6 +421,8 @@
@@ -412,6 +421,8 @@
line.className = 'sf-desk-step';
line.className = 'sf-desk-step';
line.textContent = [item.step === 'tool_call' ? 'Used ' + (item.tool || 'a tool') : item.step === 'tool_result' ? 'Evidence' : 'Step', item.text || item.input || ''].filter(Boolean).join(': ').slice(0, 1000);
line.textContent = [item.step === 'tool_call' ? 'Used ' + (item.tool || 'a tool') : item.step === 'tool_result' ? 'Evidence' : 'Step', item.text || item.input || ''].filter(Boolean).join(': ').slice(0, 1000);
steps.appendChild(line);
steps.appendChild(line);
+ if (count) count.textContent = '(' + steps.children.length + ')';
+ if (latest) latest.textContent = line.textContent.slice(0, 140);
root.querySelector('[data-sf-desk-steps-note]').textContent = 'Updating live as the agent works.';
root.querySelector('[data-sf-desk-steps-note]').textContent = 'Updating live as the agent works.';
} catch (_) { /* ignore a malformed event */ }
} catch (_) { /* ignore a malformed event */ }
}
}
@@ -299,21 +299,43 @@ const deskUrl = (value) => {
@@ -299,21 +299,43 @@ const deskUrl = (value) => {
return url && new URL(url).origin === 'https://openvibe.bot' && /^\/machines\/dev_[0-9A-HJKMNP-TV-Z]{26}\/screen$/.test(new URL(url).pathname) ? url : null;
return url && new URL(url).origin === 'https://openvibe.bot' && /^\/machines\/dev_[0-9A-HJKMNP-TV-Z]{26}\/screen$/.test(new URL(url).pathname) ? url : null;
};
};
+// The words a person reads for each state (the same in surf.js).
+const TASK_STATE = { queued: 'Getting ready', running: 'Working', verifying: 'Checking the answer', succeeded: 'Done', done: 'Done', failed: 'Did not finish', stopped: 'Stopped', cancelled: 'Stopped' };
+const sourceChip = (url) => {
+ const u = new URL(url);
+ const path = `${u.pathname}${u.search}`.replace(/\/$/, '');
+ return `<li><a href="${esc(url)}" target="_blank" rel="nofollow noopener noreferrer"><b>${esc(u.hostname.replace(/^www\./, ''))}</b>${path ? `<span>${esc(path.length > 48 ? `${path.slice(0, 47)}…` : path)}</span>` : ''}</a></li>`;
+};
+
+/**
+ * One web task (plan Z, 11-07: "Surf and computer and actor all kinda go hand in hand … clear interfaces"). The same
+ * three things every agent surface shows, in the same order: what it is doing (one line), where it works, and what you
+ * can do (take over, watch). Then the answer with its sources; every step sits behind a count. surf.js keeps it live.
+ */
function webTask({ task, actorUrl } = {}) {
function webTask({ task, actorUrl } = {}) {
const id = String(task.id);
const id = String(task.id);
const actor = `${String(actorUrl).replace(/\/+$/, '')}/tasks/${encodeURIComponent(id)}`;
const actor = `${String(actorUrl).replace(/\/+$/, '')}/tasks/${encodeURIComponent(id)}`;
const desk = deskUrl(task.desk && task.desk.url);
const desk = deskUrl(task.desk && task.desk.url);
const sources = task.result && Array.isArray(task.result.sources) ? task.result.sources.map(safeWebUrl).filter(Boolean) : [];
const sources = task.result && Array.isArray(task.result.sources) ? task.result.sources.map(safeWebUrl).filter(Boolean) : [];
const report = task.result && typeof task.result.answer === 'string' ? task.result.answer : '';
const report = task.result && typeof task.result.answer === 'string' ? task.result.answer : '';
- const body = `<div class="sf-wrap sf-page-pad sf-narrow sf-desk-page" data-sf-desk-task="${esc(id)}" data-sf-desk-open="${['queued', 'running', 'verifying'].includes(task.state) ? '1' : '0'}">
-<header class="sf-page-h sf-desk-head"><p class="sf-kicker">Web · Surf with Vibe</p><h1>Surf report</h1><p class="sf-desk-query">${esc(task.task || 'Your web task')}</p><p class="sf-desk-state" data-sf-desk-state role="status">${esc(task.state || 'queued')}</p></header>
-<section class="sf-card sf-desk-view" aria-labelledby="sf-desk-view-h"><h2 id="sf-desk-view-h">Agent desk</h2><p data-sf-desk-note>${desk ? 'Your agent is working on its private desk. Open the live viewer to watch or take over.' : 'The agent has not opened a desk, or its desk has ended.'}</p>
-<p data-sf-desk-links${desk ? '' : ' hidden'}><a class="sf-desk-watch" data-sf-desk-watch href="${esc(desk || '#')}" target="_blank" rel="noopener noreferrer">Watch live</a> · <a class="sf-desk-takeover" data-sf-desk-takeover href="${esc(desk || '#')}" target="_blank" rel="noopener noreferrer">Take over</a></p><p data-sf-desk-needs${task.needs_you ? '' : ' hidden'}>${esc(task.needs_you && task.needs_you.what || '')}</p></section>
-<section class="sf-card sf-desk-steps" aria-labelledby="sf-desk-steps-h"><h2 id="sf-desk-steps-h">Live steps</h2><ol data-sf-desk-steps></ol><p class="sf-fine" data-sf-desk-steps-note>Connecting to the agent’s live steps…</p></section>
-<section class="sf-card sf-desk-evidence" aria-labelledby="sf-desk-evidence-h"><h2 id="sf-desk-evidence-h">Sources cited</h2><ul data-sf-desk-sources>${sources.map((url) => `<li><a href="${esc(url)}" target="_blank" rel="nofollow noopener noreferrer">${esc(url)}</a></li>`).join('')}</ul><p data-sf-desk-sources-empty${sources.length ? ' hidden' : ''}>Sources will appear when the report is ready.</p></section>
-<section class="sf-card sf-desk-report" aria-labelledby="sf-desk-report-h"><h2 id="sf-desk-report-h">Final report</h2><div class="sf-desk-report-text" data-sf-desk-report>${esc(report)}</div><p data-sf-desk-report-empty${report ? ' hidden' : ''}>The agent is still working.</p></section>
-<p class="sf-fine sf-desk-actor"><a href="${esc(actor)}" target="_blank" rel="noopener noreferrer">Open full task in Actor</a> · <a href="/web">Start another surf</a></p></div>`;
- return layout({ title: 'Surf report', path: `/web/t/${encodeURIComponent(id)}`, robots: 'noindex, nofollow', body, active: '/web' });
+ const state = String(task.state || 'queued');
+ const open = ['queued', 'running', 'verifying'].includes(state);
+ const body = `<div class="sf-wrap sf-page-pad sf-narrow sf-desk-page sf-task" data-sf-desk-task="${esc(id)}" data-sf-desk-open="${open ? '1' : '0'}" data-state="${esc(state)}">
+<header class="sf-task-head"><p class="sf-kicker">Surf with Vibe</p><h1 class="sf-task-q">${esc(task.task || 'Your web task')}</h1></header>
+<section class="sf-task-card" aria-label="What your agent is doing">
+<p class="sf-task-now"><span class="sf-task-dot" aria-hidden="true"></span><b data-sf-desk-state role="status">${esc(TASK_STATE[state] || state)}</b><span class="sf-task-latest" data-sf-desk-latest>${open ? 'Your agent is on it.' : ''}</span></p>
+<p class="sf-task-where">${kit.icon('globe')}<span data-sf-desk-note>${desk ? 'On a private desk: a real browser of its own' : open ? 'Searching and reading; no desk open' : 'Searched and read the web'}</span></p>
+<p class="sf-task-needs" data-sf-desk-needs${task.needs_you ? '' : ' hidden'}>${esc(task.needs_you && task.needs_you.what || '')}</p>
+<p class="sf-task-acts" data-sf-desk-links${desk ? '' : ' hidden'}><a class="ovk-btn ovk-btn--primary sf-desk-takeover" data-sf-desk-takeover href="${esc(desk || '#')}" target="_blank" rel="noopener noreferrer">Take over</a><a class="ovk-btn sf-desk-watch" data-sf-desk-watch href="${esc(desk || '#')}" target="_blank" rel="noopener noreferrer">Watch the desk</a></p>
+<p class="sf-task-more"><a href="${esc(actor)}" target="_blank" rel="noopener noreferrer">Open in Actor</a><span aria-hidden="true">·</span><a href="/web">Ask another</a></p>
+</section>
+<section class="sf-card sf-desk-report sf-task-answer" aria-labelledby="sf-desk-report-h"><h2 id="sf-desk-report-h">The answer</h2>
+<div class="sf-desk-report-text" data-sf-desk-report>${esc(report)}</div><p class="sf-task-wait" data-sf-desk-report-empty${report ? ' hidden' : ''}>It appears here when your agent finishes, with every source it used.</p>
+<div class="sf-task-cites"><h3>Sources</h3><ul class="sf-task-sources" data-sf-desk-sources>${sources.map(sourceChip).join('')}</ul><p class="sf-task-wait" data-sf-desk-sources-empty${sources.length ? ' hidden' : ''}>None yet.</p></div>
+</section>
+<details class="sf-card sf-desk-steps sf-task-steps"${open ? ' open' : ''}><summary>Every step <span class="sf-task-count" data-sf-desk-count></span></summary><ol data-sf-desk-steps></ol><p class="sf-fine" data-sf-desk-steps-note>Connecting to the agent’s live steps…</p></details>
+<p class="sf-fine sf-task-rule">It never signs in or pays for you: at a login, a captcha or a checkout it stops and hands over.</p></div>`;
+ return layout({ title: 'Surf task', path: `/web/t/${encodeURIComponent(id)}`, robots: 'noindex, nofollow', body, active: '/web' });
}
}
function webTaskError({ notFound = false } = {}) {
function webTaskError({ notFound = false } = {}) {