From walkabout
Add a Walkabout to an app — a guided voice tour that walks the real pages with a narration-synced moving spotlight, an ask-the-app AI guide that logs every question, and one-command narrated demo videos. Use when adding onboarding tours, in-app help, or demo/training videos to any app.
How this skill is triggered — by the user, by Claude, or both
Slash command
/walkabout:walkaboutThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
You're giving an app three connected capabilities, built in this order:
You're giving an app three connected capabilities, built in this order:
Tour.tsx is the whole thing.The goal: the owner never gives a demo again. The app onboards, explains, and records itself.
All templates live in ${CLAUDE_PLUGIN_ROOT}/templates/ (this plugin's
templates/ dir) — FieldProof's REAL working files, worked examples not
scaffolds. Read them, transplant them, adapt names/paths/styling to the host
app. The deep reference (design rationale, full gotcha list, adopter notes) is
${CLAUDE_PLUGIN_ROOT}/docs/pattern.md (also at github.com/jezweb/walkabout) —
update it when you learn something new, and add the app to its Adopters list.
Copy Tour.tsx, steps.ts, halo.css (append to the app's global CSS).
Colour the halo from the host's --primary token, don't hardcode it.
halo.css carries FieldProof's sage→green in a few spots (the @property ring
initial, the tour-draw keyframe, the tour-breathe glow) — ship those on a
navy app and the halo is green, invisibly wrong until mid-tour. Key the ring +
glow off --primary instead (color-mix(in srgb, var(--primary) 55%, white)
while the line draws, settling to var(--primary); glow = var(--primary)):
the halo then brands itself AND survives a later reskin untouched. Proven on
RightCover — a navy rebrand recoloured the halo for free, zero tour edits,
because it referenced --primary. (@property initial-value can't take a
var(), so leave that neutral — only the keyframes need the token.) Then:
Restyle for the host first — the templates carry the source app's classes.
Tour.tsx and Assist.tsx use brand-card, font-display, primary-dark,
bg-surface, text-warning. On a shadcn host (the common case) map them once:
brand-card → rounded-lg border bg-card, font-display → drop, primary-dark
→ primary, text-white → text-primary-foreground, bg-surface →
bg-background, text-warning → text-amber-600 dark:text-amber-400.
The halo is box-shadow/conic-ring based (no outline); colour it from
--primary per the note above, not via these class swaps.
body = 2 lines. Narration = 2–5
(selector, text) segments per step in gen-tour-audio.py's SCRIPTS dict
— each segment describes ONE page section, top to bottom, like you're
showing a mate. End the last step with "that's the tour".data-tour="…" attribute to every element a segment describes.ELEVENLABS_API_KEY env var; voice Charlie
IKne3meq5aSn9XLyUdCD is a warm Australian male; eleven_turbo_v2_5 for
drafts, a richer model for final renders). It writes public/tour/*.mp3
AND tour/cues.gen.ts — commit both.useTour() into the app shell; offer once on first sign-in
(localStorage), restartable from a footer/menu, ?tour=N deep links.Verification is wandering, not watching: start the tour, then click
around mid-step. The guide must pause itself when you leave the step's page
("Paused while you explore"), resume where it left off, and NEVER replay
audio or yank you back when you click things. Run it on the page with the
highest z-index content (maps!) — the card is z-[1100] for a reason.
Verifying with automation: automated Chrome CAN play the narration — two
ways to grant it. Launch with --autoplay-policy=no-user-gesture-required
(the recorder templates already do), or start the tour with a REAL input
click — page.click() / getByRole(...).click() synthesise trusted events
and count as the gesture. What does NOT count: page.evaluate(() => btn.click()) — a JS-initiated click grants no gesture, so play() rejects,
ontimeupdate/onended never fire, and the spotlight + auto-advance look
broken when they aren't. End-to-end proof in automation: start the tour and
wait for the step counter to advance by itself (proven on FieldProof —
headless deep-link runs auto-advance fine with the flag).
Traps that WILL bite if you deviate from the template (full list in the knowledge doc):
[i] ONLY. Adding navigate (identity
changes per location) replays audio on every click — or hard-HANGS on
self-redirecting index routes.play() and open paused.arrivedRef is what stops the wander-detector from pausing the tour
during its own step navigation. Don't simplify it away.Copy Assist.tsx, assist-routes.ts, assist-knowledge.ts, questions.sql.
knowledge.ts entirely — it's the assistant's ONLY truth
source. Plain prose: what the app is, every page, every flow, limits, who
to contact. Facts from the code, never imagination (no invented pricing,
stats, contacts). Leave a header comment: update this file in the same
commit as any feature change — and add that rule to the app's CLAUDE.md.Verify live with three questions: one the guide covers (expect a grounded, specific answer), one it doesn't — pricing works well — (expect a plain "the guide doesn't cover this" + contact, NOT an invention), and one from a specific page (the page path is sent as context).
Copy record-tour.mjs and record-demo.mjs into the app's scripts dir.
Needs: playwright devDependency, ffmpeg/ffprobe on PATH, seeded data
that looks good on camera, and a headless-friendly sign-in.
Headless auth is the real blocker, not a one-liner — solve it first. The
templates' localStorage.setItem('<app>:api_key', …) bootstrap ONLY works for
API-key auth. Cookie/OAuth apps (better-auth and most modern stacks) can't do
that — and you do NOT add an API-key feature just to record. Two real options:
storageState (default, any auth, zero app change): sign in
once by hand, await context.storageState({ path: 'auth-state.json' }),
gitignore that file (it holds a live session cookie), then the recorder uses
newContext({ storageState: 'auth-state.json' }) and skips sign-in entirely.
Re-capture when the session TTL lapses.Also set STEPS in record-tour.mjs to the app's step count (it's hardcoded).
record-tour.mjs — records the tour headless: LOSSLESS PNG frames via
CDP screencast (timestamps included), then ffmpeg assembles and muxes the
ORIGINAL MP3s at offsets measured by patching Audio.play in-page.
Two capture paths that DON'T work — don't re-derive them: Playwright's
recordVideo (its adaptive encoder makes the WHOLE page shimmer/blink
several times a second), and getDisplayMedia tab-capture (needs a human
picking the tab; a window pick records silent video).record-demo.mjs — feature demos: segments of { say, do?, delayMs? }.
Narration cached by text-hash (iterating on actions is free); each
action fires at its narration offset — the voice says "type the
address…" while the harness types. Write actions with ROLE-BASED
locators (getByRole('button', { name: … }), not CSS) — the demo then
only renders when the markup carries real roles and names, making every
demo an accessibility regression test for free. Other viewports are one line
(390×844 → 9:16 Shorts). The same specs yield GIF slices and
reproducible screenshots for written guides.Verify by inspection, not by exit code: extract frames at known offsets
(ffmpeg -ss N -frames:v 1) and LOOK at them — right page, spotlight/action
visible; volumedetect a narration window to confirm audio landed. Then
ship the promo MP4 in the app's static assets with a <video controls>
player on its how-it-works page.
When a clip is proof of a fix rather than promo (a reviewer or client will
trust it), gate it: before sharing, run a blind audit — an independent
agent, told only the claim, samples frames and must find the pixels that show
the whole claim or it FAILs. Trust the clip only when the journey shows
precondition→action→outcome, the harness asserts the real state, AND the blind
audit confirms the pixels. Full pattern + the proven weaknesses in
docs/pattern.md → "Gate the proof".
docs/pattern.md, with anything new
you learned (gotchas earned there compound across every future adopter).
This is a PUBLIC repo — anonymise. Contribute the generalisable pattern
only; never commit project/client specifics (client or company names,
internal product names, hostnames/URLs, account/zone IDs, tokens, secrets,
real emails, file paths, schema/table names). Describe the app by its shape
("a Cloudflare Workers BI app", "a private healthcare client", "a shadcn
host"), as the Adopters list already does. The lesson is the gift; the
client's identity is not yours to share.Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub jezweb/walkabout --plugin walkabout