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

> 本文件是给 AI 编码助手（pi / Claude Code / Cursor 等）看的高层项目说明书。
> 详细文档按主题分拆在 `docs/` 目录下。
> 修改本文件后，在 pi 中运行 `/reload` 生效。

## 1. 项目是什么

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

- 仓库（公开）：`git@github.com:xing-shuyin/pi-web-ui.git`
- npm 包：`pi-web-ui`（发布者 npm 账号 `xingshuyin`）
- Node 要求：**>= 22.19.0**（pi SDK 的 dist 使用了 `import … with { type: "json" }` 语法）
- 版本：`package.json` 与 `package-lock.json` 两处同步维护

## 2. 技术栈

| 层     | 技术                                                                          |
| ------ | ----------------------------------------------------------------------------- |
| 后端   | Node + Express（静态 + `/api/health`）+ `ws`（`/ws` WebSocket 协议）          |
| 前端   | 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. 目录结构

```
pi-web-ui/
├── server/                     # 后端（Node ESM，编译到 dist/server/）
│   ├── index.ts                # 入口：express 静态 + /ws 端点、消息分发、心跳、优雅停机
│   ├── protocol.ts             # ★ 唯一事实源：wire 协议类型（client↔server 消息）
│   ├── agent-service.ts        # 核心：ClientSession（每客户端一个会话组，可并行多个对话）+ AgentService
│   ├── serialize.ts            # SDK 消息 → UiMessage 序列化
│   ├── text-sniff.ts           # 文件预览纯函数（previewKind/looksLikeText/decodeText/sniffImageMime/hexDump/countLines）
│   ├── queue-utils.ts          # 排队消息纯函数（removeFirstOccurrence：只移除第一条匹配，重复文本不连带删除）
│   ├── process-utils.ts        # 进程工具：snapshotListeningPorts/killPidTree/lookupProcessName
│   ├── client-state.ts         # ClientStateStore：<dataDir>/client-state.json 持久化
│   ├── uploads.ts              # 文件对话上传 + 保留期清理
│   ├── bg-servers.ts           # 后台任务跟踪（bash 前后端口快照 diff + 存活刷新）
│   ├── settings-service.ts     # 设置面板状态机
│   ├── goal-service.ts         # 目标/审查循环/调研向导
│   ├── i18n.ts                 # 服务端语言协商 + 翻译表注册（resolveServerLang/pick/bilingual/getServerBlock；v2 见下）
│   ├── tool-manager.ts         # ★ Agent 工具统一开关：AGENT_TOOL_CATALOG（终端 7＋子代理 7＋其他 12：edit_soft/delegate_task/ask_user_question/todo_list/browser_page/conversation_read/skill/schedule_task/schedule_list/schedule_cancel/present_files/claim_files；合计 26）＋ tool_manage 出入口（setAgentToolEnabled/applyAgentToolsGating），持久化只有 disabledAgentTools，遗留三开关双向同步
│   ├── edit-soft-tool.ts       # 独立宽松编辑工具 edit_soft（行核心匹配，忽略缩进差异；开关走统一工具 tab）
│   ├── read-tool.ts            # 覆盖内置 read：路径是目录时列目录条目（复用 SDK ls 的口径，其余原样转发内置；行为开关 readDirEnabled 在设置「工具」页，不入 AGENT_TOOL_CATALOG）
│   ├── present-files-tool.ts   # ★ AI 展示文件（present_files）：只读探测 stat/未知扩展嗅探/文本摘录 → 结构化 items 走 toolResult 的 details → 前端预览卡片；不自动打开任何窗口
│   ├── tool-info.ts            # 工具定义说明（工具卡右键 → 显示工具详细信息）：按名现取 SDK / DSH 运行时的工具定义 → `tool_info`（定义是大对象，不进快照；DSH 拿不到时回 `unsupported`）
│   ├── subagents.ts            # 第一方子代理：subagent_* 工具（spawn/get_result/steer/list/stop/templates）+ 运行态快照
│   ├── subagent-templates.ts   # 子代理模板库（全局 <dataDir>/subagent-templates.json；白名单语义；可选模型/思考强度，空=跟随主对话；enabled=false 对 AI 不可见）
│   ├── slash-commands.ts       # 斜杠命令（NATIVE_COMMANDS 内置命令拦截执行 + 目录推送）
│   ├── model-admin.ts          # 模型/服务商配置管理（含内置服务商多密钥：provider-keys.json + add/activate/remove_provider_key，模型目录复用系统默认，不复制）
│   ├── attachments.ts          # 附件构建（inline/reference/lines/imageData/fileData + 视觉桥）
│   ├── webui-context.ts        # 扩展 UI 桥（WebUIContext：widgets/statuses/dialog → 浏览器）
│   ├── themes.ts               # 主题管理（listThemes/resolveThemeFile）
│   ├── plugins.ts              # 可选界面组件插件（扫描 <dataDir>/plugins/<id>/；renderer 插件字段 view:false + renderers）
│   ├── plugin-catalog.ts       # 插件市场列表（builtin plugins/catalog.json + 用户自定义 <dataDir>/plugin-catalog.json）+ 目录同步纯函数（normalizeSyncPayload/writeCustomCatalog，issue #148）
│   ├── plugin-grants.ts        # 插件目录授权表（<dataDir>/plugin-grants.json）：工作区外目录的「记住」授权（父目录覆盖子目录），设置面板可撤销
│   ├── plugin-project.ts       # ★ 插件项目组装执行层（host.project.create：clone/写文件/git init；路径越界三道校验，授权由上层判，本模块不动授权）
│   ├── plugin-installer.ts     # ★ 插件后台作业：安装/更新/卸载跑 CLI 子进程（不占用户终端、不打断设置面板），输出按行回传 + 单作业锁 + 看门狗（issue #152）
│   ├── plugin-catalog-sync.ts  # 市场目录同步编排（拉取/校验/原子写/可选安装/重载+重推，issue #148）
│   ├── vision-bridge.ts        # 视觉桥：纯文本主模型看图转写
│   ├── files-service.ts        # 文件服务（readDirForUI/readFile/searchFiles/watcher）
│   ├── scm.ts                  # SCM 只读 git 查询（execFile git status/branches/history/filediff/commit）
│   ├── patch-node-pty.ts       # node-pty × Node --watch 兼容自愈补丁
│   ├── patch-remote-catalog.ts # pi.dev 模型目录整表替换补丁（幂等改写 SDK remote-catalog-provider：刷新后内置服务商列表=官方目录整表，不保留内置旧模型/不报“新增 N 个”）
│   ├── ensure-bash.ts          # Windows 轻量 bash 兜底（busybox-w32）
│   ├── control-socket.ts       # 本地控制 socket（status / quiesce / unquiesce）
│   ├── launch-origin.ts        # ★ 启动来源探测：本实例是否被 launchd/systemd/Windows watchdog 托管（更新面板「重启服务」的前置条件）
│   └── terminals.ts            # TerminalManager（PTY 管理 + 增量输出/按键工具）
├── web/                        # 前端（React + Vite，编译到 web/dist/）
│   ├── vite.config.ts          # dev 端口 5173，/ws 代理到后端
│   ├── src/
│   │   ├── App.tsx             # 顶层布局
│   │   ├── use-chat.ts         # ★ useChat()：WebSocket 连接管理、reducer 状态机、终端 bridge
│   │   ├── app-globals.ts      # ★ 全局运行态 store（engine/managed/tabs/版本号 + 全局发送器 appSend）
│   │   ├── types.ts            # ★ wire 协议 re-export shim（`export type * from "../../server/protocol"`）
│   │   ├── i18n.tsx            # ★ 多语文案（zh/en/it，zh 默认），新增 key 必须三处都加
│   │   ├── styles.css          # ★ 全部样式（按组件分区，带注释分隔线）；也是默认深色主题本体
│   │   ├── theme.ts            # 主题切换（/api/themes 列表 + localStorage 持久化 + applyTheme）
│   │   ├── sounds.ts           # WebAudio 提示音
│   │   ├── notify.ts           # 桌面/OS 通知（PWA）：是否吞掉通知的判定（Windows 最小化检测）+ 诊断",
│   │   ├── download.ts         # 下载（fetch→blob，绕开 Chrome Safe Browsing）
│   │   ├── ui-slots.ts         # ★ 宿主 UI 扩展点引擎（issue #146 完整版）：BUILTIN_UI_ITEMS（109 条宿主内置入口，覆盖 12 个 slot；其中 browser/sound/language/theme/update/github 六个默认 `hidden:true`，落在「⋯」溢出菜单里）+ buildUiSlots（宿主默认→插件贡献→插件 arrange→用户偏好 四层合并，22 个 slot）+ splitOverflow/restoreUiItem
│   │   ├── topbar-fit.ts       # ★ 顶栏「放不下的自动进 ⋯」纯函数（fitTopbar）：按实测宽度从尾部丢，宽度 0 的条目（桌面端 CSS 藏起的 ☰/📁）不占位也不进菜单，测不到宽度全保留，有单测
│   │   ├── context-menu-state.ts # ★ 通用右键菜单状态（contextmenu.* 槽位的 open/close + 定位/禁用/键盘导航纯函数，有单测）
│   │   ├── composer-bridge.ts  # ★ 输入框注入桥：宿主（扩展/插件）把内容塞进输入框草稿的模块级 sink 注册点，有单测（配 composer-draft.ts 的合并/去重纯函数）
│   │   ├── caret-visual-line.ts # ★ 输入框光标的首/末**视觉行**判定（镜像 div 量 offsetTop，自动折行算行；无布局时回落逻辑行），供 ↑/↓ 翻输入历史用，有单测 + 真浏览器回归（tests/composer-history-test.mjs）
│   │   ├── message-delta.ts    # message_delta 增量 patch 纯函数，有单测
│   │   ├── lazy-window.ts      # 消息列表惰性窗口化纯函数，有单测
│   │   ├── search-text.ts      # 会话内搜索索引纯函数，有单测
│   │   ├── present-items.ts    # ★ present_files 卡片纯函数（参数解析/与 details 合并/能力判定/自动打开闸门），有单测
│   │   ├── tool-info-state.ts  # 工具定义弹窗的模块级 store（打开/关闭/应答 → 视图，纯函数 + 单测）
│   │   ├── present-settings.ts # 卡片偏好：自动打开预览弹窗（localStorage，默认关）+ 已开过的 toolCallId 去重表
│   │   ├── tool-schema.ts      # 参数 JSON Schema → 表格行（参数名/类型/必填/说明，纯函数 + 单测）
│   │   ├── file-preview-bridge.ts # 工具卡片 → 文件预览弹窗的模块级 sink（App 注册 opener，同 composer-bridge 那一套）
│   │   ├── skill-block.ts      # parseSkillBlock：<skill> 块解析，有单测
│   │   ├── tool-args.ts        # 工具卡头参数提示纯函数（路径/超时安全提取，脏参数不抛错），有单测
│   │   ├── auth-token.ts       # PI_WEB_TOKEN 口令注入，有单测
│   │   ├── image-paste.ts      # 粘贴图片等比缩放 ≤1568px + PNG/JPEG 转码
│   │   ├── uuid.ts             # randomUuid（crypto 兜底），有单测
│   │   ├── protocol-version.ts # 协议版本常量
│   │   ├── main.tsx            # 入口：首帧前应用主题防闪烁 + initAuthToken
│   │   └── components/         # 见下
│   └── dist/                   # 构建产物（gitignore，但打进 npm 包）
├── bin/pi-web-ui.mjs           # CLI：前台启动 / server install|uninstall|start|stop|restart|status
├── desktop/                    # Electron 桌面壳（sidecar：随机空闲口起 dist/server + BrowserWindow）
│   ├── main.ts                 # 主进程：ready → 选空闲口 → spawn server(ELECTRON_RUN_AS_NODE) → loadURL
│   ├── electron-builder.yml    # 打包配置（win nsis / mac dmg / linux AppImage；npmRebuild:false）
│   └── README.md               # 桌面版说明：跑起来 / 约定 / 发布 / 签名（SignPath）
├── deploy/                     # 部署示例：launchd plist / systemd unit / Windows 任务 XML
├── themes/                     # 内置主题（纯 :root 调色板覆盖，不含布局）
├── make-light-theme.mjs        # 主题生成器（从 styles.css 的 :root 变量清单生成纯调色板）
├── tests/                      # 全部测试脚本（自包含：独立端口 ≥8900 + 临时 data-dir）
│   ├── run-smoke.mjs           # 零 token 协议冒烟聚合跑器（ALL 列表；插件后台作业/目录同步 plugin-jobs-test、多根工作区
│   │                           #   workspace-roots-test 都在里面）
│   ├── unit/                   # vitest 纯函数单测（插件 UI 扩展点：plugin-ui-manifest / ui-slots / context-menu /
│   │                           #   plugin-grants / plugin-project / plugin-installer / plugin-catalog-sync；多根工作区：workspace-roots）
│   │                           #   插件设置（声明式 schema / select 候选值）：plugin-settings / plugin-setting-options
│   ├── *-test.mjs              # 手写 Playwright E2E / WS 协议测试
│   │                           #   ⚠ 需 Chrome 的浏览器 E2E **不入 run-smoke**，手动 `node tests/<name>.mjs` 跑（缺 Chrome 自动 SKIP）：
│   │                           #     context-menu-ui-test（右键菜单槽位）/ ui-layout-ui-test（布局页 ↔ 界面一致性）/ plugin-settings-page-test（插件设置页）/ plugin-settings-select-ui-test（插件设置下拉）/ plugin-topbar-ui-test（顶栏条目 + 面板内后台卸载）
│   └── scratch/                # 一次性调试脚本（gitignore，不入库）
├── scripts/check-protocol-sync.mjs  # 守护 types.ts shim 单源机制 + protocol.ts 纯类型约束
├── .github/workflows/ci.yml    # CI：协议同步 → typecheck → build → vitest → 冒烟
├── .github/workflows/release-notes.yml   # tag 推送 → 按 CHANGELOG 建/更新 GitHub Release
├── .github/workflows/desktop-release.yml  # tag 推送 → windows-latest 出 NSIS 安装包并附到 Release（签名以后加这里）
├── extensions/                 # pi 扩展：webui.ts（/webui 命令启动本机服务并打开浏览器）
├── plugins/                    # 官方插件（webmail / db-client / vscode-editor / demo-mailbox / mermaid / run-trace / legado-web / wechat-ilink / image-toolkit / notes / live-preview / voice-input，各自的 README.md 见其目录；page-picker/extension 是**浏览器扩展**，不是 pi-web-ui 插件）
│   └── catalog.json            # ★ 插件市场内置列表（随包发布；社区加插件 = 在此加一条 + PR）
├── dev/                        # 本地开发辅助（notice/search 预览等，不入 npm 包）
├── Dockerfile / docker-compose.yml
├── docs/                       # 详细文档（本文件的分拆）
│   ├── architecture-core.md    # 核心架构：快照驱动、协议单源、安全边界、主题切换、多对话并发
│   ├── architecture-attachments.md  # 附件、图片、视觉桥、文件上传/预览/下载
│   ├── architecture-terminal.md    # 终端架构：PTY 管理、SCM 查询、活力检测、终端接管 bash
│   ├── architecture-plugins.md     # 插件系统：形态、协议、宿主扩展点、MCP 桥
│   ├── architecture-system-prompt.md  # 系统提示词组成：base/追加段/项目上下文/技能段 组装链路与 override 钩子
│   ├── development.md          # 开发工作流、CI、编码约定、测试规范
│   ├── release.md              # 发布流程（GitHub + npm）
│   ├── deployment.md           # 部署（CLI / Docker）
│   ├── dsh-engine.md           # DSH 引擎（agent 预设、问卷、底栏统计、工具桥）
│   ├── antigravity-proxy.md    # antigravity 反代接入
│   └── env-vars.md             # 环境变量参考
├── tsconfig.server.json / tsconfig.extensions.json / tsconfig.tests.json / web/tsconfig.json
```

