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
2 changes: 1 addition & 1 deletion .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
- run: sudo apt-get update -q && sudo apt-get install -y -q ffmpeg
- run: node site/build.mjs
- name: Verify site body
run: echo '6b131383ded1f047ad3252d9e77a1666412183535c0002c815942bfff5a9628e site/dist/index.html' | sha256sum --check
run: echo 'a49612cca7e7ff8a44ad86fb3e616567df992baad56859610b4c238522dd2d17 site/dist/index.html' | sha256sum --check
- uses: actions/configure-pages@v5
- uses: actions/upload-pages-artifact@v3
with:
Expand Down
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ The first line is for Claude Code. The second is for Codex, Cursor, Gemini CLI,

[deskstudio.ai](https://deskstudio.ai) · [Install](docs/install.md) · [Docs](#docs) · [Contribute](#contribute) · [Discussions](https://github.com/adityareddy-dev/deskstudio/discussions)

<a href="site/media/explainer.mp4"><img src="site/media/explainer.gif" alt="One brief through roughs, pick, polish, pixel proof and scored sign-off" width="760"></a>

[Watch the 50-second explainer](site/media/explainer.mp4)

## 30-second example

Ask for something in your agent:
Expand All @@ -37,18 +41,20 @@ npx --package deskstudio desk-run design "a launch card for LinkedIn"

Files land in `.desk/out/<job>/` with a short note on who did what, the scores and the cost.

<img src="site/media/one-command.gif" alt="One command typed in a terminal, then every piece the studio has signed off" width="760">
## What a run looks like

<a href="site/media/real-run.mp4"><img src="site/media/real-run.gif" alt="Eight seconds captured from a fresh Desk Studio CLI job" width="760"></a>

Type one line in your agent. The wall is the 17 pieces the studio has signed off so far. The list, with scores and cost, is in [What it makes](docs/gallery.md).
[Watch the real CLI capture](site/media/real-run.mp4)

The outputs, scores and cost are in [What it makes](docs/gallery.md).

## Why it exists

Cheap models make, strong models judge. Every visual job runs the way a studio runs: roughs, a pick, polish, proof, sign-off. The desks call models through the CLI you are already signed in to (`claude`, `codex` or `agy`) or a key (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`). `npx --package deskstudio desk-run doctor` shows which path it will take without making a model call. Scores come from the studio's model reviewers, they are not independent benchmarks.

## How it works

<img src="site/media/explainer.gif" alt="A team of desk robots passes one job along: roughs, the pick, polish, proof and the sign-off" width="760">

Three roughs on Sonnet, the pick on Opus, polish on Haiku, proof with no model, then the sign-off. The launch card job, retold with its real numbers. The five steps, the retry rules and the pixel checks are in [How a job runs](docs/how-it-works.md).

- 42 desks: the Design Manager and 41 that run as subagents. See [the team, cost and models](docs/team-and-cost.md) and [docs/roles.md](docs/roles.md).
Expand Down
36 changes: 28 additions & 8 deletions site/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,14 @@ const loop = (name, alt) => {
return frame(({ 'floor-idle': 'dashboard', 'swap-the-seat': 'change-role' })[name] || name, `<video class="loop" width="1280" height="720" autoplay muted loop playsinline preload="metadata" ${existsSync(jpg) ? `poster="media/${name}.webp"` : ''} aria-label="${esc(alt)}"><source src="media/${name}.mp4" type="video/mp4"></video>${existsSync(jpg) ? `<img class="poster" src="media/${name}.webp" width="1280" height="720" alt="${esc(alt)}" loading="lazy" decoding="async">` : ''}`);
};

// The corrected wide take and its poster share the same uncropped aspect ratio.
const heroLoop = (name, alt) => {
for (const ext of ['mp4', 'webp']) cpSync(join(media, `${name}.${ext}`), join(dist, 'media', `${name}.${ext}`));
return `<video controls muted playsinline preload="metadata" width="1920" height="1080" poster="media/${name}.webp" aria-label="${esc(alt)}"><source src="media/${name}.mp4" type="video/mp4"></video>`;
for (const file of [`${name}.mp4`, `${name}.webm`, `${name}-poster.png`, `${name}.vtt`]) cpSync(join(media, file), join(dist, 'media', file));
return `<video class="loop" autoplay muted loop playsinline preload="metadata" width="1920" height="1080" poster="media/${name}-poster.png" aria-label="${esc(alt)}"><source src="media/${name}.webm" type="video/webm"><source src="media/${name}.mp4" type="video/mp4"><track kind="captions" src="media/${name}.vtt" srclang="en" label="English"></video><img class="poster" src="media/${name}-poster.png" width="1920" height="1080" alt="${esc(alt)}"><div class="hero-video-controls"><button type="button" data-video-toggle aria-pressed="false">Pause video</button><a href="media/${name}.mp4">Open video</a></div>`;
};

const realRun = () => {
for (const ext of ['mp4', 'png']) cpSync(join(media, `real-run.${ext}`), join(dist, 'media', `real-run.${ext}`));
return `<video controls muted playsinline preload="none" width="1920" height="1080" poster="media/real-run.png" aria-label="Eight seconds captured from a fresh Desk Studio CLI job"><source src="media/real-run.mp4" type="video/mp4"></video>`;
};

// ---------- the page ----------
Expand Down Expand Up @@ -568,6 +572,8 @@ code { overflow-wrap:anywhere; }
img, video { max-width:100%; }
.media-frame img, .media-frame video { display:block; width:100%; height:auto; object-fit:contain; }
.media-frame video { background:var(--dark); }
.hero-video-controls { display:flex; align-items:center; gap:24px; padding:16px; }
.hero-video-controls button { font:inherit; color:inherit; border:1px solid currentColor; border-radius:4px; background:transparent; padding:8px 16px; cursor:pointer; }
.media-frame .poster { display:none; }
.gallery { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:48px 24px; margin-top:48px; }
.gallery figure { min-width:0; }
Expand Down Expand Up @@ -632,6 +638,7 @@ pre { white-space:pre-wrap; overflow-wrap:anywhere; font-size:16px; }
@media (prefers-reduced-motion:reduce) {
video.loop { display:none; }
.media-frame .poster { display:block; }
.hero-video-controls button { display:none; }
}

`;
Expand All @@ -655,7 +662,7 @@ const html = `<!doctype html>
<meta name="description" content="Your own design studio. A full design studio inside the coding agent you already use. One command, free, nothing leaves your machine but the model calls you already make.">
<link rel="icon" href="favicon.svg" type="image/svg+xml"><link rel="icon" href="favicon.ico" sizes="48x48"><link rel="apple-touch-icon" href="apple-touch-icon.png">
<link rel="canonical" href="https://deskstudio.ai/">
<link rel="preload" as="image" href="media/one-command-v2.webp" fetchpriority="high">
<link rel="preload" as="image" href="media/explainer-poster.png" fetchpriority="high">
<meta property="og:title" content="Your own design studio.">
<meta property="og:description" content="A full design studio inside the coding agent you already use. One command, free, nothing leaves your machine.">
<meta property="og:image" content="https://deskstudio.ai/og.png"><meta property="og:image:width" content="1200"><meta property="og:image:height" content="630">
Expand Down Expand Up @@ -692,13 +699,18 @@ const html = `<!doctype html>
<code class="command">/plugin install desk --marketplace adityareddy-dev/deskstudio</code>
<dl class="facts"><div><dt>Free, MIT</dt><dd>no account, no sign-up</dd></div><div><dt>${registry.desks.length} desks</dt><dd>each one a brief you can read</dd></div><div><dt>${goldens.length} pieces</dt><dd>signed off, with score and cost</dd></div><div><dt><a href="#contribute">${starters.length} jobs open</a></dt><dd>pick one</dd></div></dl>
</div>
<figure class="media-frame hero-media" aria-label="Desk Studio one-command design process">
<figure class="media-frame hero-media" aria-label="Desk Studio capabilities">
<div class="frame-bar" aria-hidden="true">Desk Studio</div>
${heroLoop('one-command-v2', 'One command, the real Desk Studio outputs')}
${heroLoop('explainer', 'Desk Studio: roughs, pick, polish, proof and sign-off')}
</figure>
</div>
</section>

<section class="band" aria-labelledby="real-run-heading"><div class="wrap">
<h2 id="real-run-heading">What a run looks like</h2>
<div class="media-frame" style="margin-top:32px">${realRun()}</div>
</div></section>

<section class="band dark" id="how" aria-labelledby="how-heading">
<div class="wrap">
<div class="process-head">
Expand Down Expand Up @@ -747,7 +759,6 @@ const html = `<!doctype html>
</ol>
<details class="record"><summary>The launch-card job, with its real numbers</summary><div class="record-body">
<p class="lead">This is the launch card job, retold with its real numbers.</p>
<div style="margin-top:32px">${loop('explainer', 'A team of little desk robots passes one job along: three roughs, the pick, polish rounds, proof and the sign-off, then the work comes back')}</div>

<div id="loop"></div><h2>Roughs, pick, polish, proof, sign-off.</h2>
<p class="lead">Every visual job runs the way a studio runs. This is the launch card above, as it happened.</p>
Expand Down Expand Up @@ -988,7 +999,16 @@ const html = `<!doctype html>
addEventListener('hashchange', revealAnchor);
if (location.hash) revealAnchor();
const motion = matchMedia('(prefers-reduced-motion: reduce)');
const pause = () => { if (motion.matches) for (const v of document.querySelectorAll('video')) v.pause(); };
const toggle = document.querySelector('[data-video-toggle]');
const heroVideo = document.querySelector('.hero-media video');
const syncToggle = () => {
toggle.textContent = heroVideo.paused ? 'Play video' : 'Pause video';
toggle.setAttribute('aria-pressed', String(heroVideo.paused));
};
toggle.addEventListener('click', () => { if (heroVideo.paused) heroVideo.play().catch(syncToggle); else heroVideo.pause(); });
heroVideo.addEventListener('play', syncToggle);
heroVideo.addEventListener('pause', syncToggle);
const pause = () => { if (motion.matches) for (const v of document.querySelectorAll('video')) v.pause(); syncToggle(); };
pause();
motion.addEventListener('change', pause);
})();
Expand Down
4 changes: 2 additions & 2 deletions site/media.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ mkdirSync(out, { recursive: true });

