Skip to content

Commit b19c431

Browse files
Merge remote-tracking branch 'origin/main' into posthog-code/command-center-brainrot-mode
2 parents a16c80c + 67770e3 commit b19c431

461 files changed

Lines changed: 33839 additions & 9889 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
---
2+
name: onboarding-videos
3+
description: Add, replace, and optimize the looping demo videos in the onboarding "welcome" bento grid (packages/ui/src/features/onboarding). Covers the ffmpeg compression workflow (the scripts/optimize-onboarding-videos.mjs wrapper), the faststart + right-size best practices that keep playback smooth, the first-frame poster convention, and how to wire a new clip into WelcomeScreen. Use when onboarding demo clips feel laggy, when a clip is being added/swapped/re-recorded, or when posters and videos drift out of sync.
4+
allowed-tools: Bash(node scripts/optimize-onboarding-videos.mjs:*), Bash(pnpm optimize:onboarding-videos:*), Bash(ffmpeg:*), Bash(ffprobe:*), Bash(npx biome check:*), Bash(pnpm --filter @posthog/ui typecheck:*)
5+
---
6+
7+
# Onboarding bento demo videos
8+
9+
The first onboarding step (`WelcomeScreen`) shows a bento grid of feature cards.
10+
A card with a `media` entry plays a short, muted, looping screen recording; cards
11+
without one show a static placeholder. The featured (large, top-left) card is the
12+
only one that plays at a time — hover just moves the highlight.
13+
14+
| Thing | Where |
15+
| --- | --- |
16+
| Clips + posters | `packages/ui/src/features/onboarding/assets/<feature>-<light\|dark>.{mp4,jpg}` |
17+
| Wiring (`MEDIA` map, `startTime`) | `packages/ui/src/features/onboarding/components/WelcomeScreen.tsx` |
18+
| `<video>` element + play/seek logic | `packages/ui/src/features/onboarding/components/FeatureBentoCard.tsx` |
19+
| Optimizer script | `scripts/optimize-onboarding-videos.mjs` (`pnpm optimize:onboarding-videos`) |
20+
21+
## Why clips need optimizing (the "it feels laggy" fix)
22+
23+
Raw screen recordings hitch in this UI for two reasons, both fixed by re-encoding:
24+
25+
1. **`moov` atom at the end of the file.** Without faststart the player must read
26+
to EOF before it can start, so first-play and every seek stutter. We seek on
27+
mount and on every loop (`FeatureBentoCard` parks the clip on `videoStartTime`),
28+
so this bites constantly. Fix: `-movflags +faststart`.
29+
2. **Resolution far larger than it renders.** The featured slot is never wider than
30+
~500 CSS px (the grid is `max-w-[760px]`). Even at 2× retina that's ~1000px, but
31+
recordings come in at ~1876px. Decoding huge frames and downscaling them — on
32+
every play and during the framer-motion slot reflow — is wasted work. Fix: cap
33+
width at **1000px** (which also matches the poster width exactly).
34+
35+
Bitrate/size are usually already modest; **decode cost and startup are the lag**,
36+
not bytes. Don't chase file size at the expense of width/faststart.
37+
38+
## Canonical encode target
39+
40+
H.264 · `yuv420p` · **≤1000px wide** (keep aspect, even height) · CRF 23 ·
41+
`+faststart` · no audio. These live as constants at the top of
42+
`scripts/optimize-onboarding-videos.mjs` — change them there, not ad-hoc.
43+
44+
## Optimize existing clips
45+
46+
```bash
47+
pnpm optimize:onboarding-videos # encode any clip not already optimized
48+
pnpm optimize:onboarding-videos --dry-run # show what would change
49+
pnpm optimize:onboarding-videos --force # re-encode all (after changing the target constants)
50+
```
51+
52+
Requires ffmpeg (`brew install ffmpeg`). The script tags each output with a
53+
`comment` marker, so re-runs skip already-optimized files — it's safe to run any
54+
time, including right after dropping in a new clip. It rewrites files in place;
55+
review the `git diff --stat` and the printed before/after sizes.
56+
57+
## Add or replace a clip
58+
59+
1. **Record** light + dark variants at **≥1000px wide**. The optimizer downscales to
60+
1000px but never upscales, so anything narrower ships soft — resolution is the one
61+
thing it can't fix for you. Keep it short (~10–13s); it loops.
62+
2. **Name + drop** the files as `assets/<feature>-light.mp4` and
63+
`assets/<feature>-dark.mp4`. Two things bite every time:
64+
- Recordings almost always arrive with the **light variant unsuffixed**
65+
(`foo.mp4`, only `foo-dark.mp4` is tagged). Rename it to `foo-light.mp4`.
66+
- The source filename (e.g. whatever's in `~/Downloads`) is irrelevant — the
67+
`<feature>` prefix must be the **media id** you'll use in `WelcomeScreen` and
68+
follow the existing convention, not whatever the file was called.
69+
70+
No `assets.d.ts` change needed — `*.mp4`/`*.jpg` are wildcard modules.
71+
3. **Optimize**: `pnpm optimize:onboarding-videos`.
72+
4. **Make the poster** — the still shown before play. **Use the clip's first frame**
73+
and leave `startTime` at 0, so the poster, the first played frame, and the loop
74+
point are all the same with nothing to keep in sync:
75+
76+
```bash
77+
ffmpeg -y -i assets/<feature>-<theme>.mp4 \
78+
-frames:v 1 -vf "scale=1000:-2:flags=lanczos" -q:v 3 \
79+
assets/<feature>-<theme>.jpg
80+
```
81+
82+
5. **Wire it up** in `WelcomeScreen.tsx`. For a *new* media id, four edits, all keyed
83+
by the same slug: import the `.mp4` + `.jpg`, add the id to the `MediaId` union,
84+
add its entry to the `MEDIA` map (`startTime: 0`), and set `media: "<id>"` on the
85+
target `FeatureDef`.
86+
87+
### Replacing an existing clip
88+
89+
Keep the **same asset filename** and the wiring is untouched — none of step 5
90+
applies. Drop the new file over `assets/<feature>-<theme>.mp4`, run the optimizer (a
91+
fresh drop carries no skip-marker, so it re-encodes without `--force`), and
92+
**regenerate that poster** (step 4). If you replace only one theme, re-check that
93+
light and dark still share an aspect ratio — a clip with stray padding frames
94+
differently from its sibling, and the gap shows when the user toggles theme.
95+
96+
## Poster = first frame (and startTime = 0)
97+
98+
`FeatureBentoCard` shows the poster while a card rests, then seeks the `<video>` to
99+
`MEDIA[id].startTime` on mount and loops back there (not necessarily to 0). The
100+
simple, default contract: **the poster is the clip's first frame and `startTime` is
101+
0**, so the still, the first played frame, and the loop point are all identical —
102+
nothing to keep in sync. Keep the poster the same pixel width as the clip (1000px)
103+
so the poster→video swap is seamless.
104+
105+
`startTime` can start/loop mid-clip if you ever need it (the `code-review` clip uses
106+
3s) — but then the poster MUST be that exact frame (`ffmpeg -ss <startTime> -i …`),
107+
or the still jumps the instant playback starts. Prefer the first-frame default
108+
unless you have a specific reason.
109+
110+
## Verify
111+
112+
```bash
113+
# clips: moov should print BEFORE mdat, width ≤ 1000:
114+
ffprobe -v trace <clip>.mp4 2>&1 | grep -o -m2 -E "type:'(moov|mdat)'"
115+
ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 <clip>.mp4
116+
117+
# wiring: a grown MediaId union may need a formatter reflow, so let biome fix it:
118+
npx biome check --write packages/ui/src/features/onboarding/components/WelcomeScreen.tsx
119+
pnpm --filter @posthog/ui typecheck
120+
```
121+
122+
To confirm playback feels smooth in the real app, use the `test-electron-app`
123+
skill to drive the running onboarding flow.

.claude/skills/test-electron-app/SKILL.md

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -50,17 +50,18 @@ pnpm build:deps # turbo build of @posthog/code deps
5050
tail -f /dev/null | pnpm dev:code # run in the background; leave it running
5151
```
5252

53-
`pnpm dev:code` is `electron-forge start -- --remote-debugging-port=9222` — just
54-
the app, no TUI and no watch rebuilders (fine for screenshotting/interacting).
55-
56-
**The `tail -f /dev/null |` is required, do not drop it.** `electron-forge start`
57-
runs an interactive "type `rs` to restart" stdin reader. With no stdin it hits
58-
EOF immediately, treats that as quit, tears down the Electron window, and exits
59-
**0** before the CDP port ever opens — so the app looks like it launched
60-
("✔ Launched Electron app") and then silently vanishes. Piping from
61-
`tail -f /dev/null` gives it a stdin that never closes, so it stays up. A
62-
pseudo-TTY via `script` does **not** help: `script` forwards the EOF (`^D`) and
63-
the app still quits.
53+
`pnpm dev:code` is `pnpm --filter code start`, i.e. `electron-vite dev --watch` — just
54+
the app, no `phrocs` TUI; it watches and rebuilds main/preload and hot-reloads the
55+
renderer (fine for screenshotting/interacting). The CDP port (`:9222`) is opened by
56+
the app itself in dev (the `remote-debugging-port` switch in
57+
`apps/code/src/main/bootstrap.ts`), not by a CLI flag.
58+
59+
The `tail -f /dev/null |` prefix is a harmless guard and no longer strictly required.
60+
The old `electron-forge start` ran an interactive "type `rs` to restart" stdin reader
61+
that hit EOF in a no-stdin shell, treated it as quit, and tore the Electron window
62+
down before the CDP port ever opened. `electron-vite dev` has no such reader, so a
63+
backgrounded `pnpm dev:code` with no stdin stays up on its own; keeping the pipe does
64+
no harm.
6465

6566
Then wait for the port and connect (poll, don't sleep blindly):
6667

@@ -113,7 +114,7 @@ rm -f /tmp/posthog-dev-lastuse
113114
```
114115

115116
Group-killing the launcher (`kill -TERM -PGID`) takes down `tail`, `pnpm`,
116-
`electron-forge` and Electron together, so nothing — not even the
117+
`electron-vite` and Electron together, so nothing — not even the
117118
`tail -f /dev/null` stdin pipe — lingers. Matching `remote-debugging-port=9222`
118119
hits only your dev instance (prod has no debug port and a separate
119120
`posthog-code-dev` profile), so it never touches the user's app. Verify with
@@ -212,11 +213,12 @@ PostHog Code orchestrates the agent, so the usual loop is: **prod** (the install
212213
- **Connection refused on :9222:** the app isn't running with the debug flag.
213214
Start it (see *Launching the app yourself* for the headless recipe). Verify the
214215
port: `lsof -i :9222` or `curl -s localhost:9222/json/version`.
215-
- **App launches then immediately exits 0; CDP never opens:** you started it with
216-
no stdin (background / no TTY) and `electron-forge` quit on stdin EOF. Relaunch
217-
with `tail -f /dev/null | pnpm dev:code` so stdin never closes (see *Launching
218-
the app yourself*). Note `pnpm dev` cannot run headlessly at all — its `phrocs`
219-
TUI needs a TTY; use `pnpm dev:code`.
216+
- **App launches then immediately exits; CDP never opens:** the old `electron-forge`
217+
"quit on stdin EOF" behavior is gone under `electron-vite`, so the
218+
`tail -f /dev/null |` prefix is optional and not the fix here. This is now usually a
219+
build error, the single-instance lock (another dev instance running) or a crash —
220+
check the `pnpm dev:code` output. Note `pnpm dev` cannot run headlessly at all — its
221+
`phrocs` TUI needs a TTY; use `pnpm dev:code`.
220222
- **Snapshot is empty / wrong window:** you're on the wrong target. Run
221223
`agent-browser tab` and switch to the "PostHog Code" page.
222224
- **Can't type into an input:** try `agent-browser keyboard type "text"` (types at

.github/workflows/agent-release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ jobs:
2323
persist-credentials: false
2424

2525
- name: Setup pnpm
26-
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0
26+
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
2727

2828
- name: Set up Node 24
29-
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
29+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
3030
with:
3131
node-version: 24
3232
cache: "pnpm"

.github/workflows/agent-tag.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
steps:
2222
- name: Get app token
2323
id: app-token
24-
uses: getsentry/action-github-app-token@d4b5da6c5e37703f8c3b3e43abb5705b46e159cc # v3.0.0
24+
uses: getsentry/action-github-app-token@5c1e90706fe007857338ac1bfbd7a4177db2f789 # v4.0.0
2525
with:
2626
app_id: ${{ secrets.GH_APP_ARRAY_RELEASER_APP_ID }}
2727
private_key: ${{ secrets.GH_APP_ARRAY_RELEASER_PRIVATE_KEY }}

.github/workflows/build.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
steps:
1919
- name: Detect relevant changes
2020
id: filter
21-
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
21+
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
2222
with:
2323
predicate-quantifier: every
2424
filters: |
@@ -51,10 +51,10 @@ jobs:
5151
persist-credentials: false
5252

5353
- name: Setup pnpm
54-
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0
54+
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
5555

5656
- name: Setup Node.js
57-
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
57+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
5858
with:
5959
node-version: 22
6060
cache: "pnpm"

0 commit comments

Comments
 (0)