`web/src/components/` 速览：

| 组件                                                                                      | 职责                                                                                                                                                                                                                                                                                                                                                                                    |
| ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `FilePreview.tsx`                                                                         | 文件预览弹窗：行号、点选/拖拽/Shift 选区、添加到对话；Markdown 预览可切换原文；可编辑保存                                                                                                                                                                                                                                                                                               |
| `PresentedFiles.tsx`                                                                      | `present_files` 工具卡片正文：图片/视频/音频在消息里直接显示/播放（点媒体开预览弹窗看大图），文本给开头摘录 + 「预览」按钮，每行带「预览/本地打开/在文件夹中显示/下载/复制路径」（后两项走 `file_reveal`/`file_open_default`）；AI 标 `focus` 的条目按偏好自动弹预览窗（开关/刚发生/toolCallId 未开过 三道闸）                                                                          |
| `ToolInfoDialog.tsx`                                                                      | 「工具详细信息」弹窗（工具卡右键 `contextmenu.toolcall` 的宿主内置条目）：展示工具的**定义**（说明 / 提示词摘要与要点 / 参数 schema 表格 + 原始 JSON），不含本次调用数据；内容由 `get_tool_info` 点开现取（不进快照），状态机在 `tool-info-state.ts`、schema 表格化在 `tool-schema.ts`（都是纯函数 + 单测）；portal 到 body（同 ContextMenu 的理由：消息流祖先有 overflow/transform）   |
| `LeftPanel.tsx`                                                                           | 左栏：最近项目、运行的对话、历史对话（含删除）                                                                                                                                                                                                                                                                                                                                          |
| `RightPanel.tsx`                                                                          | 文件树浏览（list_files），文件名点击→预览，📎/🔗/👁 附件按钮；服务端原生递归 watcher                                                                                                                                                                                                                                                                                                     |
| `ChatInput.tsx`                                                                           | 输入框 + 附件 chips（inline/reference/lines 三色）；全窗口拖放目标；followUp 排队/steer 插队；斜杠命令选择器                                                                                                                                                                                                                                                                            |
| `Message.tsx` / `MessageList.tsx`                                                         | 消息渲染（附件卡片、流式光标、tool 结果关联）；编辑重问保留原附件；技能卡片折叠；惰性窗口化；问题导航双通道；流式 StreamMarkdown                                                                                                                                                                                                                                                        |
| `ToolCallBlock.tsx` / `ThinkingBlock.tsx` / `BashBlock`                                   | 工具调用卡片（卡头状态图标右侧显示关键参数提示：文件路径 `.toolcall-path` / 超时 `.toolcall-timeout`，任何工具都试取，脏参数静默不显示）、思考块、bash 输出                                                                                                                                                                                                                             |
| `TerminalPanel.tsx` / `TermXterm.tsx`                                                     | 终端视图 + xterm 实例桥接                                                                                                                                                                                                                                                                                                                                                               |
| `SCMPanel.tsx`                                                                            | 源代码管理（Git）视图：status/branch/diff；提交/推送/拉取/切换分支；左栏宽度可拖分隔条调、双击复位、宽度存 localStorage（#139）                                                                                                                                                                                                                                                         |
| `BrowserControl.tsx`                                                                      | 顶栏「浏览器操作」入口 + 状态面板（扩展在不在 / 授权几个页面 / 总开关 / 打开扩展设置页 / 可照抄例子）；已授权页面一键「引用到对话」→ `mode:"page"` 的网页引用附件 chip（单页面时顶栏按钮直接变成该页面标题，点主体=引用、右侧 ▾=开面板）                                                                                                                                                |
| `TopBar.tsx` / `FooterBar.tsx`                                                            | 顶栏（模型/思考强度/后台任务/声音/新对话/视图切换）、底栏（上下文/成本/工作目录）                                                                                                                                                                                                                                                                                                       |
| `Dialog.tsx`                                                                              | 扩展 `ui.select/confirm/input` → 浏览器弹窗（正文/选项走 `Markdown(rawHtml)` 富渲染）                                                                                                                                                                                                                                                                                                   |
| `DshQuestionDialog.tsx`                                                                   | 模型提问对话框（`question_pending`，DSH 引擎经 goal-rpc userQuestions、标准 pi 引擎经 pi-web-ui 注册的 `ask_user_question` customTool 共用）：单选/多选/自定义文本 + 选中带 `preview` 的选项时「选项预览」富文本；question/detail/description/preview 走 `Markdown(rawHtml)`。待答问卷同时挂在快照（`UiState.pendingQuestion`）上，刷新/重连后由 `web/src/pending-question.ts` 恢复面板 |
| `ModelConfigModal.tsx` / `PiSetupModal.tsx`                                               | models.json 管理 / 首次配置引导                                                                                                                                                                                                                                                                                                                                                         |
| `SettingsModal.tsx`                                                                       | 设置面板（侧边栏分页：提示词/工具（含终端＋标记管理）/消息显示/技能/插件/界面插件/目标审查/视觉桥/预设/子代理模板；DSH 另有问卷页、无工具页）                                                                                                                                                                                                                                           |
| `GoalBar.tsx`                                                                             | 输入框上方目标条：设目标/清除/AI 提炼/轮数下拉                                                                                                                                                                                                                                                                                                                                          |
| `BgTasksModal.tsx`                                                                        | 后台任务弹窗：AI 启动的监听端口进程列表                                                                                                                                                                                                                                                                                                                                                 |
| `ModelThinking.tsx`                                                                       | 模型 + 思考强度下拉（模型下拉左侧按服务商筛选 + 顶部搜索过滤框）                                                                                                                                                                                                                                                                                                                        |
| `GlobalSearchModal.tsx`                                                                   | 全局搜索弹窗（Ctrl+K）：搜历史对话/最近项目/工作区文件名                                                                                                                                                                                                                                                                                                                                |
| `PluginView.tsx`                                                                          | 插件视图宿主：薄 React 壳 + 动态 import client bundle                                                                                                                                                                                                                                                                                                                                   |
| `SlotTabs.tsx`                                                                            | slot 驱动的 tab 容器（`rightpanel.tabs` 与设置面板「内置页＋插件页」共用）：除当前选中项外一律**不挂载**，切走即调插件 cleanup；选中态存 localStorage                                                                                                                                                                                                                                   |
| `ContextMenu.tsx`                                                                         | 通用右键菜单渲染层（`contextmenu.topbar/message/session/file` 四个槽位）：portal + fixed + 先渲染再实测尺寸钳制；只渲染，点条目交回宿主 `onAction` 分发                                                                                                                                                                                                                                 |
| `PluginPage.tsx`                                                                          | 插件自定义设置页宿主（`settings.pages` 槽位）：随选中项挂载/卸载（切走即 cleanup）；容器子节点只由插件写，失败提示是兄弟节点                                                                                                                                                                                                                                                            |
| `CollapsedMessage.tsx` / `LazyMount.tsx`                                                  | 消息折叠摘要行 / 消息级惰性挂载包装                                                                                                                                                                                                                                                                                                                                                     |
| `SearchBar.tsx`                                                                           | 会话内搜索栏（Ctrl+F，CSS Custom Highlight API 高亮）                                                                                                                                                                                                                                                                                                                                   |
| `Markdown.tsx` / `Dropdown.tsx` / `copy-button.tsx` / `HintTip.tsx` / `SoundSettings.tsx` | 通用件（HintTip：`?` 悬浮提示 portal 顶层渲染）                                                                                                                                                                                                                                                                                                                                         |