const ff = a => spawnSync('ffmpeg', ['-y', '-loglevel', 'error', ...a], { encoding: 'utf8' });
const rows = [];
for (const f of readdirSync(takes).filter(f => /-1920x1080\.mp4$/.test(f)).sort()) {
for (const f of readdirSync(takes).filter(f => /-1920x1080\.mp4$/.test(f) && !/^(explainer|real-run)-/.test(f)).sort()) {
const name = f.replace(/-1920x1080\.mp4$/, '');
const mp4 = join(out, `${name}.mp4`), jpg = join(out, `${name}.webp`);
const r1 = ff(['-i', join(takes, f), '-vf', 'scale=1280:720', '-c:v', 'libx264', '-crf', '27', '-preset', 'slow', '-pix_fmt', 'yuv420p', '-movflags', '+faststart', '-an', mp4]);
Expand All @@ -35,7 +35,7 @@ for (const f of readdirSync(takes).filter(f => /-1080x1920\.mp4$/.test(f)).sort(
rows.push({ name, mp4: r1.status === 0 ? statSync(mp4).size : null, jpg: r2.status === 0 ? statSync(jpg).size : null });
}
// GIFs for the README, where video does not play: 640 wide, 12 frames a second, a palette per clip.
const GIFS = ['one-command', 'explainer', 'one-line', 'the-pick', 'floor-idle', 'before-after', 'cost-meter', 'pick-it-yourself', 'swap-the-seat', 'note-to-the-desk', 'proof'];
const GIFS = ['one-command', 'one-line', 'the-pick', 'floor-idle', 'before-after', 'cost-meter', 'pick-it-yourself', 'swap-the-seat', 'note-to-the-desk', 'proof'];
for (const name of GIFS) {
const src = join(takes, `${name}-1920x1080.mp4`);
if (!existsSync(src)) continue;
Expand Down
Binary file added site/media/explainer-poster.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/media/explainer-short.mp4
Binary file not shown.
Binary file modified site/media/explainer.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified site/media/explainer.mp4
Binary file not shown.
19 changes: 19 additions & 0 deletions site/media/explainer.vtt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
WEBVTT

00:00:00.000 --> 00:00:06.000
One brief. The studio takes it from here.

00:00:06.000 --> 00:00:16.000
Roughs, pick, polish, proof, sign-off. Five roles, one job.

00:00:16.000 --> 00:00:26.000
Proof measures. 32 px inside a 38 px safe zone fails, and the job goes back.

00:00:26.000 --> 00:00:36.000
Eight axes, every one at 8 or more, average 8.5 or more. Rounds until it passes.

00:00:36.000 --> 00:00:44.000
Launch card, three rounds, $1.34.

00:00:44.000 --> 00:00:50.000
Runs inside the coding agent you already use. deskstudio.ai
Binary file added site/media/explainer.webm
Binary file not shown.
Binary file added site/media/real-run.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/media/real-run.mp4
Binary file not shown.
Binary file added site/media/real-run.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions site/motion/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Capability explainer

From this directory:

```sh
npm ci
npm run typecheck
npm run render:all
```

Set `CHROME_PATH` to an existing Chrome executable, or let Remotion download its renderer. The six-beat composition is 1500 frames, 1920 × 1080, 30 fps. `ExplainerShort` compresses beats 2–4 into 300 frames. The CLI produces H.264, VP9, a PNG poster and a 760 px GIF fallback. Full-length captions are in `captions.json`. The short cut uses `captions-short.json`. Both are burned into their videos. Beat 3 includes a 2× top-left inset with rulers for the 32 px placement and 38 px safe zone. No audio track.

`src/records.ts` copies scores and cost from `evals/golden/social-card/desk-studio-launch/state.json`. The rejected proof example is a different job, `evals/golden/social-card/desk-studio-floor/state.json`: its notes record `deskstudio.ai` at 32 px inside the 38 px LinkedIn safe zone. The eight rubric labels come from the launch record, including Copy. Round two is held even at 8.5 because Originality is 7. Costs are recorded list-price estimates.

The three output examples use the launch render, the Northlight dashboard golden and `brand/logo/cover-linkedin.png`. They are separate examples, not three files from the launch job. Palette and motion follow `brand/`; bundled Inter and Geist Mono are copied from `fonts/`. Host symbols come from [Lobe Icons](https://github.com/lobehub/lobe-icons), with its MIT license in `public/assets/`. Host names refer to the October 20 terminal-host runs with npm Desk Studio 1.0.0.

`real-run.mp4` is a contiguous eight-second screen capture from a separate fresh CLI job. Its capture details are in `real-run.json`. It is independent of the historical launch-card figures. The website generator copies committed media, so the Pages build never runs Remotion.
14 changes: 14 additions & 0 deletions site/motion/captions-short.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[
{
"beat": 2,
"text": "Five roles, one job."
},
{
"beat": 3,
"text": "32 px in a 38 px zone. Back it goes."
},
{
"beat": 4,
"text": "Eight axes. Rounds until it passes."
}
]
32 changes: 32 additions & 0 deletions site/motion/captions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
[
{
"start": 0,
"end": 6,
"text": "One brief. The studio takes it from here."
},
{
"start": 6,
"end": 16,
"text": "Roughs, pick, polish, proof, sign-off. Five roles, one job."
},
{
"start": 16,
"end": 26,
"text": "Proof measures. 32 px inside a 38 px safe zone fails, and the job goes back."
},
{
"start": 26,
"end": 36,
"text": "Eight axes, every one at 8 or more, average 8.5 or more. Rounds until it passes."
},
{
"start": 36,
"end": 44,
"text": "Launch card, three rounds, $1.34."
},
{
"start": 44,
"end": 50,
"text": "Runs inside the coding agent you already use. deskstudio.ai"
}
]
40 changes: 40 additions & 0 deletions site/motion/check-sources.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { launch, proof } from "./src/records.ts";
import { SIZES } from "../../proof/sizes.mjs";

const json = (path) =>
JSON.parse(readFileSync(new URL(path, import.meta.url), "utf8"));
const source = json(
"../../evals/golden/social-card/desk-studio-launch/state.json",
);
assert.equal(launch.id, source.id);
assert.deepEqual(launch.scores, source.scores);
assert.deepEqual(launch.cost, source.cost);
const rejected = json(
"../../evals/golden/social-card/desk-studio-floor/state.json",
);
assert.equal(proof.id, rejected.id);
assert.ok(
rejected.notes.some(
(n) =>
n.includes(`"deskstudio.ai" ${proof.actualTop}px`) &&
n.includes(`${proof.safeTop}px`),
),
);
assert.equal(
proof.safeTop,
Math.ceil(SIZES.linkedin.height * SIZES.linkedin.safe[0]),
);
const beats = json("./captions.json");
assert.equal(beats.length, 6);
assert.deepEqual(
beats.map((b) => b.start),
[0, 6, 16, 26, 36, 44],
);
assert.equal(beats.at(-1).end, 50);
for (let i = 1; i < beats.length; i++)
assert.equal(beats[i - 1].end, beats[i].start);
console.log(
"Scores, cost, rejected proof, safe zone and caption timings match their sources.",
);
Loading
Loading