openvibers/openvibe.surf Commits
Surf task card: the agent's screen in a frame with a Live badge (#101)
Changes
@@@ -221,8 +221,23 @@ html.ov-motion .sf-party-playing .sf-party-dot { animation: sf-pulse 1.6s ease-o/* Surf with Vibe: the task page (/web/t/:id): state, desk links, live steps as a timeline, sources, the report */.sf-desk-page { display: grid; gap: 14px; }/* The Computer's live still (plan 2026-11-07): size and aspect only; Opus designs the rest. */--.ov-task-still { display: block; width: 100%; max-width: 320px; aspect-ratio: 16 / 9; object-fit: cover; }++.ov-task-still { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; }.ov-task-still[hidden] { display: none; }++/* The Computer on the card (Opus, 11-07): the agent's screen in a frame, a Live badge on it, the machine under it. The++ frame shows only while there is a picture (the script shows and hides the image, every 2 s while the page is seen). */++.sf-task-screen { position: relative; margin: 4px 0 0; max-width: 420px; border-radius: 14px; overflow: hidden; border: 1px solid var(--border, #1f2d47);++ background: #05070c; box-shadow: 0 10px 28px rgba(0, 0, 0, .28); }++.sf-task-screen:has(.ov-task-still[hidden]) { display: none; }++.sf-task-screen-open { display: block; color: inherit; }++.sf-task-screen-open[href]:focus-visible { outline: 3px solid var(--accent, #38bdf8); outline-offset: -3px; }++.sf-task-screen:has(.sf-task-screen-open[href]) figcaption::after { content: "Open \2197"; flex: none; margin-left: auto; opacity: .85; }++.sf-task-screen figcaption { pointer-events: none; position: absolute; left: 0; right: 0; bottom: 0; display: flex; align-items: center; gap: 8px; padding: 18px 10px 8px;++ font-size: .8rem; font-weight: 700; color: #e6edf7; background: linear-gradient(to top, rgba(0, 0, 0, .72), transparent); }++.sf-task-screen figcaption > span:last-child { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }++.sf-task-live { flex: none; display: inline-flex; align-items: center; gap: 5px; padding: 2px 8px; border-radius: 999px; background: #dc2626; color: #fff;++ font-size: .7rem; letter-spacing: .06em; text-transform: uppercase; }++.sf-task-live::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: #fff; }++.sf-task[data-state="queued"] .sf-task-live { background: #475569; }.sf-desk-head h1 { margin-bottom: 6px; }.sf-desk-query { margin: 0; font-size: 1.05rem; line-height: 1.5; color: var(--text-primary, #e6edf7); overflow-wrap: anywhere; }.sf-desk-state { display: inline-flex; align-items: center; gap: 8px; margin: 12px 0 0; padding: 5px 12px 5px 10px; border: 1px solid var(--border, #1f2d47); border-radius: 999px; background: var(--bg-secondary, #101828); font-size: .8rem; font-weight: 800; letter-spacing: .03em; text-transform: capitalize; }@@@ -420,6 +420,9 @@if (giveForm) giveForm.hidden = !driving;var links = root.querySelector('[data-sf-desk-links]');links.hidden = !desk;++ // Tapping the screen opens the full view: the desk, when the task has one (plan Z, 11-07).++ var screen = root.querySelector('[data-sf-desk-screen]');++ if (screen) { if (desk) screen.href = desk; else screen.removeAttribute('href'); }if (desk) {root.querySelector('[data-sf-desk-watch]').href = desk;root.querySelector('[data-sf-desk-takeover]').href = desk;@@@ -331,7 +331,7 @@ function webTask({ task, actorUrl } = {}) {<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>${esc(where)}</span></p>--<img class="ov-task-still" data-sf-desk-still data-src="/api/v1/web/tasks/${esc(id)}/still" alt="Your agent's screen now" width="320" height="180" hidden>++<figure class="sf-task-screen"><a class="sf-task-screen-open" data-sf-desk-screen${desk ? ` href="${esc(desk)}"` : ''} target="_blank" rel="noopener noreferrer"><img class="ov-task-still" data-sf-desk-still data-src="/api/v1/web/tasks/${esc(id)}/still" alt="Your agent's screen now" width="320" height="180" hidden></a><figcaption><span class="sf-task-live">Live</span><span>Your agent’s screen</span></figcaption></figure><p class="sf-task-needs" data-sf-desk-needs${task.needs_you ? '' : ' hidden'}>${esc(task.needs_you && task.needs_you.what || '')}</p><form class="sf-task-takeover" method="post" action="/api/v1/web/tasks/${esc(id)}/takeover" data-sf-desk-takeover-form${desk && open && !driving ? '' : ' hidden'}><button class="ovk-btn ovk-btn--primary sf-desk-takeover" type="submit" data-sf-desk-takeover>Take over</button></form><form class="sf-task-giveback" method="post" action="/api/v1/web/tasks/${esc(id)}/giveback" data-sf-desk-giveback-form${driving ? '' : ' hidden'}><button class="ovk-btn sf-desk-takeover" type="submit" data-sf-desk-giveback>Give it back</button></form>@@@ -25,6 +25,8 @@ const COMPUTER = { session_id: 'ses_01JAB2C3D4E5F6G7H8J9K0MNPQR', machine: 'a shconst r = await t.get(`/web/t/${id}`, { cookie: signedIn });assert.strictEqual(r.status, 200, r.text);assert.match(r.text, /class="ov-task-still"/);++ // The screen in a frame with a Live badge and the machine under it (Opus, 11-07).++ assert.match(r.text, /<figure class="sf-task-screen"><a class="sf-task-screen-open" data-sf-desk-screen[^>]*><img class="ov-task-still"[^>]*hidden><\/a><figcaption><span class="sf-task-live">Live<\/span><span>Your agent’s screen<\/span><\/figcaption><\/figure>/);assert.match(r.text, new RegExp(`data-src="/api/v1/web/tasks/${id}/still"`));assert.match(r.text, /a shared desk host/);assert.match(r.text, /metered by the minute/);
@@ -221,8 +221,23 @@ html.ov-motion .sf-party-playing .sf-party-dot { animation: sf-pulse 1.6s ease-o
@@ -221,8 +221,23 @@ html.ov-motion .sf-party-playing .sf-party-dot { animation: sf-pulse 1.6s ease-o
/* Surf with Vibe: the task page (/web/t/:id): state, desk links, live steps as a timeline, sources, the report */
/* Surf with Vibe: the task page (/web/t/:id): state, desk links, live steps as a timeline, sources, the report */
.sf-desk-page { display: grid; gap: 14px; }
.sf-desk-page { display: grid; gap: 14px; }
/* The Computer's live still (plan 2026-11-07): size and aspect only; Opus designs the rest. */
/* The Computer's live still (plan 2026-11-07): size and aspect only; Opus designs the rest. */
-.ov-task-still { display: block; width: 100%; max-width: 320px; aspect-ratio: 16 / 9; object-fit: cover; }
+.ov-task-still { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; }
.ov-task-still[hidden] { display: none; }
.ov-task-still[hidden] { display: none; }
+/* The Computer on the card (Opus, 11-07): the agent's screen in a frame, a Live badge on it, the machine under it. The
+ frame shows only while there is a picture (the script shows and hides the image, every 2 s while the page is seen). */
+.sf-task-screen { position: relative; margin: 4px 0 0; max-width: 420px; border-radius: 14px; overflow: hidden; border: 1px solid var(--border, #1f2d47);
+ background: #05070c; box-shadow: 0 10px 28px rgba(0, 0, 0, .28); }
+.sf-task-screen:has(.ov-task-still[hidden]) { display: none; }
+.sf-task-screen-open { display: block; color: inherit; }
+.sf-task-screen-open[href]:focus-visible { outline: 3px solid var(--accent, #38bdf8); outline-offset: -3px; }
+.sf-task-screen:has(.sf-task-screen-open[href]) figcaption::after { content: "Open \2197"; flex: none; margin-left: auto; opacity: .85; }
+.sf-task-screen figcaption { pointer-events: none; position: absolute; left: 0; right: 0; bottom: 0; display: flex; align-items: center; gap: 8px; padding: 18px 10px 8px;
+ font-size: .8rem; font-weight: 700; color: #e6edf7; background: linear-gradient(to top, rgba(0, 0, 0, .72), transparent); }
+.sf-task-screen figcaption > span:last-child { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.sf-task-live { flex: none; display: inline-flex; align-items: center; gap: 5px; padding: 2px 8px; border-radius: 999px; background: #dc2626; color: #fff;
+ font-size: .7rem; letter-spacing: .06em; text-transform: uppercase; }
+.sf-task-live::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: #fff; }
+.sf-task[data-state="queued"] .sf-task-live { background: #475569; }
.sf-desk-head h1 { margin-bottom: 6px; }
.sf-desk-head h1 { margin-bottom: 6px; }
.sf-desk-query { margin: 0; font-size: 1.05rem; line-height: 1.5; color: var(--text-primary, #e6edf7); overflow-wrap: anywhere; }
.sf-desk-query { margin: 0; font-size: 1.05rem; line-height: 1.5; color: var(--text-primary, #e6edf7); overflow-wrap: anywhere; }
.sf-desk-state { display: inline-flex; align-items: center; gap: 8px; margin: 12px 0 0; padding: 5px 12px 5px 10px; border: 1px solid var(--border, #1f2d47); border-radius: 999px; background: var(--bg-secondary, #101828); font-size: .8rem; font-weight: 800; letter-spacing: .03em; text-transform: capitalize; }
.sf-desk-state { display: inline-flex; align-items: center; gap: 8px; margin: 12px 0 0; padding: 5px 12px 5px 10px; border: 1px solid var(--border, #1f2d47); border-radius: 999px; background: var(--bg-secondary, #101828); font-size: .8rem; font-weight: 800; letter-spacing: .03em; text-transform: capitalize; }
@@ -420,6 +420,9 @@
@@ -420,6 +420,9 @@
if (giveForm) giveForm.hidden = !driving;
if (giveForm) giveForm.hidden = !driving;
var links = root.querySelector('[data-sf-desk-links]');
var links = root.querySelector('[data-sf-desk-links]');
links.hidden = !desk;
links.hidden = !desk;
+ // Tapping the screen opens the full view: the desk, when the task has one (plan Z, 11-07).
+ var screen = root.querySelector('[data-sf-desk-screen]');
+ if (screen) { if (desk) screen.href = desk; else screen.removeAttribute('href'); }
if (desk) {
if (desk) {
root.querySelector('[data-sf-desk-watch]').href = desk;
root.querySelector('[data-sf-desk-watch]').href = desk;
root.querySelector('[data-sf-desk-takeover]').href = desk;
root.querySelector('[data-sf-desk-takeover]').href = desk;
@@ -331,7 +331,7 @@ function webTask({ task, actorUrl } = {}) {
@@ -331,7 +331,7 @@ function webTask({ task, actorUrl } = {}) {
<section class="sf-task-card" aria-label="What your agent is doing">
<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-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>${esc(where)}</span></p>
<p class="sf-task-where">${kit.icon('globe')}<span data-sf-desk-note>${esc(where)}</span></p>
-<img class="ov-task-still" data-sf-desk-still data-src="/api/v1/web/tasks/${esc(id)}/still" alt="Your agent's screen now" width="320" height="180" hidden>
+<figure class="sf-task-screen"><a class="sf-task-screen-open" data-sf-desk-screen${desk ? ` href="${esc(desk)}"` : ''} target="_blank" rel="noopener noreferrer"><img class="ov-task-still" data-sf-desk-still data-src="/api/v1/web/tasks/${esc(id)}/still" alt="Your agent's screen now" width="320" height="180" hidden></a><figcaption><span class="sf-task-live">Live</span><span>Your agent’s screen</span></figcaption></figure>
<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-needs" data-sf-desk-needs${task.needs_you ? '' : ' hidden'}>${esc(task.needs_you && task.needs_you.what || '')}</p>
<form class="sf-task-takeover" method="post" action="/api/v1/web/tasks/${esc(id)}/takeover" data-sf-desk-takeover-form${desk && open && !driving ? '' : ' hidden'}><button class="ovk-btn ovk-btn--primary sf-desk-takeover" type="submit" data-sf-desk-takeover>Take over</button></form>
<form class="sf-task-takeover" method="post" action="/api/v1/web/tasks/${esc(id)}/takeover" data-sf-desk-takeover-form${desk && open && !driving ? '' : ' hidden'}><button class="ovk-btn ovk-btn--primary sf-desk-takeover" type="submit" data-sf-desk-takeover>Take over</button></form>
<form class="sf-task-giveback" method="post" action="/api/v1/web/tasks/${esc(id)}/giveback" data-sf-desk-giveback-form${driving ? '' : ' hidden'}><button class="ovk-btn sf-desk-takeover" type="submit" data-sf-desk-giveback>Give it back</button></form>
<form class="sf-task-giveback" method="post" action="/api/v1/web/tasks/${esc(id)}/giveback" data-sf-desk-giveback-form${driving ? '' : ' hidden'}><button class="ovk-btn sf-desk-takeover" type="submit" data-sf-desk-giveback>Give it back</button></form>
@@ -25,6 +25,8 @@ const COMPUTER = { session_id: 'ses_01JAB2C3D4E5F6G7H8J9K0MNPQR', machine: 'a sh
@@ -25,6 +25,8 @@ const COMPUTER = { session_id: 'ses_01JAB2C3D4E5F6G7H8J9K0MNPQR', machine: 'a sh
const r = await t.get(`/web/t/${id}`, { cookie: signedIn });
const r = await t.get(`/web/t/${id}`, { cookie: signedIn });
assert.strictEqual(r.status, 200, r.text);
assert.strictEqual(r.status, 200, r.text);
assert.match(r.text, /class="ov-task-still"/);
assert.match(r.text, /class="ov-task-still"/);
+ // The screen in a frame with a Live badge and the machine under it (Opus, 11-07).
+ assert.match(r.text, /<figure class="sf-task-screen"><a class="sf-task-screen-open" data-sf-desk-screen[^>]*><img class="ov-task-still"[^>]*hidden><\/a><figcaption><span class="sf-task-live">Live<\/span><span>Your agent’s screen<\/span><\/figcaption><\/figure>/);
assert.match(r.text, new RegExp(`data-src="/api/v1/web/tasks/${id}/still"`));
assert.match(r.text, new RegExp(`data-src="/api/v1/web/tasks/${id}/still"`));
assert.match(r.text, /a shared desk host/);
assert.match(r.text, /a shared desk host/);
assert.match(r.text, /metered by the minute/);
assert.match(r.text, /metered by the minute/);