Skip to content

Commit 0528c2a

Browse files
sh1pt-actions-fleet[bot]ralyodioclaude
authored
Desktop demo video plays in @profullstack/player (#281)
The /desktop hero demo now upgrades to the house player (one control bar with auto-hide, fullscreen including iPhone). The player is bundled server-side on first request into /assets/pf-player.js and /assets/pf-player.css (same origin, so the CSP's script-src 'self' and style-src 'self' cover it), and pf-player-init.js loads it only on a page with a [data-pf-player] host. The <video controls> stays as the fallback. Co-authored-by: Anthony Ettinger <anthony@profullstack.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent e785e9d commit 0528c2a

9 files changed

Lines changed: 127 additions & 2 deletions

File tree

‎bun.lock‎

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"@profullstack/fleetbar": "0.2.0",
3434
"@profullstack/footer": "latest",
3535
"@profullstack/hqtui": "0.10.3",
36+
"@profullstack/player": "0.17.0",
3637
"@profullstack/stack": "0.4.0",
3738
"@profullstack/threatcrush": "0.13.30",
3839
"@simplewebauthn/server": "13.3.3",

‎public/pf-player-init.js‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Upgrades every server-rendered <div data-pf-player><video controls></video></div> to
2+
// @profullstack/player (one control bar, auto-hide, fullscreen including iPhone). The <video> keeps
3+
// its native controls until the player has loaded, and keeps them for good if it never does.
4+
const hosts = document.querySelectorAll('[data-pf-player]');
5+
if (hosts.length) {
6+
if (!document.querySelector('link[data-pf-player-css]')) {
7+
const link = document.createElement('link');
8+
link.rel = 'stylesheet';
9+
link.href = '/assets/pf-player.css';
10+
link.dataset.pfPlayerCss = '';
11+
document.head.append(link);
12+
}
13+
import('/assets/pf-player.js')
14+
.then(({ createPlayer }) => {
15+
for (const host of hosts) {
16+
const media = host.querySelector('video, audio');
17+
const src = host.dataset.src || media?.getAttribute('src');
18+
if (!media || !src) continue;
19+
try {
20+
createPlayer(host, {
21+
src,
22+
media,
23+
kind: host.dataset.kind || undefined,
24+
mediaId: host.dataset.mediaId || undefined,
25+
poster: media.getAttribute('poster') || undefined,
26+
});
27+
} catch (error) {
28+
console.error('player failed, keeping native controls', error);
29+
}
30+
}
31+
})
32+
.catch((error) => console.error('player unavailable, keeping native controls', error));
33+
}

‎public/style.css‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1845,6 +1845,17 @@ textarea {
18451845
border-radius: 14px;
18461846
background: #000;
18471847
}
1848+
.desktop-hero-video .pux-player {
1849+
aspect-ratio: 16 / 9;
1850+
border: 1px solid var(--line);
1851+
border-radius: 14px;
1852+
overflow: hidden;
1853+
background: #000;
1854+
}
1855+
.desktop-hero-video .pux-player video {
1856+
border: 0;
1857+
border-radius: 0;
1858+
}
18481859
.desktop-hero-video figcaption {
18491860
margin-top: 8px;
18501861
font-size: 13px;

‎src/player-bundle.mjs‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* @profullstack/player for the browser, served from w3bs.org's own origin (script-src 'self').
3+
*
4+
* w3bs.org has no client build step: public/ is plain JS. So the player is bundled here, once, the
5+
* first time it is asked for, into one ES module (/assets/pf-player.js) plus its stylesheet
6+
* (/assets/pf-player.css). public/pf-player-init.js imports it only on a page with a
7+
* [data-pf-player] host. Same pattern as pit.world's src/player-bundle.ts.
8+
*/
9+
import { readFileSync } from 'node:fs';
10+
import { createRequire } from 'node:module';
11+
12+
let js = null;
13+
let css = null;
14+
15+
export function playerJs() {
16+
js ??= (async () => {
17+
const out = await Bun.build({
18+
entrypoints: [new URL('./player-entry.mjs', import.meta.url).pathname],
19+
target: 'browser',
20+
format: 'esm',
21+
splitting: false,
22+
minify: true,
23+
});
24+
if (!out.success)
25+
throw new Error(`player bundle failed: ${out.logs.map((l) => l.message).join('; ')}`);
26+
return await out.outputs[0].text();
27+
})().catch((error) => {
28+
js = null;
29+
throw error;
30+
});
31+
return js;
32+
}
33+
34+
export function playerCss() {
35+
css ??= readFileSync(
36+
createRequire(import.meta.url).resolve('@profullstack/player/player.css'),
37+
'utf8',
38+
);
39+
return css;
40+
}

‎src/player-entry.mjs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// The browser side of @profullstack/player, bundled by the server (src/player-bundle.mjs) and loaded
2+
// by public/pf-player-init.js only on a page that has something to play.
3+
export { createPlayer } from '@profullstack/player';

‎src/server.mjs‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { mountInstallLinks } from './install-links.mjs';
2727
import { mountRegistryFront } from './registry-front.mjs';
2828
import { createBrowser } from './browse.mjs';
2929
import { createNet } from './net-routes.mjs';
30+
import { playerCss, playerJs } from './player-bundle.mjs';
3031

3132
export function createApp({
3233
store,
@@ -374,6 +375,19 @@ export function createApp({
374375
origin,
375376
publishTask: (userId, payload) => publishedTasks.publish(userId, payload),
376377
});
378+
// @profullstack/player, bundled once on first use (src/player-bundle.mjs); pf-player-init.js loads
379+
// it only on a page with something to play. Same origin, so script-src 'self' covers it.
380+
app.get('/assets/pf-player.js', async (_req, res) => {
381+
try {
382+
res.set('Cache-Control', 'public, max-age=3600').type('text/javascript').send(await playerJs());
383+
} catch (error) {
384+
console.error(error instanceof Error ? error.message : String(error));
385+
res.status(503).type('text/plain').send('player unavailable');
386+
}
387+
});
388+
app.get('/assets/pf-player.css', (_req, res) =>
389+
res.set('Cache-Control', 'public, max-age=3600').type('text/css').send(playerCss()),
390+
);
377391
app.use(
378392
'/assets',
379393
express.static(resolve(ROOT, 'public'), { maxAge: '1h', index: false, fallthrough: false }),

‎src/site.mjs‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -584,7 +584,7 @@ sudo rm -f /usr/share/wayland-sessions/w3bs-de.desktop /usr/local/bin/w3bs-de
584584
function desktopPage() {
585585
const prd = readFileSync(resolve(ROOT, 'docs', 'agentic-desktop-layer-prd.md'), 'utf8');
586586
const shots = `<div class="control-shots"><figure class="control-shot control-shot-main"><img src="/assets/desktop-control-app.webp" width="1280" height="1110" loading="lazy" alt="The control app with agent access Enabled: the active agent and its task, Disable agent access, Stop all agent activity and Quit and disable buttons, the emergency stop shortcut, a pending approval to type into the Kate editor, and three agents with their permission presets."><figcaption>The control app, opened from the tray. Status, the big switches, approvals and every agent's access on one page.</figcaption></figure><figure class="control-shot"><img src="/assets/desktop-control-app-access.webp" width="1280" height="900" loading="lazy" alt="Changing an agent's access in the control app: Observe, Restricted, Broad desktop or Nothing, and the list of apps a restricted agent may use."><figcaption>Change what an agent may do: observe only, restricted to named apps, the whole desktop, or nothing.</figcaption></figure><figure class="control-shot control-shot-phone"><img src="/assets/desktop-control-app-phone.webp" width="780" height="1420" loading="lazy" alt="The control app on a narrow screen: Enabled status and full-width Disable, Stop and Quit buttons."><figcaption>It stays usable in a narrow window.</figcaption></figure><p class="control-shots-note">Real screenshots of the control app running against the layer's built-in demo desktop, so the agents and tasks are sample data. The app only answers on 127.0.0.1 and signs in with a one-time link from the tray or <code>w3bs desktop control --open</code>.</p></div>`;
587-
return `<section class="page-head desktop-head"><div class="eyebrow">AGENTIC DESKTOP LAYER · W3BS</div><h1>Your desktop.<br>Your agents.<br><em>Your switch.</em></h1><p>An open, local-first layer that lets the agents you authorize use your real desktop: move the mouse, click, drag, type, use shortcuts and read what is on screen. You keep your desktop. A control in your taskbar, panel or menu bar turns it on and off.</p><figure class="desktop-hero-video"><video controls playsinline preload="metadata" poster="/assets/desktop-demo-poster.webp" width="1920" height="1080" aria-describedby="desktop-video-caption"><source src="/assets/desktop-demo.mp4" type="video/mp4"><a class="inline-link" href="/assets/desktop-demo.mp4">Download the demo video (MP4)</a></video><figcaption id="desktop-video-caption" class="muted">Bring agents to your desktop: a 48-second demo.</figcaption></figure><div class="desktop-install-box"><span class="eyebrow">MACOS AND LINUX</span><div class="identity-line"><code>${e(INSTALL)}</code><button class="small-button" type="button" data-copy="${e(INSTALL)}">Copy</button></div><div class="identity-line"><code>${e(DESKTOP_W3BS)}</code><button class="small-button" type="button" data-copy="${e(DESKTOP_W3BS)}">Copy</button></div><span class="eyebrow">WINDOWS (POWERSHELL)</span><div class="identity-line"><code>${e(DESKTOP_INSTALL_WIN)}</code><button class="small-button" type="button" data-copy="${e(DESKTOP_INSTALL_WIN)}">Copy</button></div><p class="muted">The first line installs the <a class="inline-link" href="/packages">w3bs CLI</a> (skip it if you have it), the second the desktop layer; <code>w3bs update</code> keeps both current. Without the w3bs CLI: <code>${e(DESKTOP_INSTALL)}</code></p><div class="actions"><a class="button primary" href="#steps">How to use it <span aria-hidden="true">↓</span></a><a class="text-link" href="#install">What the installer does <span aria-hidden="true">→</span></a></div></div></section><div class="draft-note desktop-status" id="status"><strong>Release status.</strong> M0/M1 done. On KDE Plasma 6.7 and GNOME Shell 50 test rigs, agents type, point, click, take screenshots and target named windows, and the tray, GNOME panel control and emergency stop switch access off. Since 0.9 agents work in web pages through DevTools instead of screenshots: they read the page as an outline of elements and click and type by element, in their own TronBrowser profile (<code>w3bs desktop browser launch</code>), with the same lease, prompts and stop switch; if DevTools cannot reach the browser they stop and say so rather than falling back to screenshots. On Windows and macOS the same workflow passes end to end on real GitHub-hosted desktops (Windows 56 of 56 checks, macOS 23 of 23). On GNOME, text with characters the keyboard layout lacks is refused rather than typed partially. Install it through the w3bs CLI, or with PowerShell on Windows (above). No platform is certified yet.</div>
587+
return `<section class="page-head desktop-head"><div class="eyebrow">AGENTIC DESKTOP LAYER · W3BS</div><h1>Your desktop.<br>Your agents.<br><em>Your switch.</em></h1><p>An open, local-first layer that lets the agents you authorize use your real desktop: move the mouse, click, drag, type, use shortcuts and read what is on screen. You keep your desktop. A control in your taskbar, panel or menu bar turns it on and off.</p><figure class="desktop-hero-video"><div class="pf-player-host" data-pf-player data-src="/assets/desktop-demo.mp4" data-kind="mp4" data-media-id="w3bs-desktop-demo"><video controls playsinline preload="metadata" poster="/assets/desktop-demo-poster.webp" width="1920" height="1080" aria-describedby="desktop-video-caption"><source src="/assets/desktop-demo.mp4" type="video/mp4"><a class="inline-link" href="/assets/desktop-demo.mp4">Download the demo video (MP4)</a></video></div><figcaption id="desktop-video-caption" class="muted">Bring agents to your desktop: a 48-second demo.</figcaption></figure><div class="desktop-install-box"><span class="eyebrow">MACOS AND LINUX</span><div class="identity-line"><code>${e(INSTALL)}</code><button class="small-button" type="button" data-copy="${e(INSTALL)}">Copy</button></div><div class="identity-line"><code>${e(DESKTOP_W3BS)}</code><button class="small-button" type="button" data-copy="${e(DESKTOP_W3BS)}">Copy</button></div><span class="eyebrow">WINDOWS (POWERSHELL)</span><div class="identity-line"><code>${e(DESKTOP_INSTALL_WIN)}</code><button class="small-button" type="button" data-copy="${e(DESKTOP_INSTALL_WIN)}">Copy</button></div><p class="muted">The first line installs the <a class="inline-link" href="/packages">w3bs CLI</a> (skip it if you have it), the second the desktop layer; <code>w3bs update</code> keeps both current. Without the w3bs CLI: <code>${e(DESKTOP_INSTALL)}</code></p><div class="actions"><a class="button primary" href="#steps">How to use it <span aria-hidden="true">↓</span></a><a class="text-link" href="#install">What the installer does <span aria-hidden="true">→</span></a></div></div></section><div class="draft-note desktop-status" id="status"><strong>Release status.</strong> M0/M1 done. On KDE Plasma 6.7 and GNOME Shell 50 test rigs, agents type, point, click, take screenshots and target named windows, and the tray, GNOME panel control and emergency stop switch access off. Since 0.9 agents work in web pages through DevTools instead of screenshots: they read the page as an outline of elements and click and type by element, in their own TronBrowser profile (<code>w3bs desktop browser launch</code>), with the same lease, prompts and stop switch; if DevTools cannot reach the browser they stop and say so rather than falling back to screenshots. On Windows and macOS the same workflow passes end to end on real GitHub-hosted desktops (Windows 56 of 56 checks, macOS 23 of 23). On GNOME, text with characters the keyboard layout lacks is refused rather than typed partially. Install it through the w3bs CLI, or with PowerShell on Windows (above). No platform is certified yet.</div>
588588
<section class="section resource-feature" id="install" aria-labelledby="install-title"><div><div class="eyebrow">INSTALL AND USE</div><h2 id="install-title">Install with w3bs.<br>One switch to control.</h2><p>Linux (KDE Plasma, GNOME; x86_64 and arm64), macOS 11 or later (Apple silicon and Intel) and Windows 10 or 11 (x64). Per-user install, no administrator rights. Builds are not code-signed: Windows SmartScreen may warn the first time, and macOS may ask for Accessibility and Screen Recording again after an upgrade.</p><p class="muted">The w3bs CLI is the same one that installs every <a class="inline-link" href="/packages">W3BS package</a>; it runs the desktop layer's own installer and remembers it for <code>w3bs update</code>. Read the installers first if you like: <a class="inline-link" href="/install.sh">install.sh</a> (w3bs CLI), <a class="inline-link" href="/desktop/install.sh">desktop/install.sh</a>, <a class="inline-link" href="/desktop/install.ps1">install.ps1</a>.</p></div><div class="terminal"><div class="terminal-tabs"><span class="terminal-title">install the agentic desktop layer</span></div><pre><code># macOS and Linux
589589
${e(INSTALL)}
590590
${e(DESKTOP_W3BS)}
@@ -843,7 +843,8 @@ export async function renderPage({ path, host, query, store, origin, browser })
843843
} else if (path === '/desktop') {
844844
title = 'Agentic Desktop Layer';
845845
description = desktopDescription;
846-
scripts = '<script type="module" src="/assets/desktop.js"></script>';
846+
scripts =
847+
'<script type="module" src="/assets/desktop.js"></script><script type="module" src="/assets/pf-player-init.js"></script>';
847848
content = desktopPage();
848849
} else if (/^\/specs\/W3BS-[A-Z]+-1$/.test(path)) {
849850
title = path.split('/').at(-1);

‎test/integration.test.mjs‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,8 @@ test('Agentic Desktop Layer page, typo redirect, raw PRD and navigation', async
237237
'Enable agent access',
238238
'Disable agent access',
239239
'/assets/desktop.js',
240+
'/assets/pf-player-init.js',
241+
'data-pf-player',
240242
])
241243
assert.ok(html.includes(text), `missing ${text}`);
242244
assert.match(html, /<a href="\/desktop" aria-current="page">Desktop<\/a>/);
@@ -287,6 +289,15 @@ test('Agentic Desktop Layer page, typo redirect, raw PRD and navigation', async
287289
assert.match(front, /<meta name="twitter:card" content="summary">/);
288290
const script = await (await fetch(`${origin}/assets/desktop.js`)).text();
289291
assert.ok(!/localhost|127\.0\.0\.1|requestPermission|getUserMedia/.test(script));
292+
// The demo video plays in @profullstack/player, bundled and served from this origin.
293+
const player = await fetch(`${origin}/assets/pf-player.js`);
294+
assert.equal(player.status, 200);
295+
assert.match(player.headers.get('content-type'), /javascript/);
296+
const playerJs = await player.text();
297+
assert.ok(playerJs.includes('pux-player') && /export\s*\{[^}]*createPlayer/.test(playerJs));
298+
const playerCss = await fetch(`${origin}/assets/pf-player.css`);
299+
assert.equal(playerCss.status, 200);
300+
assert.ok((await playerCss.text()).includes('.pux-player'));
290301
} finally {
291302
await instance.close();
292303
}

0 commit comments

Comments
 (0)