## 4. 核心架构（摘要）

> 详细文档见 `docs/architecture-*.md`

| 主题                   | 文档                                                                                                                                                                                                                | 要点                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **快照驱动**           | `docs/architecture-core.md`                                                                                                                                                                                         | 服务端是唯一事实源，60ms 节流推快照；增量快照（snapshot_delta）；message_delta 实时增量通道不经 snapshot 通道；WS permessage-deflate 压缩；多标签页序列化共享；协议版本协商                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| **协议单源**           | `docs/architecture-core.md`                                                                                                                                                                                         | `server/protocol.ts` 是唯一事实源；`web/src/types.ts` 是 `export type *` shim；新增消息只改 protocol.ts，两端 switch 各加分支                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| **全局运行态**         | `docs/architecture-core.md`                                                                                                                                                                                         | `web/src/app-globals.ts`：身份/能力（engine/managed/tabs/版本号）+ 连接态与 cwd 这类「整棵树都要」的信息放模块级 store，窄 props 组件用 `useAppField(key)` 单字段订阅、不再要 prop（吃整个 ChatState 的 App/TopBar/FooterBar 仍直读 `chat.*`）；全局发送器 `appSend` 也在这里（组件不收 `send` prop，面板的 `panelSend` 包装除外）；快照流里的数据严禁进去（store 通知绕过 memo）                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| **安全边界**           | `docs/architecture-core.md`                                                                                                                                                                                         | 默认只绑 loopback；WS Origin/Host 同权威校验；quiesce 准入控制；控制 socket；provider headers 不下发浏览器                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| **主题切换**           | `docs/architecture-core.md`                                                                                                                                                                                         | styles.css 是唯一布局文件，主题 = 纯 `:root` 变量覆盖（非整文件副本）；内置主题由 make-light-theme.mjs 从 styles.css 变量清单生成；改布局永不碰主题；终端跟随主题                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| **多对话并发**         | `docs/architecture-core.md`                                                                                                                                                                                         | 每对话独立 AgentSessionRuntime；对话按项目归属；set_cwd 切到目标项目对话；8 个上限/项目（子代理不计入）；共享同一个 ModelRuntime；左栏「运行的对话」的展示口径 = listed ∪「当前对话 + 已有内容」（#140：空白对话不入列、只影响展示，不动保留/释放规则）                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| **附件**               | `docs/architecture-attachments.md`                                                                                                                                                                                  | 三种模式（inline/reference/lines）；图片问答（base64 + 缩放）；文件上传（fileData 落盘）；视觉桥（纯文本模型看图转写）                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| **文件预览**           | `docs/architecture-attachments.md`                                                                                                                                                                                  | 512KB 上限 + 内容嗅探（文本/二进制 + GBK 回退）；媒体预览走 HTTP Range；下载绕开 Chrome Safe Browsing                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| **终端**               | `docs/architecture-terminal.md`                                                                                                                                                                                     | 每 Conversation 一个 TerminalManager；spawn 统一准入；按键编码纯函数；输出微批合并；node-pty × --watch 兼容自愈                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| **SCM**                | `docs/architecture-terminal.md`                                                                                                                                                                                     | 只读 git 查询走 execFile 直跑（不经过 shell）；git-dir watcher；写操作走可见终端 tab                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| **终端接管 bash**      | `docs/architecture-terminal.md`                                                                                                                                                                                     | 覆盖 SDK bash；设置开关 `terminalBash` 分流（关=原生 SDK 纯进程 bash，开=可见终端）；开时 `persist` 决定一次性/持久（false=跑完进程结束、输出保留；true=持久 ai-bash，shell 状态跨调用保留）；`head`/`tail` 截返回行；哨兵行技术；静默解阻（持久）；ai-bash/ai-bash-<n> 前端「AI bash」折叠分组且不计入终端数量上限                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| **read 读目录**        | `docs/architecture-core.md`                                                                                                                                                                                         | 覆盖 SDK 内置 read（customTools 按 name 覆盖，bash 是先例）：路径是目录 → 分流到 SDK 的 ls 实现列条目（`/` 后缀、同截断口径），其余（文件/图片/不存在）原样转发内置；开关 `readDirEnabled`（设置「工具」页，默认开）是行为开关不是工具开关，不入 AGENT_TOOL_CATALOG/预设，live 生效；仅 pi 引擎（DSH 无 customTool 注册面）                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| **插件**               | `docs/architecture-plugins.md`                                                                                                                                                                                      | <dataDir>/plugins/<id>/ 目录（manifest.json + index.mjs + client/entry.mjs）；attach 时热重扫；fenced-code 渲染插件（renderers + view:false，命中 ```lang 才懒加载，见 plugin-fence.ts；`view:false` 但顶层代码要常驻的插件写 `preload:true`，宿主每次进页预加载、可无 `default.mount`，见 plugin-loader.ts 的 preloaded）；官方插件走 `pi-web-ui install`（含子目录 source）分发，不进 npm 包；插件市场（plugins/catalog.json 内置列表 + 用户自定义，设置面板一键 `install --name <id>`，见 plugin-catalog.ts）；插件后台作业（安装/更新/卸载不占用户终端、设置面板就地显示进度，见 server/plugin-installer.ts）；`host.reloadCatalog()` 市场目录同步（#148）；`pi-web-ui install --build` 源码隔离构建（#150）；**UI 扩展点框架**（#146 完整版）：manifest `ui` / `host.ui.*` 贡献 + `arrange` 整理，宿主负责渲染/排序/溢出/可访问性/用户偏好/审计，插件不碰 DOM（动作经 `host.onUiAction` 回插件），合并规则与 22 个挂载点见 `web/src/ui-slots.ts`；插件自定义设置页（`settings.pages` → PluginPage.tsx）；通用右键菜单（contextmenu.* → ContextMenu.tsx）；目录授权与跨目录 fs（`server/plugin-grants.ts` + `host.fs.requestAccess/authorizedDirs/listPath/readPath/readTextPath/writePath/removePath`，工作区内免授权）；项目组装 `host.project.create`（`server/plugin-project.ts`）；插件→宿主动作桥 `window.__piWebUiHost`（宿主 API **v11**：setView / startChat / compose（塞输入框草稿，见 web/src/composer-bridge.ts）/ openSession（带目录授权，多根）/ sessions.list\|open / reloadCatalog / onUiAction / openModal+closeModal / shortcuts.register / searchProviders.register / composerProviders.register / pageCall / dom.anchors（特权 DOM 挂载点，见下），时序见 web/src/plugin-host.ts）；特权 DOM：manifest `permissions` 含 `dom` 族的插件 bundle 默认 403，需用户在设置面板逐个授权（`server/plugin-dom.ts` 的授权表 + `plugin_dom_consent`，授权后 epoch+1 重拉），限制的是“谁可以”而非能力；宿主设施版本 `PLUGIN_API_VERSION = 2`（manifest `apiVersion` 高于宿主则拒绝激活）；MCP 工具桥 |
| **多根工作区**         | `docs/architecture-plugins.md`                                                                                                                                                                                      | `set_workspace_roots`（额外工作区根，**按项目 cwd 持久化**、上限 `MAX_WORKSPACE_ROOTS = 8`，见 `server/client-state.ts` 的 `normalizeWorkspaceRoots`）：右栏「工作区根」选择器可切根/移除根；**AI 仍只在主 cwd 干活**，额外根只改「哪些算工作区内」—— `PluginManager.isInsideWorkspace()` 把它们算进去，插件 `host.fs.*` 读那些目录**免授权**；快照走 `UiState.workspaceRoots`（pi/DSH 双引擎都落快照；DSH 没有插件宿主，多根在那里只影响右栏文件树）                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| **子代理模板**         | `server/subagents.ts` + `server/subagent-templates.ts`                                                                                                                                                              | 设置面板配置角色系统提示词（append/replace）+ 技能/扩展白名单 + 可选模型/思考强度（空 = 跟随主对话当前模型/强度）；AI 经 subagent_templates 查询、subagent_spawn(template=) 选用也可不传按默认；停用模板对 AI 不可见；全局共享                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| **DSH Agent 预设**     | `server/dsh/preset-clones.ts` + `server/dsh/runtime/`（`override.patch.yml`/`custom-prompt.mjs`/`goal-rpc.mjs`/`launcher.mjs`）+ `DshPresetBar.tsx`                                                                 | dsh-web 四模式复刻（standard/ptc/minimal/cordis + 自建）：file: 克隆 shipped 预设（裸包名→入口文件 URL，roster 唯一 system 根）+ preset-plane patch 原子下发（缺失回落 legacy）；会话创建 mount preset、空白可切、首轮锁定（`agent-preset/locked`）；自定义提示词走独立 host section（minimal 压住）；桥接工具/问卷在 minimal 调用期拒绝；问卷与技能钩子必须挂 agent scope（host 收不到 scoped 事件）                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| **DSH 底栏统计**       | `server/dsh/dsh-usage.ts` + `server/dsh/runtime/goal-rpc.mjs` + `tests/dsh-stats-test.mjs`                                                                                                                          | 上下文占用 / 缓存命中 / 回复速率：老运行时吃持久 `assistant/chunk`，新版运行时走 agent scope 的 `agent/assistant-stream` 直播帧（wrapper `{ global: true }` → `assistant.stream`）+ 结算时的 `assistant/message.usage`；纯映射在 dsh-usage.ts                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| **压缩软上限**         | `server/soft-cap.ts`（纯函数：normalize/effective/softCapToReserve，单测）+ `ClientSettings.softCapTokens/softCapByModel`（全局+按模型，`client-state.json`，随预设走）+ `ClientSession.applyCompactionOverrides()` | 软上限 C → `compaction.reserveTokens = W − C` 经 `settingsManager.applyOverrides` 注入（与重试次数同一 live 机制；SDK 每次自动压缩检查前重读，无需 reload）；关闭/非法时回填 SDK 默认 16384；重放点 = 全部重试重放点 + 换模型 + 设置变更即时（含 `flushSnapshot` 推底栏标记线）；快照走 `contextUsage.softCap`（底栏琥珀色标记线 + hover 阈值）；pi 引擎独有，DSH 保持关闭                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| **工具结束实时状态**   | `docs/architecture-core.md`                                                                                                                                                                                         | tool_status 先于快照落盘，浏览器卡片立即从「执行中」→「已结束」                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| **AI 展示文件**        | `server/present-files-tool.ts` + `web/src/components/PresentedFiles.tsx`                                                                                                                                            | `present_files` 工具（工具目录第 25 个，默认开，仅 pi 引擎）：模型给路径清单 → 服务端**只读**探测（stat + 未知扩展嗅探前 4KB + 文本开头摘录 ≤1200 字、总量 6000 字）→ 结构化 items 进 toolResult 的 `details`（`serialize.ts` 现在会把 details 下发浏览器，≤64KB 超限**整丢**；随会话文件持久化，重放后卡片照常）→ 前端卡片：图片/视频/音频**内联直接看**（不折叠、不用点；媒体 URL 复用 `download.ts` 的 `downloadUrl(path, false)`，`/api/file` 白名单加了 `isAudioFile`）、文本给开头摘录 + 「预览」按钮开既有预览弹窗（`web/src/file-preview-bridge.ts` 模块级 sink，App 注册 opener）、每行「预览/本地打开/在文件夹中显示/下载/复制路径」（本地两项复用 issue #187 的 `file_open_default`/`file_reveal`，远端或无桌面主机由服务端 notice 报不支持）；AI 标 `focus: true` 的条目 → 设置「对话」页的「自动打开 AI 展示的预览」（localStorage，默认关）开着时自动弹预览窗，三道闸：开关开 + 工具结果时间戳 ≤30s（翻旧会话不弹）+ toolCallId 未开过（`web/src/present-settings.ts`）；卡片渲染**不依赖 details**（参数解析 + 按路径配对合并，缺 details 时 kind=unknown 照给预览按钮，见 `web/src/present-items.ts`）；开关在设置「工具」页（「其他」组，默认开）                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| **工具挂死看门狗**     | `docs/architecture-core.md`                                                                                                                                                                                         | 20 分钟超时自动 abort 会话；只停止运行不碰后台服务；**`ask_user_question` 豁免**（等人类回答不限时，问卷挂着也不算失联）                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| **待答问卷进快照**     | `docs/architecture-core.md`                                                                                                                                                                                         | `UiState.pendingQuestion` + `web/src/pending-question.ts`：刷新/重连后恢复问卷对话框（即时通道只推给提问那一刻在线的连接）                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| **未发送草稿进快照**   | `docs/architecture-core.md`                                                                                                                                                                                         | `UiState.draft` + `server/composer-drafts.ts`（单中心文件 `<dataDir>/composer-drafts.json`，按 sessionId 键入，每会话最新一条；localStorage 同步镜像一层）：只跟**全量快照**走（增量 delta 里 key 整个缺席，否则 `{...ui, ...d.state}` 合并会把草稿洗掉），`draft_update` 自带 sessionId（切会话的离开刷盘晚于服务端切换，不能按 active 落键）；发送/删会话即清，30 天 TTL 兜底；不进 LLM 上下文/搜索，pi 引擎独有                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| **后台任务列表**       | `docs/architecture-core.md`                                                                                                                                                                                         | bash 前后端口快照 diff；按客户端持久；单停/全部关闭                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| **扩展 UI 桥**         | `docs/architecture-core.md`                                                                                                                                                                                         | setWidget/setStatus/notify/select/confirm/input → 浏览器消息；dialog_response 回传                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| **模型操作浏览器页面** | `docs/architecture-core.md`                                                                                                                                                                                         | `browser_page` 工具：模型 → 服务端（`page_request`）→ 浏览器里的 pi-web-ui 页面（宿主桥 `pageCall`，宿主 API v3）→ page-picker 扩展 → **被授权的目标页面**（`page_response`）；闸门全在扩展侧（授权表 + 动作白名单 + 总开关 + `eval` 开关）；页面关着时工具报错，不做服务端代理                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| **对话引用**           | `server/conversation-read-tool.ts`（工具）+ `server/attachments.ts`（`conversation` 附件）+ 左栏右键 / 全局搜索（`contextmenu.session` 三件套 + `gs-quote`）                                                        | 用户把别的对话塞进输入框（💬 chip：运行中按 conversationId、历史按 sessionPath）：服务端只发小的 `<conversation-ref>` aside（转录不内联、不占 token、不过期），模型经 `conversation_read` 按需取（list 看运行+历史，read 分页读；历史 path 必须在会话列表里，任意文件拒绝；其他标签页的实时运行读不到，以落盘历史为准）；子代理会话同样注册该工具可嵌套读；开关走统一工具 tab（默认开）                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |

