Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .scratch/embedded-browser/PRD.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ PiGUI 目前对 dev server 零认知(project registry 只有 `{id, path, display

### 5. 元素标识与载荷形状(定义在 `packages/core`)

> 修订提示(2026-10-10):本节「不新增结构化附件」「落到 composer 草稿」与 `formatBrowserAnnotationPrompt` 已被标注 v2 取代,见 `.scratch/browser-annotation-v2/PRD.md` 决策 4 与 ADR-0029「修订:标注 v2」。

```ts
type BrowserAnnotationElement = {
index: number; // 标记序号,与截图上的数字一致
Expand Down
4 changes: 4 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,10 @@ _Avoid_: Panel, tab, view, plugin, widget
Pace 自带的 Surface:Changes、Files、Terminal、Browser。它们与将来扩展注册的 Surface 走同一套注册表与 Rail,只是 provider 为 `builtin`。Files 是只读的 checkout 目录树加文件预览(ADR-0035)。
_Avoid_: Core panel, native panel, first-party plugin

**Browser Comment**:
用户在 Browser Surface 的标注模式里对页面一个元素或一块区域写下的评论,保存才成立(标注 v2,ADR-0029「修订:标注 v2」)。由主进程按 Session 持有——不属于 tab 或文档,带保存时的 URL、视口与局部截图;同 URL 的新文档会重新显示它的标记,找不到元素时标为 stale。它在 composer 附件抽屉里以 `#n` 芯片列出,随下一条消息作为评论块与截图发给 Pi,发送成功即被消费。
_Avoid_: Annotation(仅指页上标记的实现层叫法), marker, note, design comment

**Session Projection**:
Pace 自己保存的查询模型,用来支撑 Session 列表、Analyze、状态索引、成本聚合、checkout 生命周期、恢复入口和 UI 快速渲染。它是从 Pi Session State、Session Trajectory 和 Pace checkout 管理事件同步出来的投影,不是 Pi 会话内容的权威来源。渲染层每个 Session 只保留一份,由一个 module 拥有并作为唯一写者(ADR-0044)。
_Avoid_: Runtime truth, independent chat state, source of record
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src/pages/design-components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2801,7 +2801,7 @@ function ComposerAttachmentDrawerGallery() {
/>
</div>
</Variant>
<Variant caption="with comment chips — one stale">
<Variant caption="with comment chips — one stale, one area">
<div className="w-96">
<ChatPromptInput
drawer={
Expand All @@ -2814,6 +2814,7 @@ function ComposerAttachmentDrawerGallery() {
warning:
"No longer on the page — sent as it was when saved",
},
{ id: "c3", label: "#3 Area · Hero spacing is uneven" },
]}
commentsLabel="Browser comments"
items={[
Expand Down
36 changes: 35 additions & 1 deletion docs/adr/0029-embedded-browser-surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

- 状态:Accepted
- 日期:2026-09-03
- 来源:`.scratch/embedded-browser/PRD.md`(决策 1–6、Spike 结论、S2 / S3 实现约束);PR #149(S0 + S1)、#153(S2)、#154(S3);issue #86
- 来源:`.scratch/embedded-browser/PRD.md`(决策 1–6、Spike 结论、S2 / S3 实现约束);PR #149(S0 + S1)、#153(S2)、#154(S3);issue #86;v2 修订:`.scratch/browser-annotation-v2/PRD.md`、PR #451 / #452 / #454 / #455(issue #446–#450)

## 背景

Expand Down Expand Up @@ -63,6 +63,30 @@ ADR-0013 把「内嵌浏览器 + DOM 标注」定为切换 Electron 外壳的承
- `WebContentsView` 设 `backgroundColor` 为面板底色,避免 macOS `transparent + vibrancy` 被开洞;bounds 永不覆盖 40px 表头带。
- 每个 tab 的导航独立递增 navigationId;截图握手绑定原页面,关闭或导航后返回的旧截图作废。渲染层在切 tab、切 Session 或关闭 tab 后丢弃仍在途的 Send to composer,避免把旧页面截图送进新上下文。

## 修订:标注 v2(2026-10-10)

本节取代上文「标注层」与「载荷回传」中被点名的 v1 内容(「只落草稿」、整视口截图握手、composer 注入路径等);v1 原文保留为历史,不重写。

- **推翻「只落草稿」**:评论以结构化附件进入 composer 附件抽屉「Browser comments」分组(Token chip:`#n 首行摘要`,区域评论为 `#n Area · 摘要`,stale 用黄色并带说明,移除即删除);`To composer` 按钮、`browser_capture_annotation` 整视口截图握手、`injectIntoComposer` / `composer-injections` 注入路径均已移除。发送时 composer 在用户草稿后追加 core `formatBrowserComments` 产出的评论块(替代 `formatBrowserAnnotationPrompt`);评论截图按评论顺序排在消息图片最后、用户自己附的图片在前,Pi 收到的仍是 `string + RuntimePromptImage[]`,Gateway / Pi 零改动。`send_prompt` / 排队 follow-up / 从队列 steer 三条路径天然覆盖——评论在入队时已烤进消息。提交成功后按构建时快照的 id 列表调 `browser_consume_comments`;发送期间新存的评论保留,失败则全部保留。
- **评论仓归主进程、Session 级**:`browser-host.ts` 每 Session 持一个评论仓,仓内顺序即 `#n`(读时重算);每条 `BrowserComment` = 标注字段 + `tabId` / `url` / `title` / `viewport` / `stale` / `hasImage` / `createdAt`。上限 200 条(`maxSessionComments`),超出拒收并发 `comment-rejected`。新评论保存时 overlay 整层隐藏两帧再上报,主进程随即 `capturePage` 裁局部截图:标记矩形外扩 48 CSS px、最小 320×200、先平移再夹进视口;图片不进状态广播,按 id 由 `browser_comment_images` 取。关闭 tab 只移除页上标记,评论仍在仓里;关窗/退出即丢,不落盘。命令:`browser_list_comments`、`browser_settle_comments`(等在途截图落定再读,发送用它)、`browser_comment_images`、`browser_delete_comment`、`browser_consume_comments`;`browser_clear_annotations` 只清当前 tab 的评论。事件:`comments-changed`(带 revision,旧答复丢弃)、`comment-rejected`、`submit-requested`。composer 订阅读取,不持有第二份副本。
- **跨文档保留**:新文档 `ready` 与 `did-navigate-in-page`(SPA 路由切换)时,主进程下发 `sync-annotations`:本 tab 中 URL 键(origin + pathname + search,忽略 hash)相同的评论。overlay 用 selector 唯一解析元素,解析不到时用 MutationObserver 再等最多 5s,仍失败上报 `stale`;stale 评论仍可发送(文本与保存时的截图仍有效)。页上只显示当前 URL 的标记,其他页面的评论只在 composer 里。
- **覆盖层交互**:点击即打开已聚焦的草稿编辑器(textarea),评论必填,保存才成为标注;点已保存的元素或徽章进入编辑(带 Delete),不产生重复;一次只有一个编辑器,有未保存内容时点别处编辑器抖动而不切走。目标收敛:跳过行内装饰节点、SVG 内部与小于 8×8 的元素,优先最近的交互控件;`↑` / `↓` 沿祖先链手动微调。悬停标签 `tag.firstClass · W×H`(有 source 时加 `file:line`)。已保存标注画细轮廓 + 外侧右上角编号徽章,相邻徽章重叠时错开。模式可感知:视口内侧 2px 主题色描边 + 顶部居中标签「Click or drag to annotate · Esc to exit」。颜色取渲染层语义 token,随 `set-design-mode` / `set-annotation-palette` 下发,overlay 用 `CSS.supports` 逐个校验、不合法的回落内置默认。工具条按钮名 `Annotate`。
- **区域框选**:标注模式下按住拖动超过 4px 进入框选,松开打开编辑器;任一边小于 4px 视为误触;Esc / pointercancel / 窗口外松开取消并吞掉随后的 click;不拦截滚动。`BrowserAnnotationElement.area`(相对锚点边框盒左上角的偏移,CSS px)存在即区域评论,`rect` 为保存时区域的视口矩形;锚点取按下点祖先链上第一个完整包含矩形且非 SVG 内部的元素,兜底 `body`;区域不带 `text`、不占有锚点(点锚点仍新建元素评论,多个区域可共享锚点);已保存区域画虚线轮廓。格式化无论有无截图都输出 `- area: W×H at (x, y)`。
- **键位**:

| 位置 | 键 | 行为 |
|---|---|---|
| 编辑器内 | `Enter` | 保存并继续标注 |
| 编辑器内 | `Shift+Enter` | 换行 |
| 编辑器内 | `Cmd/Ctrl+Enter` | 保存并立即发送 composer |
| 编辑器内 | `Esc` | 取消编辑(拖动中则取消框选) |
| 标注模式、编辑器未开 | `Cmd/Ctrl+Enter` | 立即发送(有 pending 评论时) |
| 标注模式、编辑器未开 | `Esc` | 退出标注模式 |
| 标注模式、编辑器未开 | `↑` / `↓` | 悬停目标切到父元素 / 回到子元素 |
| 任意处 | `Cmd/Ctrl+Shift+A` | 切换标注模式(页内由 overlay 监听,Pace 窗口内由 Browser 面板监听) |

与 Codex 相反(Codex 的 Enter 直发),评论框的 Enter 是保存到待发区,不与 composer 的 Enter 发送冲突。页内 `Cmd/Ctrl+Enter` 路径:overlay 在 `annotation-saved` 之后发 `submit-requested`,主进程等本 Session 在途截图落定后经浏览器事件通知渲染层,composer 自行响应——Browser 面板关着也能发。

## 已知限制

宿主:
Expand Down Expand Up @@ -93,6 +117,14 @@ ADR-0013 把「内嵌浏览器 + DOM 标注」定为切换 Electron 外壳的承
- 页面标题用于 tab 提示;实例名保持稳定的 Browser 序号。
- 注入只到达当前 Session 已挂载的 composer,不排队;注入文本作为 follow-up 草稿持久化,截图不持久化,重载后只剩文本。

v2 修订后:

- v1 的「无单条删除;清空后序号从 1 重开……页面文档替换或关闭 tab 才丢弃对应标注」、「握手 500ms 超时」「滚出视口的标记不在图上」「注入只到达已挂载 composer」各条已由 v2 取代。
- selector 重定位依赖页面结构稳定:`nth-of-type` 链在列表重排后可能命中错误元素——stale 只能识别「找不到」,识别不了「找错」。
- 评论多时图片数随之增长、占用上下文;本轮不设图片数上限(仓上限仍为 200 条)。
- 评论不跨应用重启保留(与图片附件不落盘的策略一致)。
- 区域的截图不画框,边界只在文本里(`- area:` 行)。

## 结果

- ADR-0028「Terminal / File / Browser surface 仍受 ADR-0007 冻结」修订为:Terminal 由 PR #147 解冻、Browser 由本文解冻、File 仍冻结。
Expand All @@ -105,3 +137,5 @@ ADR-0013 把「内嵌浏览器 + DOM 标注」定为切换 Electron 外壳的承
- 渲染层组件测试覆盖 surface 五态、URL 按 Project 记忆、design mode 工具条、Project tab 组迁移与恢复、跨 tab 状态隔离、过期截图丢弃及注入口。
- Electron E2E 追加两 tab 的打开、切换、标注隔离、徽标、重载恢复与全部关闭空态。
- Electron E2E(`e2e/smoke/browser-surface.spec.ts`):bounds 跟随面板、弹层换快照、`ERR_ABORTED` 不算失败、严格 CSP 页上 design mode 可用且页面读不到覆盖层、标注 → Send → composer 草稿出现模板文本与 PNG 附件。
- 标注 v2 Vitest 覆盖 overlay 交互与区域框选(jsdom)、主进程评论仓与消费竞态、core `formatBrowserComments` 模板、composer 抽屉与提交路径。
- 标注 v2 Electron E2E(同 `browser-surface.spec.ts`):严格 CSP 页经 Annotate 保存两条评论、重载文档后标记随评论仓恢复;composer 出现「Browser comments」芯片;页内 Cmd/Ctrl+Enter 发出,断言消息带评论块与两张按评论顺序的截图、发送后芯片与标记被消费。另有 tab 用例覆盖标记按 tab 隔离、切回恢复与芯片随 Session 出现。
2 changes: 1 addition & 1 deletion docs/design/chat.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ Branch / Location chip 都截断在 16rem 以内,44rem 宽下长分支名不
- `ChatQueuedMessage`:队列里的一条;`presence: "none" | "enter" | "exit"` 由 `usePresenceList` 给,不要自己传 `"enter"`。只有 `pending` 的整卡 `draggable`;拖动中 `isDragging`(45% 透明),目标位 `dropTarget: "before" | "after"`(顶/底边 accent 线,由指针落在卡片上半或下半决定)。`isWithdrawn` 显示 "Withdrawn",`isSteered` 显示 "Steered";两者都是终态,无动作、不可拖。重排 RPC 进行中卡片不可拖、drop 忽略。Pi follow-up mode 为 `all` 时整卡不可拖、drop 忽略。
- `ModelSelectorControl`:选中项来自 projection;冷会话缺少目录时异步读取 `list_available_model_controls`,不启动 Agent,读取失败不阻塞历史或发送。Session 创建期间 projection 还没有自己的 controls,此时选中项回落到 draft 提交时写入的 `last model selection`,目录则复用本次 renderer 已读到的那份,选择器因此在 Draft → Live 交接中不会消失;创建期 `isDisabled`。真正切换模型会准备运行环境。`isDisabled` 在队列模式或提交等待期间为 true;`visibleModels` 空数组 = 全显。当前选中模型即使被隐藏也保留并标注。没有第二个模型选择器,失败卡里的 `modelControl` 插槽也用它。
- `ComposerInsertMenu`:`catalogs` 是通用分组目录(`ComposerInsertCatalog[]`:`id` / `label` / `icon` / `searchLabel` / `emptyText` / `items`),一级菜单先是 Add files,再按传入顺序列出非空组(空组隐藏),点组打开 `CommandPalette` 按 label/description 搜索,选中经 `onPick(catalogId, itemId)` 回给调用方。菜单本身不携带任何领域概念——skills/prompts/extension commands 的分组和 token 插入由 `entities/prompt-command` 的 `insertCatalogs` / `commandToken` 组装,提交前的校验走 `validateCommandSubmit`(拦 Pi TUI 内置命令和排队模式下的 extension 命令)。`/` 补全的数据来自 `list_prompt_commands`(`usePromptCommands`:草稿按项目根静态解析,会话内走运行时 catalog)。
- `ComposerAttachmentDrawer`:`items` 与 `comments` 都为空返回 null;图片走 Thumbnail,文本文件与 `comments`(结构化批注,如 Browser comments)各走一行 Token,stale 批注用 `warning` 上黄色(`color="yellow"` + `description`)。附件逻辑(大小上限、拒收文案、拼进 prompt,含 `appendix` 追加块)全在 `composer-attachment-logic.ts`,从 `composer-attachments/index.ts` 导入,不在页面里重算。
- `ComposerAttachmentDrawer`:`items` 与 `comments` 都为空返回 null;图片走 Thumbnail,文本文件与 `comments`(结构化批注,如 Browser comments)各走一行 Token,stale 批注用 `warning` 上黄色(`color="yellow"` + `description`)。Browser comments 芯片标签为 `#n 摘要`,区域评论为 `#n Area · 摘要`。附件逻辑(大小上限、拒收文案、拼进 prompt,含 `appendix` 追加块)全在 `composer-attachment-logic.ts`,从 `composer-attachments/index.ts` 导入,不在页面里重算。

`ComposerInsertCatalog` 可提供静态 `items` 或异步 `search(query)`。静态空组隐藏,异步组始终可见,打开后按输入检索。搜索框阻止点击冒泡,避免 portal 内点击被 Composer 的空白处点击逻辑抢走焦点。Design 页包含异步文件搜索示例。

Expand Down
2 changes: 1 addition & 1 deletion docs/design/workspace.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@

## BrowserSurface

复合件:根只持 `state`,经 context 把状态交给子件。`state.kind` 为 `"narrow" | "unsupported" | "empty" | "live" | "error"`;`empty` 可用 `phase: "idle" | "initializing" | "opening" | "blank"`(无 phase 即 idle)。只有 `live` 渲染 viewport 占位(其 rect 驱动原生 `WebContentsView`);`narrow` / `unsupported` 不画 chrome;零 tab 的 empty 显示 Astryx `EmptyState`。页面组合 `Tabs`(实例条)+ `Toolbar`(地址/导航/Annotate 标注模式)+ `Viewport`(占位、快照、notice、空态)。弹层出现时把 `snapshot` 传给 Viewport。
复合件:根只持 `state`,经 context 把状态交给子件。`state.kind` 为 `"narrow" | "unsupported" | "empty" | "live" | "error"`;`empty` 可用 `phase: "idle" | "initializing" | "opening" | "blank"`(无 phase 即 idle)。只有 `live` 渲染 viewport 占位(其 rect 驱动原生 `WebContentsView`);`narrow` / `unsupported` 不画 chrome;零 tab 的 empty 显示 Astryx `EmptyState`。页面组合 `Tabs`(实例条)+ `Toolbar`(地址/导航/Annotate 标注模式)+ `Viewport`(占位、快照、notice、空态)。弹层出现时把 `snapshot` 传给 Viewport。页内覆盖层支持点击元素写评论与拖动框选区域(超过 4px 生效);覆盖层活在页面的 closed Shadow DOM 里,不是 `shared/ui` 组件。

```tsx
<BrowserSurface state={state}>
Expand Down
Loading
Loading