# AGENTS.md — pi-web-ui 项目指南

> 给 AI 编码助手（pi / Claude Code / Cursor 等）看的高层指南，细节按主题在 `docs/`。
> 修改后在 pi 里跑 `/reload`。精简前全文备份：`AGENTS.md.bak`。

## 1. 项目是什么

pi-web-ui 是 pi 编码智能体（`@earendil-works/pi-coding-agent` SDK）的 Web 聊天界面：
浏览器对话、文件树、附件、内置终端（xterm.js + node-pty）、模型管理、声音提醒、中英文切换。
一条命令可跑，可 Docker / systemd / launchd / Windows 计划任务部署。
另有 **Electron 桌面壳**（`desktop/`，随机空闲口起同一 server，网页版零改动；见 `desktop/README.md`）。

- 仓库：`git@github.com:xing-shuyin/pi-web-ui.git`；npm 包 `pi-web-ui`
- Node **>= 22.19.0**；版本 `package.json` 与 `package-lock.json` 两处同步

## 2. 技术栈

| 层     | 技术                                                                   |
| ------ | ---------------------------------------------------------------------- |
| 后端   | Node + Express（静态 + `/api/health`）+ `ws`（`/ws`）                  |
| 前端   | React 19 + Vite 8 + react-markdown + highlight.js + xterm.js           |
| 智能体 | `@earendil-works/pi-coding-agent` SDK（进程内，读 `~/.pi/agent` 配置） |
| 终端   | node-pty（服务端 PTY）+ `@xterm/xterm`（经 terminal bridge 转发）      |
| 样式   | 单文件 `web/src/styles.css`（CSS 变量主题，默认深色）                  |

## 3. 目录结构

完整注释版目录树见 `docs/directory-reference.md`（扁平速览）。

顶层：`server/` 后端（ESM，编译到 `dist/server/`）· `web/` 前端（编译到 `web/dist/`，gitignore 但进 npm 包）·
`bin/pi-web-ui.mjs`（CLI）· `desktop/`（Electron 壳）· `deploy/`（launchd/systemd/Windows 任务示例）·
`themes/`（完整样式表主题）· `plugins/`（官方插件，各自 README；`catalog.json` = 市场内置列表，加插件在此加一条 + PR；`page-picker/extension` 是浏览器扩展不是插件）·
`extensions/webui.ts`（pi 扩展 `/webui`）· `dev/`（本地辅助，不进包）· `docs/`（见 §4 表）· `tests/`（见下）。

server 关键文件（★=单源/入口，必读；其余按名读源码，不在此逐条注释）：
`protocol.ts`★（wire 协议唯一事实源）· `index.ts`（express + `/ws` 分发 + 心跳 + `pending` 重放队列）·
`agent-service.ts`（ClientSession/多对话并发/工具注册 customTools）· `serialize.ts`（SDK→UiMessage，`details` ≤64KB 下发）·
`tool-manager.ts`★（`AGENT_TOOL_CATALOG`，工具开关唯一事实源）· `tool-approval.ts` + `approval-rules.ts`（审批门禁 + 规则库）· `tool-prompts.ts`（bash 工具提示词唯一事实源：description/snippet/guidelines/参数 schema 三路径共用）·
`client-state.ts`（`client-state.json` 持久化：locale/workspaceRoots/softCap…）· `composer-drafts.ts`（草稿中心）·
`plugins.ts`（`UI_SLOT_ALIASES`/`UI_SLOTS` 别名+枚举）· `plugin-catalog.ts`/`plugin-catalog-sync.ts`/`plugin-installer.ts`（市场目录同步+后台安装）·
`plugin-grants.ts`（目录授权）· `plugin-project.ts`（host.project.create）·
`subagents.ts` + `subagent-templates.ts`（第一方子代理+模板库）·
`attachments.ts`（只发路径引用，不注入内容）· `files-service.ts` + `text-sniff.ts`（`decodeText`/`countLines`）·
`vision-bridge.ts`（纯文本模型看图转写）· `present-files-tool.ts`（只读探测+摘录预算）·
`terminals.ts`（PTY 管理；`terminalBash` 开关分流）· `scm.ts`（只读 git，execFile 直跑）·
`model-admin.ts`（models.json+多密钥）· `patch-remote-catalog.ts`（模型目录整表替换补丁）·
`soft-cap.ts`（压缩软上限纯函数）· `i18n.ts`（服务端语言：`resolveServerLang`/`pick`/`bilingual`/`getServerBlock`）·
`control-socket.ts`（本地 status/quiesce）· `launch-origin.ts`（托管探测）· `webui-context.ts`（扩展 UI 桥）·
`settings-service.ts`/`goal-service.ts`/`slash-commands.ts`/`themes.ts`/`uploads.ts`/`bg-servers.ts`/`queue-utils.ts`/`process-utils.ts`/`tool-info.ts`/`conversation-read-tool.ts`/`compact-context-tool.ts`/`edit-soft-tool.ts`/`read-tool.ts`/`ensure-bash.ts`/`patch-node-pty.ts`。
`server/dsh/`：`preset-clones.ts`（file: 克隆）+ `runtime/{launcher,goal-rpc,custom-prompt}.mjs` + `dsh-usage.ts`（统计映射）。

