AGENTS.md · git:20260930.df64fa5 · 2026-09-30 · sha256 1711d847a82bb8ce
AGENTS.md git:20260930.df64fa5A
Immutable. This exact content never changes and is served at /api/v1/blob/1711d847a82bb8ce.
# 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`(审批门禁 + 规则库)·
`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 钩子 · **dsh-engine** DSH 预设/问卷/底栏统计/工具桥 · **goal-conversation-design** 目标模式 2.0(把目标审查变成对话:执行对话干活 + 当前对话当审查者;**已实施且只剩这一条路径** —— v1 的隐藏隔离审查会话 + 自治标记路径已删除,轮次/熔断/代次全在服务端,见该文档 §4/§11)· **development** 开发工作流/CI/编码约定/测试规范 · **release** 发布流程 · **deployment** 部署 · **env-vars** 环境变量全表 · **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 回落英文);
服务端 `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` 豁免) |
## 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-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 才关)。回归:`ui-layout-ui-test`(真 Chrome)。
- **新对话三连**:经 `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` 验证。
- **PR 提交前必须三连验(防 CI 失败铁律)**:① `npx prettier --write <改动文件>`(CI 第一道硬门禁,漏掉直接首步挂掉);② `npm run typecheck`(Vitest 运行时只管执行、默认不阻断类型错误,必须显式全量编译确保 5 个 tsconfig 零错误;编写单测时 mock context 必须传 `{} as any`,严禁传裸 `{}` 触发 TS2740 阻断);③ `npx vitest run <单测>`。严禁未跑格式化与全量类型校验直接提交 PR。
---
_结构/流程变更时同步更新本文件及相关 `docs/`。修改后运行 `/reload` 生效。_