llmer-demo
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseDemo Flow Runner
演示流程运行器
You generate and run Playwright automation scripts from natural language scenario descriptions, with full recording (video + click/keystroke visualization). Supports both browser demos (HAR + video) and terminal/CLI demos (video of real terminal sessions via xterm.js + node-pty).
本工具可基于自然语言场景描述生成并运行Playwright自动化脚本,支持全量录制(视频+点击/按键操作可视化)。同时支持浏览器演示(HAR + 视频)和终端/CLI演示(通过xterm.js + node-pty录制真实终端会话的视频)。
Preamble (run first)
前置检查(首先运行)
bash
undefinedbash
undefinedCheck skill lib
Check skill lib
if [ -f ".claude/skills/llmer-demo/lib/index.js" ]; then
echo "SKILL_LIB: ready"
else
echo "SKILL_LIB: missing"
fi
if [ -f ".claude/skills/llmer-demo/lib/index.js" ]; then
echo "SKILL_LIB: ready"
else
echo "SKILL_LIB: missing"
fi
Check Playwright
Check Playwright
if npx playwright --version >/dev/null 2>&1; then
echo "PLAYWRIGHT: $(npx playwright --version 2>&1)"
else
echo "PLAYWRIGHT: missing"
fi
if npx playwright --version >/dev/null 2>&1; then
echo "PLAYWRIGHT: $(npx playwright --version 2>&1)"
else
echo "PLAYWRIGHT: missing"
fi
Check Chromium
Check Chromium
if [ -d "$(npx playwright install --dry-run chromium 2>/dev/null | head -1)" ] || npx playwright install --dry-run chromium 2>&1 | grep -q "is already installed"; then
echo "CHROMIUM: ready"
else
echo "CHROMIUM: missing"
fi
if [ -d "$(npx playwright install --dry-run chromium 2>/dev/null | head -1)" ] || npx playwright install --dry-run chromium 2>&1 | grep -q "is already installed"; then
echo "CHROMIUM: ready"
else
echo "CHROMIUM: missing"
fi
Check ffmpeg (required for video)
Check ffmpeg (required for video)
if command -v ffmpeg >/dev/null 2>&1; then
echo "FFMPEG: $(ffmpeg -version 2>&1 | head -1)"
else
echo "FFMPEG: missing (video conversion will fail)"
fi
if command -v ffmpeg >/dev/null 2>&1; then
echo "FFMPEG: $(ffmpeg -version 2>&1 | head -1)"
else
echo "FFMPEG: missing (video conversion will fail)"
fi
Check node-pty (required for terminal demos, optional otherwise)
Check node-pty (required for terminal demos, optional otherwise)
if node -e "require('node-pty')" 2>/dev/null; then
echo "NODE_PTY: ready"
else
echo "NODE_PTY: missing (install with 'npm install node-pty' for terminal demos)"
fi
if node -e "require('node-pty')" 2>/dev/null; then
echo "NODE_PTY: ready"
else
echo "NODE_PTY: missing (install with 'npm install node-pty' for terminal demos)"
fi
Check for .demoflow
Check for .demoflow
if [ -d ".demoflow" ]; then
echo "DEMOFLOW: initialized"
ls .demoflow/scenarios/ 2>/dev/null | head -10
else
echo "DEMOFLOW: not initialized (run /llmer-demo init)"
fi
If `SKILL_LIB` is `missing`: the skill was not installed correctly. Tell the user to run `npx skills add llmer/skill-demoflow`.
If `PLAYWRIGHT` is `missing`: run `npm install --save-dev @playwright/test`.
If `CHROMIUM` is `missing`: run `npx playwright install chromium`.
If `FFMPEG` is `missing`: warn the user that video conversion requires ffmpeg. They can install it with `brew install ffmpeg` (macOS) or `apt install ffmpeg` (Linux). Recording will still work but MP4 output will be skipped.
If `NODE_PTY` is `missing` and the user requests a terminal demo: run `npm install node-pty`. This is only required for terminal/CLI demos, not browser demos.if [ -d ".demoflow" ]; then
echo "DEMOFLOW: initialized"
ls .demoflow/scenarios/ 2>/dev/null | head -10
else
echo "DEMOFLOW: not initialized (run /llmer-demo init)"
fi
如果`SKILL_LIB`显示`missing`:说明技能安装异常,告知用户运行`npx skills add llmer/skill-demoflow`。
如果`PLAYWRIGHT`显示`missing`:运行`npm install --save-dev @playwright/test`。
如果`CHROMIUM`显示`missing`:运行`npx playwright install chromium`。
如果`FFMPEG`显示`missing`:警告用户视频转换需要ffmpeg,可通过`brew install ffmpeg`(macOS)或`apt install ffmpeg`(Linux)安装。录制功能仍可正常使用,但会跳过MP4输出。
如果`NODE_PTY`显示`missing`且用户需要终端演示:运行`npm install node-pty`。仅终端/CLI演示需要该依赖,浏览器演示不需要。Subcommands
子命令
Check first:
$ARGUMENTS- → Run the Init Flow to explore the project and scaffold
init.demoflow/ - → List available scenarios from
listand targets from.demoflow/scenarios/.demoflow/targets/ - → Launch the DemoFlow Studio web UI for adjusting frame options on existing recordings:
studio(ornpx tsx src/studio.tsif built). Opens at http://localhost:3274npm run studio - → Re-render an existing capture without re-recording. Only works if
render [scenario-name] [--style macos|windows-xp|windows-98|macos-terminal|vscode|none] [--title "..."]exists.output/{name}/recording.webm - Anything else → Run a scenario (see Run Flow below)
首先检查参数:
$ARGUMENTS- → 运行初始化流程,扫描项目并生成
init脚手架.demoflow/ - → 列出
list下的可用场景和.demoflow/scenarios/下的目标环境.demoflow/targets/ - → 启动DemoFlow Studio网页UI,用于调整已有录制的帧选项:
studio(如果已构建可运行npx tsx src/studio.ts),访问地址为http://localhost:3274npm run studio - → 无需重新录制,重新渲染已有捕获文件,仅当
render [scenario-name] [--style macos|windows-xp|windows-98|macos-terminal|vscode|none] [--title "..."]存在时生效output/{name}/recording.webm - 其他参数 → 运行对应场景(参考下方运行流程)
Init Flow
初始化流程
When is , you explore the project and bootstrap .
$ARGUMENTSinit.demoflow/当为时,扫描项目并初始化目录。
$ARGUMENTSinit.demoflow/What to do
操作步骤
-
Ensure runtime dependencies are installed:
- Verify the skill lib exists: (installed by
.claude/skills/llmer-demo/lib/index.js)skills add - Check if is in
@playwright/test. If not:package.jsonnpm install --save-dev @playwright/test - Check if Playwright Chromium is available. If not:
npx playwright install chromium
- Verify the skill lib exists:
-
Explore the codebase using the Explore agent:
- Find all routes/pages (look for or
app/directory structures, route files, page components)pages/ - Identify the auth flow (login, signup, verify, OAuth — look for auth pages, middleware, session handling)
- Find key interactive components (forms, wizards, modals, dialogs)
- Detect the framework (Next.js, Remix, SvelteKit, etc.) and UI library (shadcn, MUI, etc.)
- Look for existing environment config (.env.example, .env.local) to discover URLs, API endpoints, email services
- Check if Supabase, Mailpit, or other local services are configured (docker-compose, supabase/config.toml)
- Find all routes/pages (look for
-
Generatefrom what you discover:
.demoflow/context.md- Auth flow (login page selectors, OTP input, submit buttons)
- Key pages and their interactive elements
- Navigation patterns (sidebar, breadcrumbs, tabs, header links)
- Modals/dialogs that might appear during flows
- Rate limits or gotchas you spot in the code
- Keep it concise — focus on what a Playwright script would need to know
-
Generate target configs in:
.demoflow/targets/- — detect the dev server URL (from package.json scripts, .env), and if there's a local email server (Mailpit/Inbucket), configure auto-OTP
local.md - (if a prod URL is discoverable) — use prompt strategy for OTP
production.md - Ask the user to confirm URLs and credentials you can't find
-
Suggest 3-5 scenarios based on the flows you discover:
- The primary happy path (signup → first action → core feature)
- A CRUD flow (create, read, update, delete of the main entity)
- Navigation coverage (visit every major section)
- An edge case or error flow if obvious
- Present these to the user and ask which to generate. Write the selected ones to
.demoflow/scenarios/
-
Create the directory structure if it doesn't exist:
.demoflow/ ├── context.md ├── targets/ │ ├── local.md │ └── production.md └── scenarios/ └── (suggested scenarios) -
Ensureis excluded from the build:
scripts/- Read and check if
tsconfig.jsonis in thescriptsarrayexclude - If not, add to the
"scripts"array — Playwright imports are Node-only and will break framework builds (Next.js, Remix, etc.) if included in the compilation scopeexclude - If there's no array, create one with
exclude["scripts"]
- Read
-
Report what was created and suggest the user review context.md for accuracy.
-
确保运行时依赖已安装:
- 校验技能库文件存在:(通过
.claude/skills/llmer-demo/lib/index.js命令安装)skills add - 检查中是否包含
package.json,如果没有则运行@playwright/testnpm install --save-dev @playwright/test - 检查Playwright Chromium是否可用,如果没有则运行
npx playwright install chromium
- 校验技能库文件存在:
-
使用Explore Agent扫描代码库:
- 查找所有路由/页面(查找或
app/目录结构、路由文件、页面组件)pages/ - 识别认证流程(登录、注册、验证、OAuth — 查找认证页面、中间件、会话处理逻辑)
- 查找核心交互组件(表单、向导、弹窗、对话框)
- 检测使用的框架(Next.js、Remix、SvelteKit等)和UI库(shadcn、MUI等)
- 查找现有环境配置(.env.example、.env.local)以发现URL、API端点、邮件服务
- 检查是否配置了Supabase、Mailpit或其他本地服务(docker-compose、supabase/config.toml)
- 查找所有路由/页面(查找
-
基于扫描结果生成:
.demoflow/context.md- 认证流程(登录页选择器、OTP输入框、提交按钮)
- 核心页面及其交互元素
- 导航模式(侧边栏、面包屑、标签页、头部链接)
- 流程中可能出现的弹窗/对话框
- 代码中发现的速率限制或注意事项
- 保持内容简洁,聚焦于Playwright脚本需要的信息
-
在下生成目标环境配置:
.demoflow/targets/- — 检测开发服务器URL(从package.json脚本、.env中获取),如果存在本地邮件服务器(Mailpit/Inbucket)则配置自动OTP
local.md - (如果能发现生产环境URL) — 使用提示策略获取OTP
production.md - 对于无法找到的URL和凭证,询问用户确认
-
基于发现的流程建议3-5个场景:
- 核心 happy path(注册 → 首次操作 → 核心功能)
- CRUD流程(主实体的创建、查询、更新、删除)
- 导航覆盖(访问所有主要板块)
- 明显的边界 case 或错误流程
- 将这些场景展示给用户并询问要生成哪些,将选中的场景写入
.demoflow/scenarios/
-
如果目录不存在则创建目录结构:
.demoflow/ ├── context.md ├── targets/ │ ├── local.md │ └── production.md └── scenarios/ └── (suggested scenarios) -
确保目录被排除在构建之外:
scripts/- 读取,检查
tsconfig.json数组中是否包含excludescripts - 如果没有,将添加到
"scripts"数组 — Playwright导入仅适用于Node环境,如果被包含在编译范围内会破坏框架构建(Next.js、Remix等)exclude - 如果不存在数组,则创建该数组并添加
exclude["scripts"]
- 读取
-
告知用户已创建的内容,建议用户核对context.md的准确性。
Run Flow
运行流程
Step 1: Resolve the scenario
步骤1:解析场景
If matches a file in (by name, with or without ), read that file. Otherwise treat as an inline flow description.
$ARGUMENTS.demoflow/scenarios/.md$ARGUMENTSIf is present in arguments, use that target. Otherwise use the field from the scenario's section.
--target <name>target:## Config如果匹配下的文件(按名称匹配,可带或不带后缀),则读取该文件。否则将视为内联流程描述。
$ARGUMENTS.demoflow/scenarios/.md$ARGUMENTS如果参数中包含,则使用该目标环境。否则使用场景部分的字段指定的目标。
--target <name>## Configtarget:Step 2: Read target + context
步骤2:读取目标配置和上下文
- Read — this is the DUT (Device Under Test) config that tells you the base URL, how to handle auth/OTP, timeouts, and environment-specific behavior
.demoflow/targets/{target-name}.md - Read if it exists — app-specific UI patterns, selectors, navigation hints
.demoflow/context.md - Read the scenario file (or use the inline description)
- If needed, read relevant source files to understand selectors and page structure
- 读取— 这是被测系统(DUT)配置,包含基础URL、认证/OTP处理方式、超时时间和环境特定行为
.demoflow/targets/{target-name}.md - 如果存在则读取 — 包含应用特定的UI模式、选择器、导航提示
.demoflow/context.md - 读取场景文件(或使用内联描述)
- 必要时读取相关源文件以理解选择器和页面结构
Step 2b: Check for valid existing capture
步骤2b:检查是否存在有效的已有捕获
Before generating and running a new script, check if a valid capture already exists:
typescript
import { isCaptureValid, render } from '../.claude/skills/llmer-demo/lib/index.js'
if (isCaptureValid('output/scenario-name', { scenarioPath, targetPath })) {
// Skip re-recording — just re-render with current options
const result = await render('output/scenario-name', { frameStyle: 'macos' })
console.log('Re-rendered from cache:', result.mp4Path)
} else {
// Capture is stale — run the full recording flow below
}If returns true, skip to rendering. This avoids expensive browser replay when only frame options changed.
isCaptureValid()生成并运行新脚本之前,检查是否已存在有效的捕获:
typescript
import { isCaptureValid, render } from '../.claude/skills/llmer-demo/lib/index.js'
if (isCaptureValid('output/scenario-name', { scenarioPath, targetPath })) {
// Skip re-recording — just re-render with current options
const result = await render('output/scenario-name', { frameStyle: 'macos' })
console.log('Re-rendered from cache:', result.mp4Path)
} else {
// Capture is stale — run the full recording flow below
}如果返回true,直接跳转到渲染步骤。避免仅修改帧选项时重复执行耗时的浏览器回放。
isCaptureValid()Step 3: Generate a step-based script
步骤3:生成基于步骤的脚本
Write a script to (e.g. ) that uses with a declarative array. Do not write imperative Playwright code — the step runner handles selectors, waits, retries, and error screenshots automatically.
scripts/demo-{scenario-name}.tsdemo-full-workflow.tsrunSteps()Step[]- Imports from the skill lib (path relative to ):
scripts/typescriptimport { launchWithRecording, finalize, runSteps, isCaptureValid, render, type Step } from '../.claude/skills/llmer-demo/lib/index.js' - Config variables at the top from the target config: ,
BASE_URL, timeoutsTEST_EMAIL - A array that maps 1:1 from the scenario
steps: Step[] - Fixed boilerplate: →
launchWithRecording()→runSteps()finalize()
Output directory:
output/{scenario-name}/将脚本写入(例如),使用和声明式的数组。不要编写命令式的Playwright代码 — 步骤运行器会自动处理选择器解析、等待、重试和错误截图。
scripts/demo-{scenario-name}.tsdemo-full-workflow.tsrunSteps()Step[]- 从技能库导入(路径相对于):
scripts/typescriptimport { launchWithRecording, finalize, runSteps, isCaptureValid, render, type Step } from '../.claude/skills/llmer-demo/lib/index.js' - 顶部的配置变量来自目标配置:、
BASE_URL、超时时间TEST_EMAIL - 与场景一一映射的数组
steps: Step[] - 固定样板代码:→
launchWithRecording()→runSteps()finalize()
输出目录:
output/{scenario-name}/Step Type Reference
步骤类型参考
| Action | Fields | What it does |
|---|---|---|
| | Go to URL, auto-waits for networkidle |
| | Click element — runner resolves text to the right selector |
| | Type into input — clears first by default |
| | Choose dropdown option |
| | Press keyboard key (e.g. |
| | Wait for time, URL pattern ( |
| | Verify element is visible/hidden/attached |
| | Save screenshot to output dir |
| | Extract value into a variable |
| | Prompt user for input (OTP, etc.) — auto-pauses video |
| — | Manually trim idle time from video |
| | Escape hatch for custom logic (OTP fetch, API calls) |
| Action | Fields | What it does |
|---|---|---|
| | Go to URL, auto-waits for networkidle |
| | Click element — runner resolves text to the right selector |
| | Type into input — clears first by default |
| | Choose dropdown option |
| | Press keyboard key (e.g. |
| | Wait for time, URL pattern ( |
| | Verify element is visible/hidden/attached |
| | Save screenshot to output dir |
| | Extract value into a variable |
| | Prompt user for input (OTP, etc.) — auto-pauses video |
| — | Manually trim idle time from video |
| | Escape hatch for custom logic (OTP fetch, API calls) |
Target resolution (target
field)
target目标解析(target
字段)
targetThe runner tries multiple Playwright strategies automatically. Just use the visible text:
- → tries getByRole(button), getByText, etc. — finds what works
"Create Workspace" - →
"label:Email"— use when text is ambiguouspage.getByLabel('Email') - →
"placeholder:Enter name"page.getByPlaceholder('Enter name') - →
"role:link[Dashboard]"page.getByRole('link', { name: 'Dashboard' }) - →
"testid:submit-btn"page.getByTestId('submit-btn') - →
"css:.custom-selector"— last resortpage.locator('.custom-selector')
Default: just use the visible text. Only add prefixes when you need to disambiguate.
运行器会自动尝试多种Playwright策略,直接使用可见文本即可:
- → 尝试getByRole(button)、getByText等 — 自动匹配可用的选择器
"Create Workspace" - →
"label:Email"— 文本存在歧义时使用page.getByLabel('Email') - →
"placeholder:Enter name"page.getByPlaceholder('Enter name') - →
"role:link[Dashboard]"page.getByRole('link', { name: 'Dashboard' }) - →
"testid:submit-btn"page.getByTestId('submit-btn') - →
"css:.custom-selector"— 最后兜底方案page.locator('.custom-selector')
默认规则:直接使用可见文本,仅当需要消除歧义时添加前缀。
Variable interpolation
变量插值
saveinput${name}typescript
{ action: 'save', name: 'ws_id', from: 'url', pattern: '/workspaces/([^/]+)' },
{ action: 'navigate', url: `${BASE_URL}/workspaces/\${ws_id}/board` },Note: use in template literals so JavaScript doesn't interpolate — the runner handles it at runtime.
\${}saveinput${name}typescript
{ action: 'save', name: 'ws_id', from: 'url', pattern: '/workspaces/([^/]+)' },
{ action: 'navigate', url: `${BASE_URL}/workspaces/\${ws_id}/board` },注意:在模板字符串中使用避免JavaScript提前插值,运行器会在运行时处理插值逻辑。
\${}Key pattern
核心代码模板
typescript
import { launchWithRecording, finalize, runSteps, isCaptureValid, render, type Step } from '../.claude/skills/llmer-demo/lib/index.js'
const BASE_URL = '...' // from target
const TEST_EMAIL = '...' // from target
const SCENARIO_PATH = '.demoflow/scenarios/scenario-name.md'
const TARGET_PATH = '.demoflow/targets/local.md'
const steps: Step[] = [
{ action: 'navigate', url: `${BASE_URL}/login` },
{ action: 'fill', target: 'label:Email', value: TEST_EMAIL },
{ action: 'click', target: 'Continue with Email' },
{ action: 'wait', for: '**/verify' },
// OTP via Mailpit (custom logic → use exec)
{ action: 'exec', description: 'Fetch OTP from Mailpit', fn: async (ctx) => {
const res = await fetch('http://127.0.0.1:8025/api/v1/messages')
const msgs = await res.json() as any
const body = msgs.messages[0]?.Text ?? ''
ctx.vars.otp = (body.match(/(\d{6})/)?.[1]) ?? ''
}},
{ action: 'fill', target: 'placeholder:Enter code', value: '\${otp}' },
{ action: 'click', target: 'Verify' },
{ action: 'wait', for: '**/dashboard' },
// OR: OTP via user prompt
// { action: 'input', message: 'Enter the OTP code', saveAs: 'otp' },
]
async function main() {
if (isCaptureValid('output/scenario-name', { scenarioPath: SCENARIO_PATH, targetPath: TARGET_PATH })) {
console.log('Valid capture exists — re-rendering only')
const result = await render('output/scenario-name', { frameStyle: 'macos' })
console.log('Video:', result.mp4Path)
return
}
const session = await launchWithRecording({
outputDir: 'output/scenario-name',
headed: true,
slowMo: 100,
desktopFrame: true,
scenarioPath: SCENARIO_PATH,
targetPath: TARGET_PATH,
})
try {
await runSteps(session, steps, {
onStep: (i, step, status) => console.log(`[${status}] Step ${i + 1}: ${step.description ?? step.action}`)
})
} finally {
const result = await finalize(session)
console.log('HAR:', result.harPath)
console.log('Video:', result.mp4Path)
}
}
main()typescript
import { launchWithRecording, finalize, runSteps, isCaptureValid, render, type Step } from '../.claude/skills/llmer-demo/lib/index.js'
const BASE_URL = '...' // from target
const TEST_EMAIL = '...' // from target
const SCENARIO_PATH = '.demoflow/scenarios/scenario-name.md'
const TARGET_PATH = '.demoflow/targets/local.md'
const steps: Step[] = [
{ action: 'navigate', url: `${BASE_URL}/login` },
{ action: 'fill', target: 'label:Email', value: TEST_EMAIL },
{ action: 'click', target: 'Continue with Email' },
{ action: 'wait', for: '**/verify' },
// OTP via Mailpit (custom logic → use exec)
{ action: 'exec', description: 'Fetch OTP from Mailpit', fn: async (ctx) => {
const res = await fetch('http://127.0.0.1:8025/api/v1/messages')
const msgs = await res.json() as any
const body = msgs.messages[0]?.Text ?? ''
ctx.vars.otp = (body.match(/(\d{6})/)?.[1]) ?? ''
}},
{ action: 'fill', target: 'placeholder:Enter code', value: '\${otp}' },
{ action: 'click', target: 'Verify' },
{ action: 'wait', for: '**/dashboard' },
// OR: OTP via user prompt
// { action: 'input', message: 'Enter the OTP code', saveAs: 'otp' },
]
async function main() {
if (isCaptureValid('output/scenario-name', { scenarioPath: SCENARIO_PATH, targetPath: TARGET_PATH })) {
console.log('Valid capture exists — re-rendering only')
const result = await render('output/scenario-name', { frameStyle: 'macos' })
console.log('Video:', result.mp4Path)
return
}
const session = await launchWithRecording({
outputDir: 'output/scenario-name',
headed: true,
slowMo: 100,
desktopFrame: true,
scenarioPath: SCENARIO_PATH,
targetPath: TARGET_PATH,
})
try {
await runSteps(session, steps, {
onStep: (i, step, status) => console.log(`[${status}] Step ${i + 1}: ${step.description ?? step.action}`)
})
} finally {
const result = await finalize(session)
console.log('HAR:', result.harPath)
console.log('Video:', result.mp4Path)
}
}
main()Step 4: Run the script
步骤4:运行脚本
- Run the script in the background:
npx tsx scripts/demo-{scenario-name}.ts - Monitor for the input signal file: poll
output/{name}/.waiting-for-input - When the signal appears, read its contents (the prompt message) and ask the user for the value
- When the user responds, write their answer to
output/{name}/.input-value - The script will pick it up and continue automatically
- Wait for the script to complete
- 后台运行脚本:
npx tsx scripts/demo-{scenario-name}.ts - 监听输入信号文件:轮询
output/{name}/.waiting-for-input - 当信号文件出现时,读取其内容(提示消息)并向用户询问对应值
- 获取用户响应后,将回答写入
output/{name}/.input-value - 脚本会自动读取该值并继续执行
- 等待脚本执行完成
Step 5: Report results
步骤5:上报结果
Tell the user:
- Target used (local/production/etc.)
- HAR file path
- Video file path (MP4)
- Any errors that occurred
- Summary of what was captured
告知用户以下信息:
- 使用的目标环境(local/production等)
- HAR文件路径
- 视频文件路径(MP4)
- 发生的所有错误
- 捕获内容的摘要
Step 6: Offer adjustments
步骤6:提供调整选项
After reporting results, ask the user if they'd like to adjust the video. Present these options:
- Change frame style — re-render with ,
macos,windows-xp,windows-98,macos-terminal, orvscode(raw viewport)none - Change title — update the text shown in the frame's titlebar/tab
- Open Studio — launch the DemoFlow Studio web UI at http://localhost:3274 for live preview and adjustments
- Keep as-is — done
If the user picks a re-render option, call on the existing capture (no re-recording needed):
render()typescript
import { render } from '../.claude/skills/llmer-demo/lib/index.js'
const result = await render('output/{scenario-name}', {
frameStyle: 'windows-xp', // or 'macos', 'none'
title: 'Custom Title', // optional
})
console.log('Updated video:', result.mp4Path)If the user picks Studio, run:
node -e "import('./.claude/skills/llmer-demo/lib/studio.js').then(m => m.startStudio())"After any adjustment, report the updated file path and offer again — the user may want to try multiple styles.
上报结果后,询问用户是否需要调整视频,提供以下选项:
- 修改帧样式 — 重新渲染为、
macos、windows-xp、windows-98、macos-terminal或vscode(原始视口)样式none - 修改标题 — 更新帧标题栏/标签中显示的文本
- 打开Studio — 启动DemoFlow Studio网页UI(地址http://localhost:3274)进行实时预览和调整
- 保持原样 — 完成操作
如果用户选择重新渲染选项,调用现有捕获的方法(无需重新录制):
render()typescript
import { render } from '../.claude/skills/llmer-demo/lib/index.js'
const result = await render('output/{scenario-name}', {
frameStyle: 'windows-xp', // or 'macos', 'none'
title: 'Custom Title', // optional
})
console.log('Updated video:', result.mp4Path)如果用户选择Studio,运行:
node -e "import('./.claude/skills/llmer-demo/lib/studio.js').then(m => m.startStudio())"任何调整完成后,上报更新后的文件路径并再次询问是否需要调整,用户可能需要尝试多种样式。
Terminal Demo Flow
终端演示流程
For CLI-based product demos (agentic coding tools, CLIs, terminal applications). Uses xterm.js rendered in Playwright connected to a real PTY via node-pty.
适用于CLI类产品演示(智能编码工具、CLI、终端应用)。使用Playwright中渲染的xterm.js,通过node-pty连接到真实PTY。
Detecting terminal scenarios
识别终端场景
A scenario is a terminal demo if:
- Its section includes
## Configtype: terminal - The scenario only describes CLI commands (no URLs, no browser navigation)
- The user explicitly asks for a terminal/CLI demo
满足以下任一条件则为终端演示场景:
- 其部分包含
## Configtype: terminal - 场景仅描述CLI命令(无URL、无浏览器导航)
- 用户明确要求终端/CLI演示
Terminal scenario format
终端场景格式
markdown
undefinedmarkdown
undefinedClaude Code Refactoring Demo
Claude Code Refactoring Demo
Config
Config
type: terminal
shell: /bin/zsh
cwd: ~/projects/demo-app
frame: macos-terminal
typing_speed: 40ms
theme: dark-plus
type: terminal
shell: /bin/zsh
cwd: ~/projects/demo-app
frame: macos-terminal
typing_speed: 40ms
theme: dark-plus
Steps
Steps
[require: node npx]
[hide]
cd ~/projects/demo-app && git checkout demo-branch
[show]
- Show the project:
ls -la - [pause: 2s]
- Show the code:
cat server.ts - [pause: 3s]
- Run Claude Code:
npx claude "refactor server.ts to use async/await" - [wait-for: /✓|Done/ timeout: 120s]
- Show result:
cat server.ts - [pause: 3s]
- Run tests:
npm test - [wait-for: "passing" timeout: 30s]
undefined[require: node npx]
[hide]
cd ~/projects/demo-app && git checkout demo-branch
[show]
- Show the project:
ls -la - [pause: 2s]
- Show the code:
cat server.ts - [pause: 3s]
- Run Claude Code:
npx claude "refactor server.ts to use async/await" - [wait-for: /✓|Done/ timeout: 120s]
- Show result:
cat server.ts - [pause: 3s]
- Run tests:
npm test - [wait-for: "passing" timeout: 30s]
undefinedTerminal directives
终端指令
| Directive | Generated Code |
|---|---|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| Check dependencies exist before running |
| Set via |
| |
| |
| |
| Directive | Generated Code |
|---|---|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| Check dependencies exist before running |
| Set via |
| |
| |
| |
Terminal script pattern
终端脚本模板
typescript
import { launchTerminal, finalize, pauseRecording, resumeRecording } from '../.claude/skills/llmer-demo/lib/index.js'
const session = await launchTerminal({
outputDir: 'output/cli-demo',
shell: '/bin/zsh',
cwd: '/path/to/project',
desktopFrame: { style: 'macos-terminal', title: 'Terminal' },
theme: 'dark-plus',
typingSpeed: 50,
})
try {
// [hide] — setup commands hidden from video
pauseRecording(session)
await session.exec('cd ~/projects/demo-app')
resumeRecording(session)
// Visible demo steps
await session.exec('ls -la')
await new Promise(r => setTimeout(r, 2000))
await session.exec('npx claude "refactor server.ts"')
await session.waitForOutput(/Done|✓/, { timeout: 120_000 })
await session.exec('cat server.ts')
await new Promise(r => setTimeout(r, 3000))
} catch (err) {
await session.page.screenshot({ path: 'output/cli-demo/error.png' })
throw err
} finally {
const result = await finalize(session, { pageTitle: 'Claude Code Demo' })
console.log('Video:', result.mp4Path)
}typescript
import { launchTerminal, finalize, pauseRecording, resumeRecording } from '../.claude/skills/llmer-demo/lib/index.js'
const session = await launchTerminal({
outputDir: 'output/cli-demo',
shell: '/bin/zsh',
cwd: '/path/to/project',
desktopFrame: { style: 'macos-terminal', title: 'Terminal' },
theme: 'dark-plus',
typingSpeed: 50,
})
try {
// [hide] — setup commands hidden from video
pauseRecording(session)
await session.exec('cd ~/projects/demo-app')
resumeRecording(session)
// Visible demo steps
await session.exec('ls -la')
await new Promise(r => setTimeout(r, 2000))
await session.exec('npx claude "refactor server.ts"')
await session.waitForOutput(/Done|✓/, { timeout: 120_000 })
await session.exec('cat server.ts')
await new Promise(r => setTimeout(r, 3000))
} catch (err) {
await session.page.screenshot({ path: 'output/cli-demo/error.png' })
throw err
} finally {
const result = await finalize(session, { pageTitle: 'Claude Code Demo' })
console.log('Video:', result.mp4Path)
}launchTerminal(options) → TerminalSession
launchTerminal(options) → TerminalSessionlaunchTerminal(options) → TerminalSession
launchTerminal(options) → TerminalSessionLaunch a terminal session with Playwright recording: xterm.js in browser connected to real PTY.
| Option | Type | Default | Description |
|---|---|---|---|
| | required | Output directory for video, screenshots. |
| | | Terminal canvas pixel size. |
| | | Show the browser window. |
| | | Desktop frame style. |
| | | Shell to spawn. |
| | | Working directory for PTY. |
| | | Extra environment variables. |
| | | Color theme: |
| | | Font size in px. |
| | | Font family. |
| | | Default ms delay per character. |
通过Playwright录制启动终端会话:浏览器中的xterm.js连接到真实PTY。
| Option | Type | Default | Description |
|---|---|---|---|
| | required | Output directory for video, screenshots. |
| | | Terminal canvas pixel size. |
| | | Show the browser window. |
| | | Desktop frame style. |
| | | Shell to spawn. |
| | | Working directory for PTY. |
| | | Extra environment variables. |
| | | Color theme: |
| | | Font size in px. |
| | | Font family. |
| | | Default ms delay per character. |
TerminalSession methods
TerminalSession方法
| Method | Description |
|---|---|
| Type text character-by-character with visual delay. |
| Send keystroke: |
| Type command + Enter + wait for prompt to return. |
| Wait for regex/string to appear in terminal buffer. Default timeout: 30s. |
| Wait for shell prompt to return (command finished). Default timeout: 30s. |
| Clear the terminal screen. |
The session also has , , , like . Use / for pause trimming. Pass to at the end.
browsercontextpageoutputDirRecordingSessionpauseRecording(session)resumeRecording(session)finalize(session, { pageTitle: '...' })| Method | Description |
|---|---|
| Type text character-by-character with visual delay. |
| Send keystroke: |
| Type command + Enter + wait for prompt to return. |
| Wait for regex/string to appear in terminal buffer. Default timeout: 30s. |
| Wait for shell prompt to return (command finished). Default timeout: 30s. |
| Clear the terminal screen. |
会话还包含、、、等属性,和一致。可使用 / 进行暂停裁剪,最后调用结束。
browsercontextpageoutputDirRecordingSessionpauseRecording(session)resumeRecording(session)finalize(session, { pageTitle: '...' })Recording Library Reference
录制库参考
All functions are exported from the skill lib at .
.claude/skills/llmer-demo/lib/index.js所有函数都从技能库导出。
.claude/skills/llmer-demo/lib/index.jsrunSteps(session, steps, options?) → { vars }
runSteps(session, steps, options?) → { vars }runSteps(session, steps, options?) → { vars }
runSteps(session, steps, options?) → { vars }Execute a declarative array against a recording session. Handles selector resolution, waits, inter-step delays, error screenshots, and variable interpolation.
Step[]| Option | Type | Default | Description |
|---|---|---|---|
| | | Ms between steps for video readability. |
| | | Ms to wait for elements before failing. |
| | | Take error screenshot when a step fails. |
| | — | Progress callback for logging. |
Returns — all saved variables from and steps.
{ vars: Record<string, string> }saveinputOn failure, throws with message: . Takes screenshot automatically.
Step N failed (description): original errorerror-step-N.png针对录制会话执行声明式数组,自动处理选择器解析、等待、步骤间延迟、错误截图和变量插值。
Step[]| Option | Type | Default | Description |
|---|---|---|---|
| | | Ms between steps for video readability. |
| | | Ms to wait for elements before failing. |
| | | Take error screenshot when a step fails. |
| | — | Progress callback for logging. |
返回 — 所有和步骤保存的变量。
{ vars: Record<string, string> }saveinput失败时抛出异常,消息格式为:,自动拍摄截图。
Step N failed (description): original errorerror-step-N.pnglaunchWithRecording(options) → RecordingSession
launchWithRecording(options) → RecordingSessionlaunchWithRecording(options) → RecordingSession
launchWithRecording(options) → RecordingSessionLaunch a Chromium browser with full recording: HAR capture, video, and click visualization.
| Option | Type | Default | Description |
|---|---|---|---|
| | required | Output directory for HAR, video, screenshots. Created if missing. |
| | | Browser viewport size. Also sets video resolution. |
| | | Show the browser window. Set |
| | | Delay between actions in ms. Higher = more readable video. |
| | | Bypass HTTPS certificate errors (useful for local dev). |
| | | Wrap video in desktop chrome. See Desktop Frame. |
Returns a with , , , and .
RecordingSessionbrowsercontextpageoutputDir启动Chromium浏览器并开启全量录制:HAR捕获、视频、点击可视化。
| Option | Type | Default | Description |
|---|---|---|---|
| | required | Output directory for HAR, video, screenshots. Created if missing. |
| | | Browser viewport size. Also sets video resolution. |
| | | Show the browser window. Set |
| | | Delay between actions in ms. Higher = more readable video. |
| | | Bypass HTTPS certificate errors (useful for local dev). |
| | | Wrap video in desktop chrome. See Desktop Frame. |
返回包含、、和的对象。
browsercontextpageoutputDirRecordingSessionfinalize(session, overrides?) → RecordingResult
finalize(session, overrides?) → RecordingResultfinalize(session, overrides?) → RecordingResult
finalize(session, overrides?) → RecordingResultClose the browser, save a capture manifest, convert video to MP4, and apply desktop frame compositing. Must be called in a finally block — skipping this loses the HAR and video.
Optional : — use to set meaningful metadata for terminal sessions (which otherwise show ).
overrides{ pageTitle?: string, pageUrl?: string }localhost:XXXXXReturns:
| Field | Type | Description |
|---|---|---|
| | Path to the HAR file (always present) |
| | Path to MP4 video ( |
| | Path to raw WebM video |
Pipeline: close browser → rename WebM → save manifest.json (git hash, viewport, pauses, page info) → trim pauses (if any) → convert to MP4 → composite with desktop frame → clean up temps.
关闭浏览器、保存捕获清单、将视频转换为MP4并应用桌面帧合成。必须在finally块中调用 — 跳过该调用会丢失HAR和视频。
可选参数: — 用于为终端会话设置有意义的元数据(否则会显示)。
overrides{ pageTitle?: string, pageUrl?: string }localhost:XXXXX返回值:
| Field | Type | Description |
|---|---|---|
| | Path to the HAR file (always present) |
| | Path to MP4 video ( |
| | Path to raw WebM video |
处理流程:关闭浏览器 → 重命名WebM → 保存manifest.json(git哈希、视口、暂停点、页面信息) → 裁剪暂停片段(如果有) → 转换为MP4 → 合成桌面帧 → 清理临时文件。
render(outputDir, options?) → RenderResult
render(outputDir, options?) → RenderResultrender(outputDir, options?) → RenderResult
render(outputDir, options?) → RenderResultRe-render an existing capture to MP4 without re-recording. Reads viewport and pause data from . Use this to change frame style, title, or resolution on an already-captured recording.
manifest.json| Option | Type | Default | Description |
|---|---|---|---|
| | | Frame style. |
| | manifest page title | Window title / tab text. |
| | manifest page URL | Address bar URL (XP style). |
| | | Desktop resolution for the frame. |
Returns .
{ mp4Path: string | null }重新渲染已有捕获为MP4,无需重新录制。从读取视口和暂停数据,可用于修改已捕获录制的帧样式、标题或分辨率。
manifest.json| Option | Type | Default | Description |
|---|---|---|---|
| | | Frame style. |
| | manifest page title | Window title / tab text. |
| | manifest page URL | Address bar URL (XP style). |
| | | Desktop resolution for the frame. |
返回。
{ mp4Path: string | null }isCaptureValid(outputDir, options?) → boolean
isCaptureValid(outputDir, options?) → booleanisCaptureValid(outputDir, options?) → boolean
isCaptureValid(outputDir, options?) → booleanCheck whether a previous capture can be reused. Returns if:
true- and
manifest.jsonexist inrecording.webmoutputDir - Current git HEAD matches the manifest's commit hash
- Working tree is clean (not dirty)
- Skill lib hash matches (detects lib code changes across linked repos)
- Scenario/target file hashes match (if paths provided)
| Option | Type | Description |
|---|---|---|
| | Path to scenario file — hash is compared to manifest. |
| | Path to target file — hash is compared to manifest. |
Use this before recording to skip re-capture when only render options changed.
检查是否可以复用之前的捕获,满足以下条件时返回:
true- 中存在
outputDir和manifest.jsonrecording.webm - 当前git HEAD与清单中的commit哈希匹配
- 工作树干净(无改动)
- 技能库哈希匹配(检测链接仓库之间的库代码变更)
- 场景/目标文件哈希匹配(如果提供了路径)
| Option | Type | Description |
|---|---|---|
| | Path to scenario file — hash is compared to manifest. |
| | Path to target file — hash is compared to manifest. |
录制前调用该方法,可在仅修改渲染选项时跳过重新捕获。
requestInput(outputDir, message, options?) → string
requestInput(outputDir, message, options?) → stringrequestInput(outputDir, message, options?) → string
requestInput(outputDir, message, options?) → stringPause the script and wait for external input (OTP codes, 2FA tokens, manual confirmation).
| Option | Type | Default | Description |
|---|---|---|---|
| | — | If provided, video is auto-paused while waiting and idle time is trimmed from final video. |
| | | Max wait time in ms before throwing. |
Writes signal file. Polls for response file. Auto-cleans both files after input is received.
.waiting-for-input.input-value暂停脚本并等待外部输入(OTP码、2FA令牌、手动确认)。
| Option | Type | Default | Description |
|---|---|---|---|
| | — | If provided, video is auto-paused while waiting and idle time is trimmed from final video. |
| | | Max wait time in ms before throwing. |
写入信号文件,轮询响应文件,收到输入后自动清理两个文件。
.waiting-for-input.input-valuepauseRecording(session)
/ resumeRecording(session)
pauseRecording(session)resumeRecording(session)pauseRecording(session)
/ resumeRecording(session)
pauseRecording(session)resumeRecording(session)Manually mark idle periods. The paused segments are trimmed from the final MP4 using ffmpeg trim + concat filters. Use when you have a known wait that isn't handled by .
requestInputtypescript
pauseRecording(session)
await someSlowOperation()
resumeRecording(session)手动标记空闲时段,暂停的片段会通过ffmpeg裁剪+拼接过滤器从最终MP4中移除。当存在未被处理的已知等待时使用。
requestInputtypescript
pauseRecording(session)
await someSlowOperation()
resumeRecording(session)provideInput(outputDir, value)
provideInput(outputDir, value)provideInput(outputDir, value)
provideInput(outputDir, value)Write input from the skill/CLI side. Called by the skill runner after getting the value from the user.
从技能/CLI侧写入输入,技能运行器从用户获取值后调用该方法。
checkWaiting(outputDir) → string | null
checkWaiting(outputDir) → string | nullcheckWaiting(outputDir) → string | null
checkWaiting(outputDir) → string | nullCheck if a script is waiting for input. Returns the prompt message or .
null检查脚本是否在等待输入,返回提示消息或。
nullDesktop Frame
桌面帧
Videos are composited onto a desktop OS frame (titlebar + window chrome + wallpaper background) for polished output.
视频会被合成到桌面操作系统帧(标题栏+窗口边框+壁纸背景)上,输出效果更专业。
Options
选项
Pass to :
launchWithRecording({ desktopFrame: ... })| Value | Behavior |
|---|---|
| macOS Sonoma style frame |
| No frame — raw viewport video |
| macOS Sonoma with traffic lights and tab |
| Windows XP with IE chrome, taskbar, Start button (uses XP.css) |
| Windows 98 with classic grey chrome (uses 98.css) |
| macOS Terminal.app style — dark titlebar, traffic lights, no address bar |
| VS Code integrated terminal — dark chrome, tab bar, blue status bar |
| Override the tab/titlebar text (default: page title at finalize) |
| Desktop resolution (default: 1920x1080) |
| Shift window up/down from center (negative = up) |
| Solid wallpaper color (overrides default gradient) |
传入:
launchWithRecording({ desktopFrame: ... })| Value | Behavior |
|---|---|
| macOS Sonoma style frame |
| No frame — raw viewport video |
| macOS Sonoma with traffic lights and tab |
| Windows XP with IE chrome, taskbar, Start button (uses XP.css) |
| Windows 98 with classic grey chrome (uses 98.css) |
| macOS Terminal.app style — dark titlebar, traffic lights, no address bar |
| VS Code integrated terminal — dark chrome, tab bar, blue status bar |
| Override the tab/titlebar text (default: page title at finalize) |
| Desktop resolution (default: 1920x1080) |
| Shift window up/down from center (negative = up) |
| Solid wallpaper color (overrides default gradient) |
Frame anatomy
帧结构
macOS (default):
- Dark gradient wallpaper (purple/blue tones)
- Window vertically centered with rounded corners, drop shadow
- Titlebar: traffic lights (red/yellow/green) + centered tab with page title
- Content area: your recorded video
Windows XP (via XP.css):
- Blue sky + green hills wallpaper
- Authentic XP title bar with minimize/maximize/close
- Address bar with URL + Go button, status bar
- XP taskbar at bottom with green Start button + clock
Windows 98 (via 98.css):
- Teal wallpaper (classic default)
- Classic grey window chrome with beveled edges
- Address bar with URL, status bar
- Grey taskbar with Start button
macOS Terminal:
- Same purple/blue gradient wallpaper as macOS browser frame
- Dark titlebar (#3c3c3c) with traffic lights
- Centered title text (shell name or custom)
- No address bar, no tab — clean terminal window look
- Best for CLI/terminal demos
VS Code:
- Dark chrome (#1f1f1f) with traffic lights
- Tab bar with terminal tab icon ()
>_ - Blue status bar with branch name + line/col info
- Best for agentic coding tool demos shown in IDE context
macOS(默认):
- 深色渐变壁纸(紫/蓝色调)
- 垂直居中的窗口,带圆角和阴影
- 标题栏:红绿灯(红/黄/绿)+ 居中的页面标题标签
- 内容区域:录制的视频
Windows XP(基于XP.css):
- 蓝天+绿山壁纸
- 正宗的XP标题栏,带最小化/最大化/关闭按钮
- 带URL和Go按钮的地址栏、状态栏
- 底部的XP任务栏,带绿色开始按钮和时钟
Windows 98(基于98.css):
- 青色壁纸(经典默认)
- 经典灰色窗口边框,带斜角边缘
- 带URL的地址栏、状态栏
- 带开始按钮的灰色任务栏
macOS Terminal:
- 和macOS浏览器帧相同的紫/蓝渐变壁纸
- 深色标题栏(#3c3c3c),带红绿灯
- 居中的标题文本(shell名称或自定义)
- 无地址栏、无标签,简洁的终端窗口外观
- 最适合CLI/终端演示
VS Code:
- 深色边框(#1f1f1f),带红绿灯
- 标签栏,带终端标签图标()
>_ - 蓝色状态栏,带分支名称+行/列信息
- 最适合在IDE上下文展示的智能编码工具演示
How it works
实现原理
- Browser records WebM video at viewport size
- converts WebM → MP4 (with pause trimming if needed)
finalize() - Playwright renders the frame HTML to a PNG at the desktop resolution
- ffmpeg overlays the MP4 onto the frame PNG at the calculated content position
- Framed MP4 replaces the original
The frame is a static PNG — it doesn't change during the video. The page title shown in the tab is captured from the page at finalize time.
- 浏览器录制视口尺寸的WebM视频
- 将WebM转换为MP4(如果需要则裁剪暂停片段)
finalize() - Playwright将帧HTML渲染为桌面分辨率的PNG
- ffmpeg将MP4叠加到帧PNG的计算内容位置上
- 带帧的MP4替换原文件
帧是静态PNG,视频播放过程中不会变化。标签中显示的页面标题是finalize阶段从页面捕获的。
Click Visualization
点击可视化
Every page automatically gets a click visualization script injected via . When the user clicks anywhere:
addInitScript- A red circle (30px, semi-transparent) appears at the click point
- The circle expands to 2.5x and fades out over 900ms
- Removed from DOM after 1200ms
This is captured in the video recording — no post-processing needed. The visualization works across all page navigations (re-injected on each new page load).
每个页面都会通过自动注入点击可视化脚本,用户点击任意位置时:
addInitScript- 点击位置会出现一个红色半透明圆圈(30px)
- 圆圈在900ms内扩大到2.5倍并逐渐消失
- 1200ms后从DOM中移除
该效果会被录制到视频中,无需后期处理。可视化效果在所有页面导航中都生效(每次加载新页面时重新注入)。
Output Files
输出文件
After a successful run, contains:
output/{scenario-name}/| File | Description |
|---|---|
| Full network capture (importable in Chrome DevTools Network tab) |
| Polished video with click indicators + desktop frame |
| Raw video from Playwright (pre-conversion) |
| Capture metadata (git hash, viewport, pauses) + render options |
| Screenshot at point of failure (only on error) |
If desktop frame is disabled, is the raw viewport video without chrome.
recording.mp4成功运行后,目录包含:
output/{scenario-name}/| File | Description |
|---|---|
| Full network capture (importable in Chrome DevTools Network tab) |
| Polished video with click indicators + desktop frame |
| Raw video from Playwright (pre-conversion) |
| Capture metadata (git hash, viewport, pauses) + render options |
| Screenshot at point of failure (only on error) |
如果禁用了桌面帧,是没有边框的原始视口视频。
recording.mp4Target Resolution
目标环境解析
Targets are Markdown files in that describe the runtime environment. They contain:
.demoflow/targets/- Connection: URL and email/credentials
- Auth: How to handle OTP (auto via Mailpit, or prompt the user)
- Behavior: Timeouts, headed/headless, slow motion
- Notes: Environment-specific gotchas
When generating the script, read the target file and use its values. If a value uses , generate a unique ID (e.g. ).
${RUN_ID}Date.now().toString(36)目标环境是下的Markdown文件,描述运行时环境,包含:
.demoflow/targets/- 连接信息:URL和邮箱/凭证
- 认证:OTP处理方式(通过Mailpit自动获取,或提示用户输入)
- 行为:超时时间、 headed/headless模式、慢动作参数
- 注意事项:环境特定的问题
生成脚本时,读取目标文件并使用其值。如果值使用,则生成唯一ID(例如)。
${RUN_ID}Date.now().toString(36)Handling Directives
指令处理
When you see these in the scenario, map them to objects:
Step| Directive | Step |
|---|---|
| |
| |
| |
| |
| |
在场景中看到以下指令时,映射为对应的对象:
Step| Directive | Step |
|---|---|
| |
| |
| |
| |
| |
Script Generation Guidelines
脚本生成规范
- Map scenario text directly to Step objects. "Click Create Trust" → . Don't overthink selectors.
{ action: 'click', target: 'Create Trust' } - Use plain text targets by default. The runner resolves them automatically. Only add prefixes (,
label:, etc.) when the text is ambiguous.css: - Don't add manual waits or delays. The runner injects between steps and auto-waits for navigation after
actionDelaysteps.navigate - Use steps sparingly — only for custom logic like fetching OTP from Mailpit or calling APIs. Most scenarios need 0-2 exec steps.
exec - Never skip — even on error, it saves the HAR and whatever video was captured. The try/finally structure in the template handles this.
finalize() - Use variable interpolation () for dynamic values from
\${varName}orsavesteps. Don't thread local variables through code.input - The boilerplate never changes. Only modify the array and the config constants at the top.
steps
- 直接将场景文本映射为Step对象。"点击Create Trust" → ,无需过度纠结选择器。
{ action: 'click', target: 'Create Trust' } - 默认使用纯文本目标,运行器会自动解析。仅当文本存在歧义时添加前缀(、
label:等)。css: - 不要添加手动等待或延迟,运行器会在步骤之间注入,且
actionDelay步骤后自动等待导航完成。navigate - 尽量少用步骤 — 仅用于自定义逻辑,例如从Mailpit获取OTP或调用API。大多数场景需要0-2个exec步骤。
exec - 永远不要跳过— 即使发生错误,它也会保存HAR和已捕获的视频内容。模板中的try/finally结构已经处理了这一点。
finalize() - 使用变量插值()引用
\${varName}或save步骤的动态值,不要在代码中传递局部变量。input - 样板代码固定不变,仅修改数组和顶部的配置常量即可。
steps
Tips
提示
- Always call in finally. If you skip it, the HAR is lost and the browser process leaks.
finalize() - Pass to
{ session }. This auto-pauses video during idle waits so the final MP4 doesn't have dead time.requestInput() - Use for video quality. 100ms is a good default. Bump to 200-300ms for demos where you want the viewer to see each step clearly.
slowMo - Use /
pauseRecordingaround long waits. Any operation where the screen is static for >2s should be trimmed.resumeRecording - Set viewport to match your target audience. 1280x720 is a safe default. Use 1920x1080 for full-HD demos, but note the desktop frame adds chrome around it.
- The desktop frame title is captured at finalize. Navigate to the most meaningful page before the script ends so the title bar shows something useful.
- Use or
desktopFrame: { style: 'windows-xp' }for retro frames. XP uses authentic XP.css styling with IE chrome and taskbar. 98 gives classic grey beveled chrome. Both use the XP.css library for faithful rendering.'windows-98' - Use or
macos-terminalframe for terminal demos.vscodegives a clean Terminal.app look;macos-terminalwraps the terminal in VS Code chrome — great for agentic coding demos.vscode - For terminal demos, pass to
{ pageTitle: '...' }. Terminal sessions run atfinalize()so the default page title/URL are meaningless.localhost:XXXXX - Check for ffmpeg before running. Without it, you get HAR + WebM but no MP4 and no desktop frame compositing.
- 始终在finally中调用。如果跳过,HAR会丢失,浏览器进程会泄漏。
finalize() - 给传递
requestInput()参数。这会在空闲等待期间自动暂停视频,避免最终MP4出现空白片段。{ session } - 调整提升视频质量。100ms是不错的默认值,如果希望观众清晰看到每个步骤,可提升到200-300ms。
slowMo - 长时间等待前后使用/
pauseRecording。任何屏幕静止超过2秒的操作都应该被裁剪。resumeRecording - 根据目标受众设置视口。1280x720是安全的默认值,全高清演示可使用1920x1080,但注意桌面帧会在周围添加边框。
- 桌面帧标题在finalize阶段捕获。脚本结束前导航到最有意义的页面,确保标题栏显示有用的内容。
- 使用或
desktopFrame: { style: 'windows-xp' }实现复古帧效果。XP使用正宗的XP.css样式,带IE边框和任务栏;98提供经典的灰色斜角边框,两者都使用XP.css库实现逼真渲染。'windows-98' - 终端演示使用或
macos-terminal帧。vscode提供简洁的Terminal.app外观;macos-terminal将终端包裹在VS Code边框中 — 非常适合智能编码演示。vscode - 终端演示给传递
finalize()参数。终端会话运行在{ pageTitle: '...' },默认页面标题/URL没有意义。localhost:XXXXX - 运行前检查ffmpeg是否存在。没有ffmpeg时,你会得到HAR + WebM,但没有MP4和桌面帧合成。
Completion Status
完成状态
When the run finishes, report status:
- DONE — Scenario completed. HAR + video saved. Report file paths.
- DONE_WITH_CONCERNS — Completed, but with issues (flaky selectors, slow loads, skipped steps). List each concern.
- BLOCKED — Cannot proceed. State what is blocking (missing dependency, app not running, auth failed) and what was tried.
- NEEDS_CONTEXT — Missing information required to continue (no target URL, unknown auth flow, ambiguous scenario steps). State exactly what you need.
运行结束后,上报状态:
- DONE — 场景完成,HAR + 视频已保存,上报文件路径。
- DONE_WITH_CONCERNS — 已完成,但存在问题(选择器不稳定、加载缓慢、跳过步骤),列出所有问题。
- BLOCKED — 无法继续,说明阻塞原因(缺失依赖、应用未运行、认证失败)和已尝试的操作。
- NEEDS_CONTEXT — 缺少继续所需的信息(无目标URL、未知认证流程、场景步骤歧义),明确说明需要的信息。
Example Invocations
调用示例
- — explore the project, generate context + targets + suggested scenarios
/llmer-demo init - — show available scenarios and targets
/llmer-demo list - — runs
/llmer-demo workspace-switchingwith its default target.demoflow/scenarios/workspace-switching.md - — override to use production target
/llmer-demo workspace-switching --target production - — generates from inline description using default target
/llmer-demo "log in and navigate to the dashboard" - — terminal demo from inline description (auto-detected: no URLs)
/llmer-demo "run npm install then npm test and show the output" - — runs a terminal scenario from
/llmer-demo cli-onboarding(detected via.demoflow/scenarios/cli-onboarding.mdin Config)type: terminal
- — 扫描项目,生成上下文+目标配置+建议场景
/llmer-demo init - — 展示可用场景和目标环境
/llmer-demo list - — 使用默认目标运行
/llmer-demo workspace-switching.demoflow/scenarios/workspace-switching.md - — 覆盖为生产环境目标
/llmer-demo workspace-switching --target production - — 使用默认目标从内联描述生成场景
/llmer-demo "log in and navigate to the dashboard" - — 从内联描述生成终端演示(自动识别:无URL)
/llmer-demo "run npm install then npm test and show the output" - — 从
/llmer-demo cli-onboarding运行终端场景(通过Config中的.demoflow/scenarios/cli-onboarding.md识别)type: terminal