- name
- browser-preview
- tips_exempt
- Managed preview expiry is an internal lifecycle safety boundary; the existing browser-preview skill and status output already expose the operator-visible behavior.
- description
- Hub 内嵌浏览器预览 localhost 应用。 Use when: 写前端代码、跑 dev server、需要看页面效果、调 UI、operator说"看看效果"。 Not for: 后端纯 API 开发、不涉及页面的工作。 Output: 前端页面在 Hub browser panel 中实时预览。
- triggers
- ["看效果","看看页面","preview","浏览器预览","打开浏览器","pnpm dev","dev server","localhost","前端效果","看看 UI","HMR","热更新"]
# Browser Preview
Hub 内置了嵌入式浏览器面板(F120),可以直接预览运行中的 localhost 应用。猫猫写完前端代码不用让operator切浏览器看效果。
## 工作流
### 基础流程(端口发现 → 预览)
1. **启动 dev server**:交互 Terminal 可直接前台跑;要把 Clowder AI feature worktree 的标准服务交给operator跨回合查看时,猫的 invocation/`-p` 会话必须用仓库的 managed launcher:
`pnpm --dir /absolute/cat-cafe-feature-worktree preview:process start --port PORT --cwd /absolute/cat-cafe-feature-worktree -- env -u NODE_ENV -u npm_config_production -u NPM_CONFIG_PRODUCTION CAT_CAFE_WORKSPACE_ROOT=/absolute/cat-cafe CAT_CAFE_RUNTIME_ROOT=/absolute/cat-cafe-feature-worktree pnpm dev:direct`
outer `--dir` 与 inner `--cwd` 必须解析为同一个物理 worktree。不要用 tool 的 per-call workdir 隐式代替这个坐标:provider hook 可能只看到 turn cwd。outer form 只识别这条受约束的 `dev:direct` child;它不是任意 `COMMAND` 的通行证。
自定义 child 或外部项目的 **start** 仍用 ordinary form:从实际包含 `preview:process` package script 的 Clowder AI checkout 作为 tool cwd,执行 `pnpm preview:process start --port PORT --cwd /absolute/project/path -- COMMAND [ARGS...]`,不加 outer `--dir`。这条启动命令走 ordinary policy,不获得 typed worktree authority。后续 status/stop 按目标 `--cwd` 的身份选 grammar,而不是照抄 start:目标是 `cat-cafe-*` worktree 时必须用 bounded outer `--dir`;其他项目目标才继续用 ordinary form。
macOS 上它会把目标直接注册为一次性 user LaunchAgent;普通 detached child、`nohup`、`setsid` 或 PTY 都不算独立托管。托管预览默认 8 小时自动到期(可用 `--lifetime-seconds N` 缩短,最长 24 小时),避免遗留 watcher 持续制造文件事件。
已合入代码的标准 Alpha 可从实际 main checkout 托管:`pnpm --dir /absolute/cat-cafe preview:process start --cwd /absolute/cat-cafe --port 3011 -- pnpm alpha:start -- --no-quick`。`--no-quick` 可省略;start/status/stop 共用相同 package/target cwd 和 3011。允许选项与隔离端口只由 `scripts/lib/alpha-launch-contract.mjs` 决定;named Alpha 的 instance/ports 继续由 `alpha-coordinates.mjs` 验证,不传任意 child/env/path。Alpha 启动合同和 browser VerificationPlan 分别负责启动与验证。
2. **Hub 自动检测端口** → 弹出 toast 提示"检测到 localhost:xxxx 启动"
3. **点击 Open Preview** → 自动打开 browser panel 并加载页面
4. **也可以手动**:切到 workspace 的 Browser tab,输入 `localhost:port` 按 Go
改代码 → HMR 热更新 → browser panel 内页面自动刷新,无需手动操作。
### 猫主动打开浏览器(Phase C — 必须掌握)
operator说过:"别手动让我输入,你最好打开浏览器,把页面放出来。"
**猫应该主动替operator打开浏览器**,不要等operator点 toast 或手动输 URL。
#### 调用步骤(按顺序执行,不要跳步)
```
Step 1: 确认目标服务器在跑
若由猫启动,先用 managed launcher 启动/查状态:
pnpm --dir /absolute/cat-cafe-feature-worktree preview:process start --port PORT --cwd /absolute/cat-cafe-feature-worktree -- env -u NODE_ENV -u npm_config_production -u NPM_CONFIG_PRODUCTION CAT_CAFE_WORKSPACE_ROOT=/absolute/cat-cafe CAT_CAFE_RUNTIME_ROOT=/absolute/cat-cafe-feature-worktree pnpm dev:direct
pnpm --dir /absolute/cat-cafe-feature-worktree preview:process status --port PORT --cwd /absolute/cat-cafe-feature-worktree --json
自定义 child(tool cwd 必须是含 preview:process script 的 Clowder AI checkout):
pnpm preview:process start --port PORT --cwd /absolute/cat-cafe-feature-worktree -- COMMAND [ARGS...]
pnpm --dir /absolute/cat-cafe-feature-worktree preview:process status --port PORT --cwd /absolute/cat-cafe-feature-worktree --json
外部项目目标(非 cat-cafe-* worktree)才用 ordinary status:
pnpm preview:process status --port PORT --cwd /absolute/external-project --json
→ 只有 status=running 才进入下步;unavailable/unmanaged/stopped 必须如实报告
→ macOS 跨回合展示还必须有 origin=launchd;origin=detached 只证明 launcher 已退出,
没证明脱离 invocation supervisor,不得承诺“回复后还会活着”
curl -s -o /dev/null -w "%{http_code}" http://localhost:PORT
→ 200/301/304 = 可以继续
→ 000/connection refused = 服务器没起来,先启动再说
Step 2: 调用 typed MCP
cat_cafe_preview_open({
port: PORT,
path: "/",
worktreeId: "当前 worktreeId(有就传)",
threadId: "当前 threadId(有就传)"
})
Step 3: 读返回的 deliveryStatus,再决定怎么报告
→ applied = Hub 前端真实接收并应用了(面板已打开)——只有这时才能说"已打开"
→ queued = 目标 thread 不在前台,已写入其 ThreadState;切到该 thread 自动揭示
→ blocked = presentation lock 等阻止了展示(看 deliveryReason)
→ unconfirmed = 没有任何 Hub 客户端确认送达(没连接 / 无匹配 client)——必须如实说"未能确认打开"
```
> **admission ≠ visible**:`allowed: true` 只证明服务端受理了请求。报告"已打开"之前必须看到 `deliveryStatus: "applied"`。
> **running ≠ durable**:同一 invocation 内的 `status: "running"` 只证明当前可达。macOS 只有 `origin: "launchd"` 才证明已经交给用户会话级服务管理;“确实跨回合存活”必须由后续 invocation 的 status/HTTP 探针或operator现场画面确认。
> **durable ≠ immortal**:`status --json` 的 `expiresAt` 是硬截止时间。展示提前结束就显式 `stop`;确需延长时,先停掉旧实例再重新 `start`,不要绕过租约另开后台 watcher。
#### 工具参数
| 参数 | 必填 | 说明 |
|------|------|------|
| `port` | **是** | dev server 端口号 |
| `path` | 否 | 页面路径,默认 `/` |
| `threadId` | invocation 免传 / agent-key **必传** | invocation 调用由服务端从 invocation 记录推导;持久 agent-key 必须显式传,缺失直接报错。传了保证精确送达到该 thread |
| `worktreeId` | 建议传 | 精确到 worktree;不传走 user-scope 送达,同用户其他 tab 会按 thread 归属 queue |
> **怎么获取 worktreeId**:就是你当前工作的 worktree 目录名。例如你在 `cat-cafe-f120-fix` 目录里工作,worktreeId 就是 `cat-cafe-f120-fix`。如果你在主仓库 `cat-cafe` 里,就不需要传。
#### 常见错误
| 现象 | 原因 | 修法 |
|------|------|------|
| 右侧无反应 | 目标服务器没在跑 / 未认证或 thread 归属不对 / MCP callback 未配置 | 先 `curl localhost:PORT` 确认目标服务,再读工具返回的 `deliveryStatus` / 错误(401=未认证,400/403=thread scope) |
| `{"error":"Proxy error","message":"socket hang up"}` | 目标服务器已退出 | 重启服务器,再刷新 Browser panel |
| 猫回复后页面立刻 stopped | 服务仍在 invocation 的 PTY/进程监督域;`detached`/PPID=1 也可能被 supervisor 按 coalition 回收 | macOS 用 managed launcher 并确认 `status=running, origin=launchd`;`cat-cafe-*` 目标的 status/stop 始终带相同 bounded outer `--dir` 与 inner `--cwd` |
| 打开了系统 Chrome | 用了 Playwright/Chrome MCP 等外部工具 | **不要用外部浏览器工具!** auto-open 是 Hub 内嵌预览,不是系统浏览器 |
| 两个重复 tab | React Strict Mode(已修复) | 升级到最新代码 |
- 适用场景:写完前端代码后、operator说"看看效果"、需要展示复杂页面
- ⚠️ **不要传 `html` 参数**(后端不支持);简单 HTML 可视化用 `html_widget` rich block
### 两层可视化策略
operator拍板:"简单的用富文本,复杂的用猫主动打开浏览器。"
| 场景 | 方式 | 怎么做 |
|------|------|--------|
| 简单可视化(图表、动画、计算器) | `html_widget` rich block 内联渲染 | 用 `rich-messaging` skill 发 `html_widget` block |
| 复杂应用(完整页面、多组件交互) | 猫主动打开浏览器 | 调用 `auto-open` API |
## 技术要点(猫猫需要知道的)
| 项目 | 说明 |
|------|------|
| **Preview Gateway** | 独立端口(默认 4100),反向代理 localhost 应用 |
| **为什么不直连** | iframe 跨端口需要代理剥离 X-Frame-Options/CSP |
| **iframe sandbox** | `allow-scripts allow-forms allow-popups allow-downloads allow-same-origin`(安全:独立 origin) |
| **WebSocket/HMR** | 代理层支持 WebSocket 升级,Vite/Next/Webpack HMR 正常工作 |
| **端口排除** | Clowder AI 自身端口(3003/3004/6398/6399/18888 等)自动排除 |
| **审计** | 每次 open/close/navigate 都有审计日志 |
| **Console 面板** | bridge script 注入到 iframe,捕获 console.log/warn/error,在面板展示 |
| **一键截图** | SVG foreignObject + canvas 截图,上传后端,toast 展示 |
| **送达契约** | 认证 + exact-thread:anonymous → 401;invocation 推导 thread;agent-key 必传 threadId 且校验归属。事件只发射一次到 caller 的 user room(tenant scope,无 preview:global/worktree 广播),回执同房间收集 |
| **多 Tab** | 同时预览多个 localhost 页面,Tab 切换独立状态;同一事件多 tab 各自回执,服务端聚合取最优(applied > blocked > queued,skipped 不参评) |
| **进程来源** | `preview:process status --json` 返回 `origin`。macOS 的 `launchd` 是跨 invocation 托管;其他平台的 `detached` 只保证 launcher 退出后继续运行,宿主 supervisor 是否回收仍需外部 service manager 证明 |
| **生命周期** | 每个 managed preview 都必须返回 `expiresAt`;默认 8 小时、最长 24 小时,到期后 launcher 会 TERM→KILL 自己拥有的进程组。长期展示要显式续开,不允许无限 watcher |
## 什么时候主动用
- 写完前端组件/页面 → **主动调 auto-open 打开浏览器展示**(不要等operator点)
- 调样式/布局 → 改代码后在 browser panel 里实时查看
- operator说"看看效果"/"给我看看" → 主动打开 browser panel 展示
- dev server 已在 Terminal 跑着 → 主动打开浏览器,不要只提示
- invocation/`-p` 中启动的 dev server → 必须走 `preview:process`,并在报告中同时给出 status 与 origin;不能把 `running/detached` 写成跨回合已存活
- 跨回合展示 → 同时检查 `expiresAt`;status/stop 按目标 cwd 身份选择,不按启动命令族选择:`cat-cafe-*` worktree 一律使用相同 bounded outer `--dir` + inner `--cwd` + port,非 worktree 项目才使用 ordinary form
- 简单可视化(图表/动画) → 用 `html_widget` rich block 内联渲染
- Console 有报错 → browser panel 下方 Console 面板自动展开,可以看
- 需要截图 → browser panel 工具栏一键截图;默认先存到 `${TMPDIR}/cat-cafe-evidence/...`,不要落仓库根目录(见 `../.cat-cafe-shared-refs/evidence-output-contract.md`)
## 不要做的事
- **不要跳过 Step 1(验证服务器)直接调 `cat_cafe_preview_open`** — 服务器没跑 = proxy error
- **不要用普通后台 shell/PTY、`nohup`、`setsid` 或普通 detached child 冒充长期托管** — PPID=1 仍可能被 invocation supervisor 按进程族回收
- **不要给 self-identifying `pnpm --dir … preview:process` 拼 global pnpm options、wrapper 或任意 child** — native guard 只承认 exact package/target 坐标与受约束的 Clowder AI `dev:direct` child;自定义 child 用 Step 1 已列的 ordinary form,不改写或包装 bounded form 绕过
- **不要用 `launchctl submit` 包裹会立即退出的 launcher** — inferred keepalive 会反复重启 launcher 形成自旋;macOS 直接用 `preview:process` 生成的一次性 LaunchAgent
- **不要用 Playwright / Chrome MCP / `open` 命令打开系统浏览器** — F120 是 Hub 内嵌预览,走 iframe,不走系统浏览器
- **不要手写 `/api/preview/auto-open` 的 `curl`** — 主路径是 `cat_cafe_preview_open`
- 不要手动去构造 gateway URL(让 Hub 前端处理)
- 不要尝试预览外部 URL(只支持 localhost)
- 不要预览 Clowder AI 自身服务端口(会被端口验证拦截)
- 不要把临时截图顺手留在仓库根目录;要入库时再显式归档到正式目录
## 和其他 skill 的区别
| Skill | 关注点 |
|-------|--------|
| **browser-preview(本 skill)** | Hub 内预览 localhost 前端页面 |
| `tdd` | 写代码的测试驱动纪律 |
| `quality-gate` | 开发完成后的自检(含对照设计稿) |
View on GitHub