# Browser Preview

> Hub 内嵌浏览器预览 localhost 应用。 Use when: 写前端代码、跑 dev server、需要看页面效果、调 UI、operator说"看看效果"。 Not for: 后端纯 API 开发、不涉及页面的工作。 Output: 前端页面在 Hub browser panel 中实时预览。

- Skill: `zts212653/browser-preview` (Agent Skill)
- Install (CLI): `npx skillmds@latest add zts212653/browser-preview`
- Raw SKILL.md: https://api.skillmd.com/api/skills/zts212653/browser-preview/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: zts212653 (https://skillmd.com/u/zts212653)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/zts212653/browser-preview

---


# Browser Preview

Hub 内置了嵌入式浏览器面板（F120），可以直接预览运行中的 localhost 应用。猫猫写完前端代码不用让operator切浏览器看效果。

## 工作流

### 基础流程（端口发现 → 预览）
1. **启动 dev server**：交互 Terminal 可直接前台跑；要把页面交给operator跨回合查看时，猫的 invocation/`-p` 会话必须用仓库的 managed launcher：
   `pnpm preview:process start --port PORT --cwd /absolute/project/path -- COMMAND [ARGS...]`
   macOS 上它会把目标直接注册为一次性 user LaunchAgent；普通 detached child、`nohup`、`setsid` 或 PTY 都不算独立托管。托管预览默认 8 小时自动到期（可用 `--lifetime-seconds N` 缩短，最长 24 小时），避免遗留 watcher 持续制造文件事件。
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 preview:process start --port PORT --cwd /absolute/project/path -- COMMAND [ARGS...]
  pnpm preview:process status --port PORT --cwd /absolute/project/path --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 用 `pnpm preview:process start ...` 并确认 `status=running, origin=launchd`；结束展示时用同一 cwd/port 执行 `stop` |
| 打开了系统 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`；任务结束或不再展示时执行同一 cwd/port 的 `preview:process stop`
- 简单可视化（图表/动画） → 用 `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 按进程族回收
- **不要用 `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` | 开发完成后的自检（含对照设计稿） |