## 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 (vite) + build:server (tsc)
npm start            # 跑编译产物 dist/server/index.js（生产）
npm test             # vitest 纯函数单测
npm run test:smoke   # 零 token 协议冒烟聚合跑器
npm run desktop:dev  # Electron 桌面壳指到本地 sidecar（:随机口，需先 npm run build）
npm run build:extension  # 打包 page-picker 浏览器扩展（esbuild → plugins/page-picker/extension/dist/）
npm run pack:extension   # 打浏览器扩展 zip → release/（打 tag 时 CI 自动出包并挂 Release）
npm run desktop:dist # 本地打桌面安装包 → release/（gitignore；CI 也跑同一条）
```

**关键约定**：缩进用 Tab；i18n 走 `useT()`（核心只含 `zh`/`en`，其余语言是 `locales/*.json` 可下载语言包、不进 npm，缺 key 自动回落英文；新 key 加 zh+en 即可，`tests/unit/locales.test.ts` 锁 key 对齐；8 个语言包也要同步加 key 且顺序与 zh 一致）；服务端多语言（issue #91）：`server/i18n.ts` 中英内联 + 翻译表（v2）。`resolveServerLang` 只做归一（zh-CN→zh、pt-BR→pt，空→en）；`pick(lang,zh,en,key?)` 第 4 参数是全局唯一翻译 key（`<模块>.<slug>`，如 `subagents.list.empty`），zh 走内联中文、其他语言查表、缺表/缺 key 回落英文；多行块用 `getServerBlock`（表里存 `\n` 拼接的一行）；tool definition 用 `bilingual(en,zh)` 静态双语（无 key，模型看英文无碍）；模板内容/用户覆盖保持 zh/en 字段（配置品，不进表）。第三语言的表放在语言包的 `serverStrings` 节（与 `strings` 同文件、一次下载全带走，`validatePack` 校验；缺 key 自动回英文所以部分翻译可安全上线）；服务端启动 + 包安装/删除时经 `loadServerStrings`/`unloadServerStrings` 注册。浏览器经 `hello.locale`/`set_locale` 上报 UI 语言（`client-state.json` 的 `locale` 持久化，切换经 settings reload 通道自动应用）；maker 统一收可选 `lang?: () => ServerLang`，推 UI 的 notice 走 `text`+`textEn` 双字段（前端按 locale 自选）；样式全部在 `styles.css`；新增协议消息只改 `protocol.ts` 再两端 switch 加分支；**前端新增服务端 URL（`/ws`、`/api/*`、`/plugins/*`、`/themes/*`）一律用 `web/src/base-url.ts` 的 `appUrl()` 包一层**（nginx 子路径反代依赖应用根前缀，裸写根路径会在子路径部署下 404）。

**测试规范**：端口隔离（≥8900）；data-dir 隔离（`mkdtempSync`）；精确清理自己进程；不允许 `pkill -f` 杀全局。

## 6. 发布流程

> 详细文档见 `docs/release.md`

```bash
# 升版本 → 写 CHANGELOG（含 npm run changelog:i18n 自动记文案增量）→ 自检构建 → commit → push → 打 tag（Action 自动建 Release + 出桌面安装包）→ npm publish
npm run typecheck && npm run build
npm run changelog:i18n   # 文案有增减时必跑：自动刷新 CHANGELOG Unreleased 的 ### i18n
# 预览 Release 说明（只看不发）：node scripts/release-notes.mjs X.Y.Z --base v<上个版本>
git add -A && git commit -m "feat(xxx): 描述"
git push origin main
git tag vX.Y.Z && git push origin vX.Y.Z   # tag 带 v 前缀，数字与 npm 版本一致；推送后 Action 自动创建/更新 GitHub Release（含现场生成的 ### i18n），无需手跑 gh release create
npm publish
```

注意事项：版本号必须高于 npm registry；提交信息不要带 `Co-authored-by`；升级后需手动重启服务 `pi-web-ui server restart`；发布前检查示例文件不泄密。

## 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`     | 数据目录（client-state / uploads / plugins）                                                                                                        |
| `PI_WEB_SDK`                | `bundled`       | 用哪一份 pi SDK（issue #260）：`bundled`（默认）= 自带副本；`global` = 祖先链上有**更新**版本时改用它（升级全局 pi CLI 默认不会改变服务运行的 SDK） |
| `PI_WEB_TOKEN`              | 空              | 可选共享口令鉴权                                                                                                                                    |
| `PI_WEB_PLUGIN_CATALOG_URL` | 官方清单 URL | 开机插件市场清单来源：默认指向官方社区清单 `xing-shuyin.github.io/pi-web-ui-plugins/catalog.json`（仅拉取目录文档写可安装列表，**不自动安装插件**）；设为空串或 `off`/`0`/`false`/`no` 可关闭；配合 `PI_WEB_PLUGIN_CATALOG_INSTALL=1` 才会在开机时顺手自动安装 |
| `PI_WEB_TOOL_TIMEOUT_MS`    | 20 分钟         | 工具挂死看门狗超时（`ask_user_question` 问卷豁免，不受此限制）                                                                                      |

## 8. 部署

> 详细文档见 `docs/deployment.md`

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

## 9. 常见坑

- **服务活着时跑 `npm run build` 会黑屏**：vite 先清空 `web/dist` 再写新文件，构建窗口内打开页面，`index.html` 与 hash 产物对不上；旧版里缺失的产物会穿透 `express.static` 落进 SPA catch-all 回 200 的 HTML，浏览器当 JS 执行失败，且 SW 会把它按 200 缓进 STATIC_CACHE（之后服务恢复了也好不了，必须 Unregister SW）。正确姿势：先停服务 → build → 启动 → 黑页标签 Unregister SW 后重载。服务端已加 `/assets/*` 等缺失 404（不再回 HTML）、SW 只缓存 content-type 对得上的资源。

- **改了 `protocol.ts` 后忘了在两端 dispatch/onmessage switch 加分支** → 前端收到未知消息类型被 switch 静默丢弃，表现为"没反应"。先跑 `npm run typecheck`。

- **toolResult 的 `details` 现在会下发浏览器**（`server/serialize.ts`，JSON ≤64KB 才带、超限**整丢**；随会话文件持久化，重放后还在）：`present_files` 的卡片数据、`ask_user_question` 的答案、`todo_list` 都走它，前端不必为了一点元数据再打一次 HTTP。两个坑：**新增自定义工具时别把大块内容塞进 details**（bash 的 details 里带整份 output，小输出会因此翻倍进快照；工具作者要自己封顶，见 `present-files-tool.ts` 的摘录预算）；**前端要有「拿不到 details 也能渲染」的兑底**（present_files 卡片就是「工具参数解析 + 可选 details 增强」，见 `web/src/present-items.ts`）。

- **新增可开关的 Agent 工具要动两处**：① `server/tool-manager.ts` 的 `AGENT_TOOL_CATALOG` 加一项（开关的唯一事实源；「其他」组同时给 `descKey`/`offHintKey` 文案 key，顺序即设置页渲染顺序；`todo_list` 例外固定 markers 分区）② `server/agent-service.ts` 的 customTools 注册（DSH 除外，它无 customTool 注册面）＋ `tests/unit/tool-registration.test.ts` 的 FACTORY_TOOLS 给工厂登记产出（逼着当场决定入目录还是内核）。设置页「其他」组按目录循环渲染（`OTHER_AGENT_TOOLS`），不再手写行。历史教训：`skill` 与 `present_files` 都曾只有目录、设置里找不到开关；`claim_files` 更惨——连目录都没进。两道静态守卫：注册→目录（tool-registration，含 bash/read 永不入目录）＋目录→设置行（settings-tool-rows），否则 CI 直接报红。工具名字符串只在 tool-manager 定义，工具模块 import＋re-export（present-files/skill/conversation_read/claim_files/delegate_task 模式）。
- **nginx 子路径部署（页面在 /pi/ 下）插件/WS/API 加载失败** → 大概率是新增的请求路径没走 `appUrl()`（见关键约定），请求落在网站根绕过了 `location /pi/` 的剥离转发；排查时先看浏览器 Network 的请求带没带 `/pi` 前缀。
- **快照 60ms 节流**：调试时 `get_state` 可立即推一次（`cs.flushSnapshot()`）。
- **snapshot 发送背压**：`send()` 在序列化之前检查 `ws.bufferedAmount`，超过阈值时丢弃 snapshot（全量幂等且稍后必有更新）；丢弃时安排 250ms 重试 timer。
- **`hello` 前/会话未就绪时的命令**：`server/index.ts` 的 `pending` 队列会缓存并在 attach 后重放。
- **clientId 每标签页独立**（issue #10）：前端 `getClientId()` 存 sessionStorage（非 localStorage），同源多标签页是多个独立客户端。回归：`multi-tab-test.mjs`。关浏览器重开 = 新 clientId，旧 ClientSession 变残留：**无其他在线浏览器时 attach 自动认领**最近断开的有内容残留（只换 map 键、不搬 runtime；首帧推送前必须按新 id 重接 `wireClient`，否则把自己当“另一处”），有在线标签时不认领（跑着的照常走 elsewhere 只读 + 双写拦截）。回归：`tests/orphan-adopt-test.mjs` + `tests/unit/orphan-adopt.test.ts`。**手动过户**：右键 elsewhere 行 → `take_over_conversation` 把整段对话（含子代理后代、等答复问卷/页调用、看门狗剩余计时）搬到本页并切过去（搬 runtime 本体，单 writer 不变；id 撞车时搬入方改名；源 active 自动修好，双方通知；源页旧对话框经 question_retracted 同步收起）。elsewhere 口径 = 运行中 + 源会话仍持有的已结束空闲对话（`isStreaming:false` + `sessionFile`，结束后仍可过户查看；空白对话不入列）。**新标签/切项目不再自动恢复别处持有的会话**：最近文件被其他在线客户端持有（跑着或空闲）时空白落地 + 提示去向（之前只拦 running，空闲照样恢复出第二个 writer，两边轮流发送分叉历史）。回归：`tests/takeover-test.mjs` + `tests/idle-takeover-test.mjs`（结束后过户 + 空白落地）。**过户搬不动工具闭包**：`ask_user_question` / `browser_page` 是建 runtime 时把当时的 ClientSession + conversationId 闭包进去的，过户只搬对话 —— 所以这两条桥接在**调用瞬间**按 runtime 身份解析「现在谁持有它」（`ClientSession.bridgeTarget` + `findConversationHome`，锚点是稳定的 `created.session` 对象而不是会改名的 conv id）；否则过户后新提问的 question_pending / page_request 会推给**过户前那台设备**（持有方收不到、老设备上那张问卷还不进快照 → 刷新即丢，pi 引擎问卷不超时 ⇒ run 永远挂住）。**跨页作答**：点 elsewhere 行的 `?` 经 `peek_elsewhere_question` 把问卷原文拉到本页弹框，`question_answer` 带 `owner` 由持有方 resolve（本页只展示/关闭，id 对方作用域、不进 answered 集合；问卷消失则明确通知）。别处列表靠运行签名驱动其他页重推（覆盖空闲行：流式→空闲、空闲行出现/消失都重推；问卷态已计入签名，挂起/解决不改变流式集合，不带它已打开的页看不到 `?`）。回归：`tests/remote-answer-test.mjs`。回归：`tests/takeover-test.mjs`。
- **`host.schedule` 的超长延迟会溢出成 1ms（死循环）**：宿主用 `setTimeout(next - now)` 引爆，而 Node 的 setTimeout 延迟超过 2^31-1ms（≈24.8 天）会被**静默截断成 1ms**（打一条 `TimeoutOverflowWarning`）；旧版 `nextCronFire` 在「一年内找不到」时还会回一个 366 天后的哨兵值。两者相遇 = 「1ms 后再触发 → 插件回调写盘/广播 → 再排下一次」的死循环（`{monthly, dom:31}`、`0 9 1 1 *` 都是合法且常见的触发档位）。现在 `armDelay()` 把延迟切成 ≤6 小时的分片、醒来重算，`nextCronFire` 找不到回 `null`（后台面板显示「不再触发」）。插件侧也有一条硬规则：**别把远期 cron 交给 `host.schedule`**（notes 插件只挂一条 `* * * * *` 巡检，每条提醒自带 `nextDue` 落库）。回归：`tests/plugin-cron-overflow-test.mjs`（5 秒内 0 次触发才算过）。
- **socket 半开**：服务端每 2s 发送一次心跳并携带主机资源指标；客户端 30s 无消息主动断开重连（指数退避 1s→10s）。
- **预览与附件行号**：`countLines` 不算尾随换行；前端 `split("\n")` 后也要 pop 掉末尾空串。
- **Windows 老中文文件乱码**：预览/内联附件/行附件统一走 `decodeText`（严格 UTF-8 失败 → GBK → latin1）。
- **Windows 窗口最小化后收不到桌面通知**（Win11 + Edge/Chrome 实测，2026-09）：最小化后 `document.hasFocus()` 仍为 `true`、`visibilityState` 仍为 `"visible"`，连 `blur`/`visibilitychange` 都不发 —— 只有原生窗口矩形会变（`screenX/screenY` 变成 -21334（Edge）/-32000（Chrome）、`outerWidth/Height` 塌成标题栏 108×20 / 160×28）。判定全在 `web/src/notify.ts`（`isCollapsedWindow` + `shouldSuppressNotify`，纯函数 + 单测）；Windows 上还额外要求「最近 2 分钟内有页面交互」才肯吞掉通知（焦点/可见性本来就在骗人，宁可多提醒也不静默）。**另一个坑：通知不能带 `tag`** —— Windows 把同 tag 的新通知当成「替掉旧条目」而且静默（没横幅、没提示音），只要通知中心里还留着一条 pi-web-ui 通知，后面每条都会被无声替换（`showNotification` 仍然 resolve，页面上看不出问题；`renotify: true` 实测救不回来）。排查入口：顶栏声音下拉 → 通知块底部的「发送测试通知」按钮（`NotifyToggle.tsx` 的 `SHOW_NOTIFY_TEST_PANEL` 常量，默认关闭，排障时改成 `true`），它显示实际通道（sw/page）、浏览器是否真的持有这条通知（`getNotifications()` 计数）与判定依据（焦点/可见性/最小化/空闲）。
- **模型列表刷新 = 官方目录整表替换（非并集）**：内置服务商（opencode-go 等）的模型目录来自 pi.dev（`https://pi.dev/api/models/providers/<id>`），`server/patch-remote-catalog.ts` 在启动时幂等改写 SDK 的 `remote-catalog-provider.js`，使 `getModels` 在远程数据存在时**整表返回官方目录**（无内置旧模型残留、无“新增 N 个”合并）；`listModels()` 先 `mr.refresh({ allowNetwork: true })` 与官方接口校验（SDK 4h 窗口内走 304）。注意：改 `node_modules` 的补丁在 `npm install`/SDK 升级后会失效，服务重启时自动重打；SDK 源码结构变化时自动跳过（回落 SDK 默认并集语义，不崩溃）。验证：`tests/scratch/verify-patch.mjs`。
- **PI_WEB_TOKEN 改口令后旧 cookie 卡死**（issue #71）：有效 token 请求会刷新 `pi_web_token` cookie 为当前值，401 且带失效 cookie 时自动 Expire——用户改了口令后**一次正确的 `?token=` 进入即永久恢复，无需清缓存**；别再实现「仅在无 cookie 时才下发」的旧逻辑（那是卡死根因）。回归：`tests/token-auth-test.mjs`。
- **`pi_web_token` cookie 存的是 `encodeURIComponent` 后的值**（issue #261）：cookie-value 只允许 ASCII，所以 `=` / `+` / 空格 / 非 ASCII 口令必须转义才能下发——**读取时一律先解码再和 `AUTH_TOKEN` 比**（`server/auth-cookie.ts` 的 `decodeCookieToken`，脏值解不开就原样返回）。反例（曾经的真 bug）：拿 `encodeURIComponent(AUTH_TOKEN)` 去比，于是 base64 口令尾巴上的 `=` 被存成 `%3D`，带 cookie 的请求全 401——表现为「`?token=` 那一次 200，之后每个资源都 401」。手写/旧客户端的明文 cookie 也要继续接受（`requestTokens` 两种候选都试）。
- **CSS 变量引用必须先有定义**：`var(--ghost)` 引用一个全仓没有 `--ghost:` 声明的变量按规范是 guaranteed-invalid —— 整条声明在计算值阶段失效（`background` 直接没背景、`box-shadow` 连投影与聚焦光环一起丢），带 fallback 的则静默用硬编码值（浅色主题下必是深色块）。`--bg-elev1`（正确名 `--bg-elev`）全史未定义还被 10 处引用，goalbar 因此一直没填充。回归靠 `tests/unit/css-tokens.test.ts`（静态体检，CI 必跑）；新增豁免（运行时注入的变量、刻意的中性兜底）要在该测试的豁免表里写上理由。
- **插件 UI 扩展点的 slot 必须两端同口径**：manifest `ui` 与运行时 `host.ui.register()` 走的是**同一套**别名映射 + 枚举校验（`UI_SLOT_ALIASES` / `UI_SLOTS`，见 `server/plugins.ts`），只改一边就会变成“注册了但界面上没有”—— 前端 `buildUiSlots` 遇到不认识的 slot 是**静默丢弃**，最难排。条目全局 id 是 `<pluginId>:<itemId>`、内置条目是 `host:<name>`；合并优先级固定为「宿主默认 < 插件贡献 < 插件 arrange < 用户偏好」，**用户偏好永远最后说话**。
- **新增「可隐藏 / 可排序」的宿主入口必须在渲染组件里接上 slot**：布局页的勾选框与 ↑↓ 只对**真按 slot 列表渲染**的东西生效。顶栏整条与底栏整条由 `TopBar.tsx` / `FooterBar.tsx` 里的 `hostNodes` 节点工厂 + `uiPrimary`/`bottombarItems` 顺序驱动（顶栏是**单一扁直流**：slot 顺序直排、align 只决定落两个 spacer 划出的哪一段，无任何按种类包裹的容器、无贴边例外；放不下/被隐藏的入口落进「⋯」溢出菜单：菜单型整块搬组件、动作类走 `dispatchHostOverflow`），消息工具条由 `Message.tsx` 跳 `hidden`，右栏 tab 由 `RightPanel.tsx` 按 `uiRightPanelTabs` 顺序渲染，输入框/文件预览/目标条/SCM/终端/左栏分区同理（各组件的 `*HostNodes` + `renderMergedToolbar`，见 `web/src/slot-toolbar.tsx`）。**加新入口忘了这一处 = 一个点了没反应的勾选框**（历史上就这么退化过）。另外：slot 数据没传时要退化成「按内置默认顺序全画」，别把整条顶栏/底栏清空。回归：`tests/ui-layout-ui-test.mjs`（真 Chrome，不入 run-smoke；含 #162 的 elementFromPoint 可见可点断言）。
- **顶栏平铺化（方案 A）的两条硬规则**：① 渲染层不得再加按种类分组的容器（`.brand` / `.view-switch` / `.topbar-desktop` 已全部删除）—— 跨类调序会把组撕成两截，而且手机端会退化成「一整组被 CSS 藏掉」；条目外观一律挂在条目自己身上（`.chip` / `.tb-tab` / `.plugin-topbar-item` / `.panel-toggle`）。②「放不下」与「被隐藏」必须走**同一个**「⋯」菜单、且由**实测宽度**决定（`web/src/topbar-fit.ts` 纯函数 + `TopBar` 的 ResizeObserver：宽度按 id 缓存，被丢进菜单的条目量不到宽度，缓存才能让它在窗口变宽时回来；宽度为 0 的条目（桌面端 CSS 藏起的 ☰/📁）不占位也不进菜单；测不到宽度时全保留）。手机端不再有第二套硬编码「⋯」面板，桌面与手机共用一份 slot 数据。单测：`tests/unit/topbar-fit.test.ts` + `tests/unit/topbar-panel-toggle.test.ts`（结构锁 + 实测溢出接线）。
- **往下展开的菜单不许挂在横滑/圆角裁剪容器里**（issue #162）：顶栏「⋯」溢出菜单曾用 `position:absolute` 挂在 `.view-switch{overflow:hidden}`（桌面）/ `.topbar-flow{overflow-x:auto}`（窄屏；扁平化之前叫 `.topbar-actions`）里，往下展开的部分全被祖先裁掉 —— `z-index` 救不了 overflow 裁剪，`overflow-x:auto + overflow-y:visible` 写了也白写（浏览器把可见轴算成 auto）。解法与 `ContextMenu.tsx` 同路：portal 到 body + `position:fixed` + 按触发按钮实测锚定钳制。另：这类菜单的关闭回调若是内联箭头，effect 每 render 解绑/重绑全套 document 监听，离散按键可能落在空窗里丢键 —— 关闭走 ref，监听在菜单开着期间只装一次。
- **菜单/下拉面板类浮层严禁因页面滚动而关闭**：浮层（如顶栏「⋯」菜单、插件面板 `PluginMenu`、各类按钮下拉等）统一接入 `web/src/use-floating-panel.ts` 的 `useFloatingPanel` Hook。**严禁在 `scroll` 事件中关闭浮层**——`addEventListener("scroll", ..., true)` 处于捕获期，页面内任何子元素滚动（如 AI 流式生成触发消息流自动吸底、侧边栏滚动）都会触发事件，导致弹窗被秒关。正解是 `scroll`/`resize` 时只重算锚点坐标保持贴合（`measure`），只有点击外部（`mousedown`）或按 `Escape` 才关闭。
- **新对话必须自动聚焦输入框**：新建对话（点击顶栏/左栏/预设条的新对话按钮、或进入/切换到空白新会话）必须通过 `web/src/composer-bridge.ts` 的 `focusComposer()` 与 `ChatInput` 的空白会话自动聚焦机制聚焦输入框（触屏设备 `IS_TOUCH` 豁免以防软键盘遮挡）；在非 `chat` 视图（如 terminal/scm）点击新对话必须同步切回 `chat` 视图，避免界面停在旧视图且无法打字。
- **`set_workspace_roots` 是按项目（cwd）存的**：同一客户端在不同项目下各有一份额外根（`client-state.json` 的 `workspaceRoots[cwd]`），切项目不会把 A 项目的根带到 B。安全含义：**额外根 = 工作区内** —— `isInsideWorkspace()` 算进去后，插件 `host.fs.*` 读那些目录不再需要单独授权（工作区外才要走确认框），用户加根前得知道这一点（右栏文案已提示）。
- **Playwright 脚本**：Chrome 路径由 `tests/lib/chrome.mjs` 逐平台探测（`PI_WEB_CHROME` 可覆盖），不再写死本机路径；脚本里取仓库根一律用 `fileURLToPath(new URL("..", import.meta.url))`——`URL.pathname` 在 Windows 上得到 `/E:/...`，`spawn` 会直接 ENOENT；服务端进程清理在 win32 走 `tests/lib/port-utils.mjs` 的 `freePort`（负数 PID 的进程组在 Windows 上不存在）。
- **DSH shipped 预设在 launcher boot 下不可直接 mount**：composition 的裸包名按 agent scope 的 baseUrl（= cordis.yml 目录）解析，官方靠 PluginPackages resolution 走安装锚点、pi-web-ui 没装那套。解法是 `server/dsh/preset-clones.ts` 的 file: 克隆（裸名→运行时树入口文件 URL，注意 `file:` 必须指到**文件**，目录 URL 恒判 broken；含子路径的包按 exports map 解析），roster 以 clone 目录为唯一 system 根并关 `includeShippedRoot`（同名早根优先，否则 shipped 的 broken 条目遮住 clone）。回归：`tests/unit/preset-clones.test.ts` + `tests/dsh-smoke-test.mjs` 的 2.5 节。
- **DSH 的 agent-scoped 事件 host 收不到**：`user-questions/request` 与 `agent/pre-step` 只在 agent scope 递送，host 注册的观察者永远看不见（且新版 `UserQuestionService` 已删 `registerProvider`，旧写法直接让整个运行时 boot 崩溃）。问卷 answerer 与技能目录过滤钩子必须在每次会话创建的 `setup()` 里挂到 agent scope（见 `goal-rpc.mjs` 的 `agentSetup`）。
- **`web/src/i18n.tsx` 的文案 value 只许是字符串字面量（含 `+` 续行），不许用变量/模板表达式/函数调用**：`npm run changelog:i18n`（`scripts/i18n-diff.mjs`）是手写解析器，只认引号字面量 + `+` 拼接（0.87.0 的 `filesHelp` 长文案曾用 `+` 续行把脚本搞崩过一次，修完才支持）。改完文案必跑一次该脚本验证；新增豁免先修解析器再合。
- **DSH 底栏统计（上下文占用 / 缓存命中 / 回复速率）吃的是「直播帧 + usage」事件面**：新版运行时不发持久 `assistant/chunk` 了（改成 agent scope 的 `agent/assistant-stream` 直播帧 → **host 侧同样收不到**，见上一条），usage 只在结束时随持久 `assistant/message.usage` 落一次。所以 wrapper（`goal-rpc.mjs`）构造器用 `{ global: true }` 订阅直播帧并转 `assistant.stream` 通知；服务端两条路共用 `applyStreamChunk`，`conv.liveChunks` 标记互斥（过渡期两边都在也不会喂两遍），纯映射在 `server/dsh/dsh-usage.ts`（单测 + `tests/dsh-stats-test.mjs` 假 runtime 回归，零 key / 零子进程）。**漏了这条路 = 没有 streamingMessage / 没有速率 / 底栏上下文永远是 `—`。**

---

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