Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@profullstack/fleetbar": "0.2.0",
"@profullstack/footer": "latest",
"@profullstack/hqtui": "0.10.3",
"@profullstack/player": "0.17.0",
"@profullstack/stack": "0.4.0",
"@profullstack/threatcrush": "0.13.30",
"@simplewebauthn/server": "13.3.3",
Expand Down
33 changes: 33 additions & 0 deletions public/pf-player-init.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Upgrades every server-rendered <div data-pf-player><video controls></video></div> to
// @profullstack/player (one control bar, auto-hide, fullscreen including iPhone). The <video> keeps
// its native controls until the player has loaded, and keeps them for good if it never does.
const hosts = document.querySelectorAll('[data-pf-player]');
if (hosts.length) {
if (!document.querySelector('link[data-pf-player-css]')) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = '/assets/pf-player.css';
link.dataset.pfPlayerCss = '';
document.head.append(link);
}
import('/assets/pf-player.js')
.then(({ createPlayer }) => {
for (const host of hosts) {
const media = host.querySelector('video, audio');
const src = host.dataset.src || media?.getAttribute('src');
if (!media || !src) continue;
try {
createPlayer(host, {
src,
media,
kind: host.dataset.kind || undefined,
mediaId: host.dataset.mediaId || undefined,
poster: media.getAttribute('poster') || undefined,
});
} catch (error) {
console.error('player failed, keeping native controls', error);
}
}
})
.catch((error) => console.error('player unavailable, keeping native controls', error));
}
11 changes: 11 additions & 0 deletions public/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -1845,6 +1845,17 @@ textarea {
border-radius: 14px;
background: #000;
}
.desktop-hero-video .pux-player {
aspect-ratio: 16 / 9;
border: 1px solid var(--line);
border-radius: 14px;
overflow: hidden;
background: #000;
}
.desktop-hero-video .pux-player video {
border: 0;
border-radius: 0;
}
.desktop-hero-video figcaption {
margin-top: 8px;
font-size: 13px;
Expand Down
40 changes: 40 additions & 0 deletions src/player-bundle.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* @profullstack/player for the browser, served from w3bs.org's own origin (script-src 'self').
*
* w3bs.org has no client build step: public/ is plain JS. So the player is bundled here, once, the
* first time it is asked for, into one ES module (/assets/pf-player.js) plus its stylesheet
* (/assets/pf-player.css). public/pf-player-init.js imports it only on a page with a
* [data-pf-player] host. Same pattern as pit.world's src/player-bundle.ts.
*/
import { readFileSync } from 'node:fs';
import { createRequire } from 'node:module';

let js = null;
let css = null;

export function playerJs() {
js ??= (async () => {
const out = await Bun.build({
entrypoints: [new URL('./player-entry.mjs', import.meta.url).pathname],
target: 'browser',
format: 'esm',
splitting: false,
minify: true,
});
if (!out.success)
throw new Error(`player bundle failed: ${out.logs.map((l) => l.message).join('; ')}`);
return await out.outputs[0].text();
})().catch((error) => {
js = null;
throw error;
});
return js;
}

export function playerCss() {
css ??= readFileSync(
createRequire(import.meta.url).resolve('@profullstack/player/player.css'),
'utf8',
);
return css;
}
3 changes: 3 additions & 0 deletions src/player-entry.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// The browser side of @profullstack/player, bundled by the server (src/player-bundle.mjs) and loaded
// by public/pf-player-init.js only on a page that has something to play.
export { createPlayer } from '@profullstack/player';
14 changes: 14 additions & 0 deletions src/server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { mountInstallLinks } from './install-links.mjs';
import { mountRegistryFront } from './registry-front.mjs';
import { createBrowser } from './browse.mjs';
import { createNet } from './net-routes.mjs';
import { playerCss, playerJs } from './player-bundle.mjs';

export function createApp({
store,
Expand Down Expand Up @@ -374,6 +375,19 @@ export function createApp({
origin,
publishTask: (userId, payload) => publishedTasks.publish(userId, payload),
});
// @profullstack/player, bundled once on first use (src/player-bundle.mjs); pf-player-init.js loads
// it only on a page with something to play. Same origin, so script-src 'self' covers it.
app.get('/assets/pf-player.js', async (_req, res) => {
try {
res.set('Cache-Control', 'public, max-age=3600').type('text/javascript').send(await playerJs());
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
res.status(503).type('text/plain').send('player unavailable');
}
});
app.get('/assets/pf-player.css', (_req, res) =>
res.set('Cache-Control', 'public, max-age=3600').type('text/css').send(playerCss()),
);
app.use(
'/assets',
express.static(resolve(ROOT, 'public'), { maxAge: '1h', index: false, fallthrough: false }),
Expand Down
5 changes: 3 additions & 2 deletions src/site.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,7 @@ sudo rm -f /usr/share/wayland-sessions/w3bs-de.desktop /usr/local/bin/w3bs-de
function desktopPage() {
const prd = readFileSync(resolve(ROOT, 'docs', 'agentic-desktop-layer-prd.md'), 'utf8');
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>`;
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>
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>
<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
${e(INSTALL)}
${e(DESKTOP_W3BS)}
Expand Down Expand Up @@ -843,7 +843,8 @@ export async function renderPage({ path, host, query, store, origin, browser })
} else if (path === '/desktop') {
title = 'Agentic Desktop Layer';
description = desktopDescription;
scripts = '<script type="module" src="/assets/desktop.js"></script>';
scripts =
'<script type="module" src="/assets/desktop.js"></script><script type="module" src="/assets/pf-player-init.js"></script>';
content = desktopPage();
} else if (/^\/specs\/W3BS-[A-Z]+-1$/.test(path)) {
title = path.split('/').at(-1);
Expand Down
11 changes: 11 additions & 0 deletions test/integration.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,8 @@ test('Agentic Desktop Layer page, typo redirect, raw PRD and navigation', async
'Enable agent access',
'Disable agent access',
'/assets/desktop.js',
'/assets/pf-player-init.js',
'data-pf-player',
])
assert.ok(html.includes(text), `missing ${text}`);
assert.match(html, /<a href="\/desktop" aria-current="page">Desktop<\/a>/);
Expand Down Expand Up @@ -287,6 +289,15 @@ test('Agentic Desktop Layer page, typo redirect, raw PRD and navigation', async
assert.match(front, /<meta name="twitter:card" content="summary">/);
const script = await (await fetch(`${origin}/assets/desktop.js`)).text();
assert.ok(!/localhost|127\.0\.0\.1|requestPermission|getUserMedia/.test(script));
// The demo video plays in @profullstack/player, bundled and served from this origin.
const player = await fetch(`${origin}/assets/pf-player.js`);
assert.equal(player.status, 200);
assert.match(player.headers.get('content-type'), /javascript/);
const playerJs = await player.text();
assert.ok(playerJs.includes('pux-player') && /export\s*\{[^}]*createPlayer/.test(playerJs));
const playerCss = await fetch(`${origin}/assets/pf-player.css`);
assert.equal(playerCss.status, 200);
assert.ok((await playerCss.text()).includes('.pux-player'));
} finally {
await instance.close();
}
Expand Down
Loading