web/src 关键（★=先读这三个）：`use-chat.ts`★（WS+reducer+终端 bridge）· `app-globals.ts`★（模块级 store + `appSend`；快照数据禁入）·
`types.ts`（`export type * from "../../server/protocol"` shim，由 `scripts/check-protocol-sync.mjs` 守护）·
`ui-slots.ts`★（宿主 UI 扩展点：四层合并+22 slot）· `slot-toolbar.tsx`（各组件 `*HostNodes`+`renderMergedToolbar`）·
`base-url.ts`（`appUrl()`，见 §9）· `i18n.tsx`（文案只许字符串字面量，见 §9）· `styles.css`（全部样式）·
`notify.ts`（`isCollapsedWindow`/`shouldSuppressNotify`）· `composer-bridge.ts` + `composer-draft.ts`（`advanceComposerSession`/`focusComposer`）·
`topbar-fit.ts`（实测宽度溢出）· `use-floating-panel.ts`（浮层统一 Hook）· `pending-question.ts`（问卷恢复）·
`present-items.ts`/`present-settings.ts`/`file-preview-bridge.ts`（present_files 卡片）· `tool-info-state.ts`/`tool-schema.ts`· `plugin-host.ts`（宿主 API 时序）。
components 按文件名自解释，只记特殊机制：`ContextMenu`/`ToolInfoDialog`（portal 到 body + fixed，同因 overflow 裁剪）·
`TopBar`/`FooterBar`（单一扁平流，slot 顺序直排+`hostNodes`）· `SlotTabs`/`PluginPage`（切走即 cleanup 不挂载）·其余读源码。

tests：`run-smoke.mjs`（零 token 冒烟聚合，ALL 列表）· `unit/`（vitest 纯函数单测）· `*-test.mjs`（手写 Playwright/WS；**需 Chrome 的不入 smoke**，手动跑，缺 Chrome 自动 SKIP）· `scratch/`（一次性，gitignore）。
CI：`ci.yml`（协议同步→typecheck→build→vitest→冒烟）· `release-notes.yml` · `desktop-release.yml`。

## 4. 核心架构（每主题一句话）

`docs/` 索引（找详细文档先看这里，均为 `docs/<名>.md`）：**architecture-core** 快照驱动/协议单源/安全边界/主题/多对话并发 · **architecture-attachments** 附件/图片/视觉桥/上传预览下载 · **architecture-terminal** 终端 PTY/SCM/活力检测/bash 接管 · **architecture-plugins** 插件形态/协议/宿主扩展点/MCP 桥/多根工作区 · **architecture-system-prompt** 系统提示词组装链路与 override 钩子 · **tool-context-budget** 工具上下文预算（文案精简 / 逐工具覆盖 / **延迟加载** + 前缀缓存硬约束）· **dsh-engine** DSH 预设/问卷/底栏统计/工具桥 · **goal-conversation-design** 目标模式 2.0（把目标审查变成对话：执行对话干活 + 当前对话当审查者；**已实施且只剩这一条路径** —— v1 的隐藏隔离审查会话 + 自治标记路径已删除，轮次/熔断/代次全在服务端，见该文档 §4/§11）· **development** 开发工作流/CI/编码约定/测试规范 · **release** 发布流程 · **deployment** 部署 · **env-vars** 环境变量全表 · **preset-sharing** 预设导入/导出/社区共享仓库（交换格式 / 白名单净化 / 一键分享 / 浏览列表）· **antigravity-proxy** 反代接入 · **directory-reference** 完整注释版目录树

