openvibers/openvibe.surf Commits

Surf task card: the agent's screen in a frame with a Live badge (#101)

OP OpenVibers committed 6 days ago · f5da57c82c · 4 files, +22 −2

* Surf's task card: the agent's screen in a frame, a Live badge on it, the machine under it

The Computer still had only its size. It is now a framed 16:9 picture (420 px at most, full width on phones) with a
Live badge and "Your agent's screen on <machine>" over a shade at its foot. The frame shows only while there is a
picture. height: auto lets the 16:9 ratio win over the img's height attribute, so the whole screen shows.

* Tapping the agent's screen opens the full view (the desk), as the plan says

The frame is a link to the desk when the task has one ("Open" in its corner); surf.js keeps the link in step with
the task. The caption says "Your agent's screen" only: the line above already names the machine.

Changes

ddiff --git a/public/css/surf.css b/public/css/surf.css
iindex 0cecfdb..c57442e 100644
---- a/public/css/surf.css
++++ b/public/css/surf.css
@@@ -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; }
ddiff --git a/public/js/surf.js b/public/js/surf.js
iindex e9c5353..2168ec2 100644
---- a/public/js/surf.js
++++ b/public/js/surf.js
@@@ -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;
ddiff --git a/server/views/index.js b/server/views/index.js
iindex 6757f09..2217fc9 100644
---- a/server/views/index.js
++++ b/server/views/index.js
@@@ -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>
ddiff --git a/test/computer.test.js b/test/computer.test.js
iindex 4f8c259..5f0d13c 100644
---- a/test/computer.test.js
++++ b/test/computer.test.js
@@@ -25,6 +25,8 @@ const COMPUTER = { session_id: 'ses_01JAB2C3D4E5F6G7H8J9K0MNPQR', machine: 'a sh
const 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/);