| 主题                  | 文档                               | 一句话                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| --------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 快照驱动              | `docs/architecture-core.md`        | 服务端唯一事实源，60ms 节流；`snapshot_delta` 增量 + `message_delta` 实时通道；`send()` 背压超阈丢 snapshot（幂等，250ms 重试）；`get_state`/`flushSnapshot` 立即推一次                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| 协议单源              | 同上                               | 只改 `server/protocol.ts`，两端 switch 各加分支；`web/src/types.ts` 是 shim                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| 全局运行态            | 同上                               | 整树共享放 `app-globals.ts`（`useAppField` 单字段订阅，组件不收 `send` prop 用 `appSend`）；快照流数据禁入 store                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| 安全边界              | 同上                               | 默认 loopback；WS Origin/Host 校验；quiesce 准入；provider headers 不下发浏览器                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| 主题                  | 同上                               | styles.css 是共享基线；主题=完整样式表可覆盖一切；终端跟随主题                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| 多对话并发            | 同上                               | 每对话独立 runtime，上限 8/项目（子代理不计）；运行列表口径 listed ∪ 有内容的当前对话；clientId 存 sessionStorage，每标签页独立                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| 附件/预览             | `docs/architecture-attachments.md` | 只给路径引用（`reference`/`lines`，内容不注入）；预览 512KB 上限+嗅探+GBK 回退；媒体走 HTTP Range；下载绕 Safe Browsing                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| 终端/SCM              | `docs/architecture-terminal.md`    | 每 Conversation 一个 TerminalManager；`terminalBash` 开关分流（`persist` 决定一次性/ai-bash 持久）；SCM 只读走 execFile，写操作走可见终端                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| 工具开关/read 目录    | `docs/architecture-core.md`        | `AGENT_TOOL_CATALOG` 唯一事实源；`read` 覆盖目录走 ls 口径（`readDirEnabled` 是行为开关，不入目录；仅 pi 引擎）；`read`/`write`/`edit` 三处覆盖与第三方扩展同名工具的共存（基底 = 扩展实现优先）见 `server/tool-overrides.ts`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| 审批                  | `server/tool-approval.ts`          | 规则库 `<dataDir>/approval-rules.json`（ask/deny/allow）；三档放行：全局关→本对话全部允许→允许同类；记忆只在内存，随过户搬                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| 问卷/草稿进快照       | `docs/architecture-core.md`        | `UiState.pendingQuestion`（刷新恢复）+ `UiState.draft`（只跟全量快照，`draft_update` 自带 sessionId）                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| 临时对话              | `server/agent-service.ts`          | `new_chat {ephemeral:true}`→内存不落盘；`UiState.isEphemeral` 恒存在于 light state；`persist_conversation` 一键转正（id 不变）                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| 插件/UI 扩展点        | `docs/architecture-plugins.md`     | `<dataDir>/plugins/<id>/`；manifest `ui` 与 `host.ui.register()` 同一套别名+枚举；合并「宿主<插件<arrange<用户偏好」；插件不碰 DOM；`PLUGIN_API_VERSION=2`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| 多根工作区            | 同上                               | `set_workspace_roots` 按项目 cwd 存（上限 8）；**额外根=工作区内**，插件读免授权；AI 只在主 cwd 干活                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| 子代理模板            | `server/subagent-templates.ts`     | 全局共享；append/replace + 白名单 + 可选模型/强度（空=跟随）；停用对 AI 不可见                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| DSH                   | `docs/dsh-engine.md`               | 四预设 file: 克隆 mount；问卷/技能钩子挂 agent scope（host 收不到 scoped 事件）；底栏统计吃直播帧+usage（`dsh-usage.ts`）                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| 压缩软上限            | `server/soft-cap.ts`               | C→`reserveTokens=W−C` live 注入；关/非法回填 16384；pi 引擎独有                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| 看门狗                | `docs/architecture-core.md`        | 20 分钟 abort 会话（不碰后台服务）；`ask_user_question` 豁免                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| present_files         | `server/present-files-tool.ts`     | 只读探测+摘录预算走 `details`；前端无 details 也能渲染（参数解析+合并）；远端本地打开报不支持                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| browser_page/对话引用 | `docs/architecture-core.md`        | 闸门在扩展侧；对话引用只发 `<conversation-ref>` aside，模型经 `conversation_read` 按需取                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| 计划模式              | `server/plan-mode.ts`              | **目标条展开行**的 `host:goal-plan` —— **一次性动作按钮，不是开关**：点一下 = 开服务端只规划闸门 + 把目标输入直接当消息发出（`set_plan_mode{true}` 紧跟 `prompt`，同 WS 按序 → 本轮在闸门内跑）；空输入则只开闸门。与提炼/发送同列同口径（「点一下直接发」），`goalModeEnabled=false` 时随目标条一起隐藏。**退出计划的唯一出口 = 计划看板里的「开始实施」**（PlanBoard：`set_plan_mode{false}` + 发实施请求）；开启时通过 `applyToolGating` 动态将写类与旁路工具从活跃集剔除（模型视野内无写工具，物理杜绝倾倒代码发起写调用）；`withPlanModeGate` 硬闸门挂 customTools 整列 + write/edit 覆盖 + 终端 `checkSafety`，非只读 bash 与越权写调用显式抛错拦截（isError 为 true）；提示词追加 `PLAN_MODE_SYSTEM_PROMPT`（切换时 reload，**正文可在设置页「提示词」区改**：append/replace + 自定义，语义同视觉桥/AI 提交信息，不进预设；**禁代码倾倒**——不贴整文件/完整函数，代码只留 ~20 行短片段；**小需求跳过 `plan_update` 走短计划**）；产物走 `plan_update` 看板；DSH 不支持。回归 `plan-mode-test` |
| 审查者模式            | `server/delegate-mode.ts`          | **自动委派（默认关）**：会话级开关（`set_delegate_mode`，设置页「审查」区 + 底栏 `host:status-delegate` 标识）→ 开启后本对话**只审阅不施工**：用户每条 prompt 由服务端转给一个**常驻落盘执行对话**（`dispatchToDelegate`，首轮 prompt 交给 `spawnSubagentConversation` 起步、之后 `sendUserMessage` 追加），主对话这一轮不跑模型；`withDelegationGate` 硬闸门拒写类/非常规 bash/**派发类**（派活是服务端的活）+ `DELEGATION_SYSTEM_PROMPT` 段（切换时 reload）。**计划模式优先**（同开不派活，否则与 plan 拒 spawn 死锁）；DSH 不支持（明确 notice，不静默）。执行对话跑完只通知（不自动验收）。布尔随转录落盘（`delegate/mode`），执行对话 id 只在本进程内有效。回归 `delegate-mode-test`                                                                                                                                                                                                                                                                                                          |

## 5. 开发工作流

> 详见 `docs/development.md`

```bash
npm run dev          # node --watch 后端(:8788) + vite 前端(:5173)
npm run typecheck    # 双端 tsc --noEmit（提交前必跑）
npm run format       # prettier（提交前必跑；只检查用 format:check）
npm run lint         # oxlint（提交前必跑；自动修用 lint:fix）
npm run build        # build:web + build:server
npm start            # 跑 dist/server/index.js（生产）
npm test             # vitest
npm run test:smoke   # 零 token 冒烟
```

约定：缩进 Tab；样式全在 `styles.css`；协议消息只改 `protocol.ts`（§4）；服务端 URL 一律 `appUrl()` 包一层（§9）。
i18n：前端 `useT()`，核心 `zh`/`en`（新 key 两处都加，`tests/unit/locales.test.ts` 锁对齐；语言包 `locales/*.json` 缺 key 回落英文）；守卫 `tests/unit/i18n-dead-keys.test.ts` 拦「加了没人引用的 key」（模板拼接的 `thinking.*` / `promptTok_*` 在白名单里）；
服务端 `pick(lang,zh,en,key?)`（key 全局唯一 `<模块>.<slug>`；多行 `getServerBlock`）；**工具定义提示词（description/promptSnippet/promptGuidelines）纯英文精简**，守卫 `tests/unit/tool-prompt-hygiene.test.ts`；notice 推 UI 用 `text`+`textEn` 双字段。
测试：端口 ≥8900 隔离；data-dir `mkdtempSync` 隔离；精确清理自己进程；**禁 `pkill -f`**。
Playwright：Chrome 路径走 `tests/lib/chrome.mjs`（`PI_WEB_CHROME` 可覆盖）；仓库根用 `fileURLToPath(new URL("..", import.meta.url))`（Windows 下 `URL.pathname` 会 ENOENT）；win32 清理走 `tests/lib/port-utils.mjs` 的 `freePort`。

## 6. 发布流程

> 详见 `docs/release.md`

```bash
npm run typecheck && npm run build
npm run changelog:i18n   # 文案有增减必跑
git add -A && git commit -m "feat(xxx): 描述"   # 不带 Co-authored-by
git push origin main
git tag vX.Y.Z && git push origin vX.Y.Z   # tag 带 v，与 npm 版本一致；Action 自动建 Release+桌面包
npm publish
```

版本须高于 registry；升级后 `pi-web-ui server restart`；发布前检查示例文件不泄密；Release 说明预览：`node scripts/release-notes.mjs X.Y.Z --base v<上版>`。

## 7. 环境变量

> 完整列表见 `docs/env-vars.md`

| 变量                        | 默认            | 作用                                                                                                                                                                                                                                                                         |
| --------------------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PI_WEB_PORT`               | `8787`          | HTTP 端口                                                                                                                                                                                                                                                                    |
| `PI_WEB_HOST`               | `127.0.0.1`     | 监听地址（默认 loopback）                                                                                                                                                                                                                                                    |
| `PI_WEB_CWD`                | `process.cwd()` | 智能体工作区                                                                                                                                                                                                                                                                 |
| `PI_WEB_DATA_DIR`           | `~/.pi-web`     | 数据目录                                                                                                                                                                                                                                                                     |
| `PI_WEB_SDK`                | `global`        | #321 起默认跟随：机器上有**更新**的 pi 副本（祖先链，如全局 pi CLI）就用它（多份取最高），没有或更旧回落自带副本；`bundled`/off/0/false/no = 强制自带（可复现，报 bug 用）。升级全局 pi 后**重启服务**即生效；更新面板会亮「运行中 vs 机器上」差距并提供「安装全局引擎」入口 |
| `PI_WEB_TOKEN`              | 空              | 共享口令鉴权                                                                                                                                                                                                                                                                 |
| `PI_WEB_PLUGIN_CATALOG_URL` | 官方清单        | 市场目录来源；空/`off`/`0`/`false`/`no` 关闭；`PI_WEB_PLUGIN_CATALOG_INSTALL=1` 才开机自动安装                                                                                                                                                                               |
| `PI_WEB_TOOL_TIMEOUT_MS`    | 20 分钟         | 看门狗超时（`ask_user_question` 豁免）                                                                                                                                                                                                                                       |
| `PI_WEB_TOOL_LAZY_LOADING`  | `1`（开）       | 工具延迟加载的**部署级默认**（设置页开关优先）：`0`/`false`/`off` = 全部工具直接活跃（见 `docs/tool-context-budget.md`）                                                                                                                                                     |
| `PI_WEB_PRESET_REPO`        | `xing-shuyin/pi-web-ui-presets` | 设置预设的社区共享仓库（`owner/name`）；`off`/`0`/`false`/`no` = 关闭一键分享（导入/导出仍可用），见 `docs/preset-sharing.md`                                                                                                                                                    |
| `PI_WEB_PRESET_CATALOG_URL` | 仓库 `index.json` 的 raw 地址 | 「浏览分享」的列表来源；空串或 `off` = 关闭浏览；5 分钟缓存 + 刷新绕过，失败保留上次列表                                                                                                                                                                                       |
| `PI_WEB_PRESET_GH`          | `gh`            | 一键分享调用的 GitHub CLI 路径（不可用时回落「复制 JSON + 打开预填建 Issue 页」）                                                                                                                                                                                               |

## 8. 部署

> 详见 `docs/deployment.md`

- 前台：`pi-web-ui --port 9000 --cwd /path`
- 自启：`pi-web-ui server install`（macOS→launchd / Linux→systemd / Windows→登录 Run 键，免管理员）
- Docker：`docker compose up -d`

## 9. 常见坑（现行有效禁令清单）

- **`details` ≤64KB 超限整丢**：`serialize.ts` 下发+随会话持久；新工具别塞大块内容（自封顶，见 `present-files-tool.ts` 摘录预算）；前端渲染不许依赖 details（参数解析+可选合并，见 `present-items.ts`）。
- **工具提示词三处职责分离（单一口径 + 守卫）**：模型同一轮里能同时看到三处工具信息 —— tool schema 的 `description`（写「做什么 + 副作用/边界」，不写「何时用」）、系统提示词 `Available tools` 列表里的 `promptSnippet`（只写触发条件；列表已渲染成 `- name: snippet`，所以不写工具名前缀；≤80c）、`Guidelines` 段里的 `promptGuidelines`（何时用/顺序/禁止/跨工具路由）。**同一条信息只说一遍**：snippet 与 guideline 都不复述 description，参数说明不复述主描述；同一个 schema 块被多个工具用（SSH 凭据、连接/库参数、坐标参数）要抽共享常量 —— 所有活动工具的 schema 同时在上下文里，写 N 份就是花 N 份 token。主描述 ≤600c（`patch-tool` 这类整份 op 语法表豁免）。守卫 `tests/unit/tool-prompt-hygiene.test.ts`（CI 必跑）：纯英文、长度上限、snippet 工具名前缀、snippet/guideline 复述检测、同文件同义重复检测。**`edit` 的模型可见文案也归本项目单源**：SDK 自带的 description 与 guidelines 逐条复述同一批规则，无扩展覆盖时（`fallback`）由 `agent-service.ts` 的 `EDIT_DESCRIPTION` / `EDIT_GUIDELINES` 顶上（只换文案，参数 schema / 执行体 / 渲染仍用 SDK）；有扩展 edit 时（`composeWith`）它的文案一字不动。
- **逐工具文案可编辑（设置→工具区「编辑文案」）**：上面那三处（description/snippet/guidelines）都可被用户逐工具覆盖，存 `ClientSettings.toolPromptOverrides`（不进预设，纯运行偏好），留空 = 用出厂默认。归一化/打补丁的唯一事实源是 `server/tool-prompt-overrides.ts`：`_toolDefinitions` 永为出厂值（复原基准），`applyToolPromptOverrides` 在 `applyToolGating()` 链尾给 `_toolRegistry` 的 AgentTool 打 description 补丁 + 重置 `_toolPromptSnippets`/`_toolPromptGuidelines`（`_refreshToolRegistry()` 会重建注册表，所以每次门控后都要重放）——注册表是 `[...扩展, ...customTools]` 合并后的全集，所以核心内置/本项目工具/插件/MCP 一网打尽；SDK 改私有字段名则返回 null 降级。系统提示词那一侧（compose 循环 + `getToolInfo`/`getToolPrompt`）按同一张表取生效文案。**新增可编辑维度要同时改三处**：`server/tool-prompt-overrides.ts`（归一化/打补丁）、`protocol.ts` 的 `UiToolPromptOverride`（wire 面）、设置页编辑器（`ToolPromptEditor`）。回归 `tests/unit/tool-prompt-overrides.test.ts` + `tests/tool-prompt-test.mjs`（后者用 mock LLM 断言真正发给模型的 tools[].description 与系统提示词）。DSH 无 pi 工具注册面，设置页不显示入口、服务端回 `unsupported`。
- **工具延迟加载（设置→工具区「工具按需加载」，默认开）**：只有核心工具（`bash`/`read`/`edit`/`write`）+ 常驻脚手架 `load_tools` 的 schema 进上下文，其余工具在系统提示词里只有「名字 + 一行摘要」目录，模型调 `load_tools` 后才把参数 schema 追加进来（`server/load-tools-tool.ts`）。门控不另开路径：未加载 = 临时加进禁用名单（`lazyLoadingDisabledTools()`）走 `applyAgentToolsGating`，加载只是把名字拿出来；`load_tools` 由 `forceActive` 保证非 standard 预设下仍活跃，且**不入目录**（不能单独关掉，否则机制断了）；已加载集合按 **session 对象**存在 `WeakMap`（过户跟着走，转录恢复时用当时的活跃集做种子）。**硬约束 —— 不得打坏前缀缓存**（provider prompt cache 按前缀命中）：① 系统提示词的 `{{tools}}` 永远列**完整目录且不过滤已加载**（`promptToolCatalog()`），文字与已加载集合无关；② Guidelines 段只取基线（核心 + `load_tools`），被加载工具的要点随 `load_tools` **回执文字**进对话（追加）；③ tools 数组只允许 `toolsAdded` 增量（无 `toolsRemoved`/redefinition），供应商按初始声明锚定流式注入。回归 `tests/lazy-tools-test.mjs`（零 token）逐字断言「加载前后 system 文本相同」+「第二次 tools 以第一次为前缀」；单测 `tests/unit/tool-manager.test.ts` 的 `lazyLoadingDisabledTools` / `forceActive`。细节见 `docs/tool-context-budget.md`。DSH 不适用（设置行隐藏）。
- **一个工具多条执行路径 → 提示词必须单源**：bash 有原生/终端/分流三条路径，提示词（description/snippet/guidelines/参数 schema）统一在 `server/tool-prompts.ts`，`makeAdaptiveBashTool` 只覆盖 `execute`。被覆盖的那份文案**永远不发送却仍会被继续维护**（历史上终端版整段写死、"prefer head/tail over piping" 只写在它里面），是最容易积累的腐化点。
- **新增可开关工具动三处**：`tool-manager.ts` 的 `AGENT_TOOL_CATALOG`（唯一事实源；工具名字符串只在此定义，模块 import+re-export）＋ `agent-service.ts` 的 customTools 注册（DSH 无注册面除外）＋ `tests/unit/tool-registration.test.ts` 的 FACTORY_TOOLS。**对 SDK 内置工具的覆盖（read/write/edit）不进创建时的 customTools** —— 那会静默顶掉第三方扩展注册的同名工具，要走 `server/tool-overrides.ts`（基底 = 扩展实现优先，见 `docs/architecture-core.md`「覆盖与第三方扩展同名工具共存」）。两道 CI 守卫：注册→目录、目录→设置行（`OTHER_AGENT_TOOLS` 循环渲染，不手写行；`todo_list` 固定 markers 分区；核心内置工具 bash/read/edit/write 不入目录，走设置页「核心工具」区单独开关（`CORE_BUILTIN_TOOL_NAMES`，禁用名单同样接受））。
- **服务端 URL 必须 `appUrl()` 包一层**（`/ws`、`/api/*`、`/plugins/*`、`/themes/*`），否则 nginx 子路径部署 404；排查先看请求带没带应用根前缀。
- **elsewhere/过户**：`take_over_conversation` 搬 runtime 本体（单 writer）；`ask_user_question`/`browser_page` 在调用瞬间按 runtime 身份解析持有方（`bridgeTarget`+`findConversationHome`，锚点是 `created.session` 对象不是 conv id）；跨页作答 `question_answer` 带 `owner`；「另一处」行左键两段确认；elsewhere 只列 live 会话。回归：`takeover-test`/`idle-takeover-test`/`remote-answer-test`/`elsewhere-lifecycle-test`。
- **远期 cron 别交给 `host.schedule`**：Node 超 2^31-1ms 的 setTimeout 截断成 1ms → 死循环；远期用 `armDelay` 分片（≤6h）+ `nextCronFire` 回 null（「不再触发」）；notes 式需求挂一条 `* * * * *` 巡检、自带 `nextDue` 落库。回归：`plugin-cron-overflow-test.mjs`。
- **工作区禁家目录根**：`server install` 默认 `~/pi-web-ui`；`$HOME` 的同步扫描会被坏挂载挂起致事件循环假死。`ready` 只代表传输通，不代表会话就绪——测试判活按插件真实内容，不拿 `ready` 当信号；`attach` 链用户路径禁同步 `statSync`。
- **文本解码/行号**：预览·附件统一 `decodeText`（UTF-8→GBK→latin1）；`countLines` 不算尾随换行，前端 `split("\n")` 后 pop 末尾空串。
- **Windows 最小化通知**：`hasFocus`/`visibilityState` 在最小化时照常撒谎，只看窗口矩形（`isCollapsedWindow`，`notify.ts`）；Windows 吞通知还要求近 2 分钟有页面交互；**通知禁带 `tag`**（同 tag 静默替换，`renotify` 救不回）。排查：`NotifyToggle.tsx` 的 `SHOW_NOTIFY_TEST_PANEL` 改 true。
- **模型目录=官方整表替换**：`patch-remote-catalog.ts` 启动幂等改写 SDK（无旧残留、无"新增 N 个"）；`npm install`/SDK 升级后重启自动重打，结构变化自动跳过。验证：`tests/scratch/verify-patch.mjs`。
- **token cookie**：`pi_web_token` 存 `encodeURIComponent` 后的值，**读时先 `decodeCookieToken` 再比**（`server/auth-cookie.ts`）；改口令后一次正确的 `?token=` 即永久恢复（有效请求刷新 cookie，401+失效 cookie 自动 Expire）。回归：`token-auth-test.mjs`。
- **CSS 变量先定义后引用**：未定义的 `var(--x)` 整条声明失效；守卫 `tests/unit/css-tokens.test.ts`（CI 必跑），新增豁免写理由。
- **长英文/长路径不许溢容器**（手机端高频）：`.messages` 是 `overflow-y:auto`，未声明的 `overflow-x` 计算值即 `auto` —— 任何子元素横向溢出都会变成整条消息区的横向滚动条。三条同源病根：①`white-space: pre-wrap` 只在空白处折行，挡不住无空格长 token（长单词/绝对路径/URL/model id）；②行级 flex 子项 `min-width:auto` 不肯缩，一个长单词顶宽整张卡片；③宽表格/长公式要自己收着。统一口径：**行容器**（装长文本的 flex 行/列）先钉 `min-width:0` + `max-width:100%`；**能断行的**用 `min-width:0` + `overflow-wrap:anywhere` + `word-break:break-word`（**基线在 `.md`**，p/li/td/a/内联 code 靠继承全覆盖）；**单行元素**用 `overflow:hidden` + `text-overflow:ellipsis`（overflow 非 visible 时 `min-width:auto` 自动解析为 0）；代码块/终端输出走 `pre` + `overflow-x:auto`。高度封顶的列表（如 `.plan-board-steps`）必须 `overflow-y:auto` + `overscroll-behavior:contain`，否则条目一多整页连输入框/顶栏/状态栏一起滚。守卫 `tests/unit/text-wrap.test.ts`（静态扫 styles.css，CI 必跑；清单分「可断行容器 / 单行文本 / 行容器」三档），新增「渲染模型文本的容器或行」要进它的清单 —— 目标条、工具调用卡头、折叠徽标都已登记（窄屏实报过：目标条一行长文案把消息区顶出横向滚动条）。
- **消息级按钮只有两个落点：卡头=纯复制键，`.msg-actions` 行=正文/用户消息**（口径简化过，别再加第三处）：① **所有卡头只有自己的复制键** —— 思考 / 工具 / 附件 / 技能 / 压缩摘要共用的 `.chead` 行右端就是 `.chead-copy`（复制思考 / 复制参数），**不挂任何消息级按钮**（历史包袱 `headExtra` prop + `.chead-actions` 容器 + 对应 CSS 已全删，见 `ThinkingBlock` / `ToolCallBlock`；此前「谁恰好是第一个 head 谁挂整排按钮」导致同一条回复里两张 bash 卡右端按钮数不一样，窄屏实报）；② **`.msg-actions` 行只给「有正文的助手消息」和「用户消息」**（`Message.tsx` 的 `showMessageActions = !editing && (role === "user" || canCopyWhole)`）—— 纯工具调用 / 纯思考 / 附件卡 / 插件消息没有正文，`canCopyWhole`（复制三件套要正文）与 `canSpeak` 都为假，那一行只剩「派生分支 / 回滚」这类对卡片无意义的按钮，复制已在卡头。**助手消息把这一行锚在最后一个正文块之后**（`anchorIndex` 扫 `asText` 找末个 text 块插在它后面，窄屏实报「先说话、再发工具」时按钮被工具卡隔开、看着像属于工具卡），无正文块才落消息末尾；**用户消息不锚**（`.msg-user .msg-body` 是气泡，插进去会坐进气泡里）。用户那一行是「直接重问 / 编辑重问 / 派生分支 / 回滚 / 复制三件套」。**右键菜单不受影响**：`contextmenu.toolcall`（卡头右键）+ `contextmenu.message`（消息右键）保持全功能，是这两条之外的功能入口。
- **卡头（`.chead`）右端的复制键要对齐**（思考 / 工具 / 附件 / 技能 / 压缩摘要共用一套）：触屏 `hover:none` 下 `.chead > .chead-copy` 在流内常驻并钉 `flex:none`（右缘对齐基准），卡头成了一行「左：图标+标题+参数提示，右：复制键」。写 `.toolcall-path` 等提示文本时**必须可缩**（`min-width:0` + ellipsis，不许 `flex-shrink:0`）：不让步 → 整行顶宽 → 复制键被顶出卡头右缘（`.toolcall overflow:hidden` 裁掉、点不到），且每行按提示长度各自右移，右端**列不齐**（窄屏实报）。配套口径：主体 `.chead-title.toolcall-name` 写 `flex:none`（选择器要 `.chead-title.toolcall-name` 提权重才压得住后面那条 `.chead-title`），提示文本一律 `flex: 0 1 auto` + `min-width:0`（缺口全由它们吸收），复制键钉 `flex:none`；≤640px 额外把**路径少显示点**（`max-width: min(20ch, 32%)`，命令预览 `min(28ch, 44%)`）并把 `.chead` 的 `gap` 收到 4px。守卫：同一个 `text-wrap.test.ts` 的「卡片头右端按钮列」分组（`.chead` 行容器 / 四个 hint 可缩 / `.chead > .chead-copy` 的 `flex:none` / 工具名 `flex:none` / 思考标题可缩）。⚠️ 别用 `flex-wrap:wrap` 兜底：复制键是最后一个子项，换行会把它单独甩到第二行左端。
- **UI 扩展点三规则**：① slot 两端同口径（manifest `ui` 与 `host.ui.register()` 同一别名+枚举；前端不认识静默丢弃）；② 新可隐藏/可排序入口必须在渲染组件接 slot（`hostNodes`+顺序驱动，无数据时退化全画；顶栏单一扁平流，禁按种类分组容器；溢出与隐藏走同一「⋯」菜单、实测宽度决定，`topbar-fit.ts`）；③ 往下展开的菜单 portal 到 body + fixed（z-index 救不了 overflow 裁剪），关闭走 ref；浮层统一 `useFloatingPanel`，**禁在 scroll 里关**（只重算锚点；mousedown/Escape 才关）；**禁把 slot 容器做成 `position:fixed` 贴边浮层**（会压住左右面板里的按钮）——`sidebar.left/right` 的贴边停靠栏（`SideDock`）是 `.layout` 里的 flex 子项，由它给面板/主区让出宽度，无图标时整条不渲染（`return null`，不占宽）；想要旧行为由设置项 `uiLayout.sideDockFloat` 开关（悬浮模式才加 `.side-dock-float` 回落到 `fixed`，**且折叠按钮只在这个模式下出现** —— 流内停靠栏只占一条窄边，收起省不下宽度）；停靠栏里的 `align` 是**竖轴**（start=靠上 / center=居中 / end=靠下，三段各一颗内容高的贴边药丸，配置入口是布局页那个对齐下拉）。回归：`ui-layout-ui-test`、`side-dock-align-ui-test`（真 Chrome）。
- **图标编辑模式（拖拽改布局）**：顶栏「⋯」→「编辑图标」进入（`components/IconEditor.tsx`）。四个栏（`topbar.primary`/`bottombar`/`sidebar.left`/`sidebar.right`）+「待放回」托盘同处一面板，拖拽 = **直接写同一份 `uiLayout`**（`order`/`align`/`slots`/`hidden`，与设置页互通），翻译口径全在纯函数 `web/src/ui-layout-edit.ts`（单测 `ui-layout-edit.test.ts`）。三条约束：① **⋯ 因此常驻**（顶栏有任何条目就画，否则用户根本进不来），② 拖拽用 **pointer 事件**而非 HTML5 DnD（DnD 在触屏上不触发，`PluginMenu` 的旧实现只能桌面用），③ 内置条目的 `icon` 名字要在 `host-icon.tsx` 登记（否则编辑面板里只剩文字），守卫 `host-icon.test.ts`。
  ④ **「⋯」菜单里的行必须同字号**：portal 挂在 `body` 下，菜单里自绘的扁平行（`.tb-row`，如「编辑图标」）会继承 `body` 的 14px，比搬进来的 chip 行（`.chip` 自带 12.5px）大一号 —— `.plugin-topbar-menu` 与菜单行选择器都写死 `font-size: 12.5px`；图标格 `.plugin-icon-glyph` 只装 emoji 字形，行内图标要**直接放 svg**（走 15px 方盒规则），包进 glyph 时另有 `svg{width/height:100%}` 兜底。守卫 `topbar-overflow-menu.test.ts`。
- **手机端抽屉横滑手势**（`web/src/swipe-drawer.ts` 纯判定 + `use-swipe-drawer.ts` DOM 粘合）：从屏幕左/右边缘往内横滑拉出该侧面板（跟手），面板开着时在遮罩/面板上往外横滑收回。三条约束：① **起手区**（`SWIPE_EDGE_ZONE_PX`）要避开系统侧滑返回，且轴向必须横向主导才接管（竖向整手作废，交给页面滚动）、起手点下面是横向可滚动元素（`pre`/宽表格）时**一律放行**；② 跟手期间直接写面板内联 `transform` + `.drawer-dragging`（禁过渡，不走 React 状态），松手先清内联值再让 React 的 `open` 类过渡到终态 —— 所以移动端遮罩改成**常驻**（`.drawer-backdrop.persistent` / `.on`）才能垫在下面渐显；③ 常量与 CSS 绑死（`drawerWidth` = `min(300px, 86vw)`、`DRAWER_HIDDEN_RATIO` = `translateX(±105%)`），改样式要同步改。回归：`tests/unit/swipe-drawer.test.ts`、`swipe-drawer-ui-test`（真 Chrome 触摸，手动跑）。终端视图的 `term-commands` 抽屉未接手势。
- **新对话三连**：经 `focusComposer()` 自动聚焦输入框（触屏豁免）；非 chat 视图先切回 chat；待发附件 chips 随会话清空（`advanceComposerSession`，判据 `UiState.sessionId`；空 sessionId 瞬时态不清）。单测：`composer-draft.test.ts`。
- **目标模式 2.0 三规则**（唯一路径：主对话=审查者、服务端另起**落盘**常驻执行对话）：① 一轮 = 服务端派活 + 服务端判定，**轮次/熔断/代次全在服务端**，契约明令禁止模型自己 spawn/wait（否则预算与停滞检测同时失真）；② 取样口径 = 工作区 `git diff` 指纹 + **执行者会话**错误特征（主对话是审查者，它的工具报错不代表执行受挫）；③ 审查指令投递前先 `waitMainIdle`、**先登记 verdict waiter 再投递**（`conv.awaitingVerdict` + 代次作废丢弃迟到结论）；**「中止/关闭就是停止」** —— `waitRoleAgent` 返回 `canceled`/`gone` 即终止循环（不续轮），`error`/`timeout` 才在预算内续轮。执行者落盘 → 占项目 8 个普通对话名额之一（满员即中止并提示，无降级路径）。回归：`tests/unit/goal-delegated.test.ts`、`goal-delegated-test`（零 token，已在 ALL 列表）。
- **DSH 三规则**：shipped 预设用 `preset-clones.ts` 的 file: 克隆（`file:` 指文件不是目录；roster 以 clone 为唯一 system 根，关 `includeShippedRoot`）；问卷 answerer 与技能过滤钩子挂每次会话 `setup()` 的 agent scope；底栏统计吃直播帧（wrapper `{global:true}` 转 `assistant.stream`）+ `assistant/message.usage`，纯映射 `dsh-usage.ts`。回归：`preset-clones.test.ts`、`dsh-smoke-test.mjs` §2.5、`dsh-stats-test.mjs`。
- **`i18n.tsx` value 只许字符串字面量**（`+` 续行可）：`scripts/i18n-diff.mjs` 手写解析器只认这个；改完文案跑 `npm run changelog:i18n` 验证。
- **预设分享只走白名单净化**：导入的外部 JSON（粘贴/文件/网址/社区目录）在服务端 `server/preset-share.ts` 里过四道闸门——形状（`format=pi-web-ui-preset` / `version=1` / name / settings 对象）→ 19 字段白名单 + 类型校验（不符丢弃并回报 `rejected`，未知字段回报 `ignored`）→ 尺寸上限（文档 512KB）→ 抓取收口（只 http(s)、拒内网/回环/`.local`、超时 10s）。**绝不许把外部 JSON 直接摊进 `ClientSettings`**：`settings` 里出现白名单外的键就是 bug。落盘前必须 `dryRun` 预览（用户确认才写），且导入后的预设与本地保存的预设走**同一批归一化函数**。两个引擎共用同一份编排（`PresetSharePort`），别在 DSH 侧另写一套。协议/一键分享细节见 `docs/preset-sharing.md`；回归 `tests/unit/preset-share.test.ts` + `tests/settings-test.mjs`（后者显式 `PI_WEB_PRESET_REPO=0`，**绝不在共享仓库真开 Issue**）。
- **PR 提交前必须三连验（防 CI 失败铁律）**：① `npx prettier --write <改动文件>`（CI 第一道硬门禁，漏掉直接首步挂掉）；② `npm run typecheck`（Vitest 运行时只管执行、默认不阻断类型错误，必须显式全量编译确保 5 个 tsconfig 零错误；编写单测时 mock context 必须传 `{} as any`，严禁传裸 `{}` 触发 TS2740 阻断）；③ `npx vitest run <单测>`。严禁未跑格式化与全量类型校验直接提交 PR。

---

_结构/流程变更时同步更新本文件及相关 `docs/`。修改后运行 `/reload` 生效。_
