Files
ycode/docs/加号菜单-REA核对-20261016.md
T

161 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 当前 cr 加号菜单:REA 源码核对
2026-10-16;WorkBuddy 5.8.2。只读已保存 REA JS/CSS,未操作原版 UI、未修改生产代码。本报告区分源码控制流、实际可恢复组件及仍须原版运行时确认的行为,不将 ARIA/函数存在等同于键盘全状态已验收。
## Evidence、SHA 与范围
JS 来自 `.tmp/rea-runtime-scripts.json`,Evidence `ev_717fa5e846e64d67ac19c8925d121bff632c0d3aefb3603b10858559cabb2dcb`;同版本后续 `.tmp/rea-expert-runtime-scripts-20261016.json` Evidence `ev_e91787b1d4dea651582c6cf12827775b9068b0ee8b80c68ba08d4c1d87d94e90`。原 ASAR root SHA `035254bc76310ea46317433e4840602094543ef323d2d5c5aff7c131729de918`。本轮重算所用 JS SHA,并用 Babel AST 定位具名函数。
| 简称 | `renderer/assets` 原文件 | SHA-256 |
|---|---|---|
| I | `src-DbE1_mgM.js` | `6c6ce63c93854783270e855572797d75e536e3b8d4548410e31fe9d147e1feb4` |
| C | `contexts-BMeWSUHk.js` | `5f35b7489b107e21ef452c8ee88a95265756f6fc444841eef869228bbc44768b` |
| H | `home-CC-czDJL.js` | `121d5a07acccb09e48f20f3a94f9fac8b10770e4a82bca1916d2cccd333f8d14` |
| L | `wb-input-add-B045GnIg.js` | `7842fa4321d5351bf6c822bbd0253e0d6d6e68cc9cfae3f2470b3d493c23daf4` |
CSS 来自 `.tmp/rea-css-recovered/manifest.json`,Evidence `ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94`,本轮逐 SHA 核对:
- `src-CyXHA0IC.css`:`a5e26b537bb50323aefbf25abddad52e288dd3868598be8d255d70886fe77cfe`。
- `desktop-sdk-setup-Bh0rg_xP.css`:`b2ee58c0caf560eeddc4eb8c91d86cabddbec634b8d37ed5d6b85d0644c0e142`。
函数 offset 均为导出 JS 的 UTF-16 code units,start 包含/end 不包含。
## 当前组件图与状态
```mermaid
flowchart TD
A[InputToolbarImpl] --> B[AddMenuImpl / memo AddMenu]
B --> C[useAddMenuItems: provider条件与顺序]
B --> D[Popover: 点击主入口]
D --> E[PopoverLayer: fixed定位/portal/外点击/退场]
E --> F[Menu + MenuItem + MenuDivider]
F --> G[SubMenu: hover计时/焦点/嵌套锁]
G --> H[Files / ReferenceFiles / Mode]
G --> I[Experts / Skills / Connectors]
H --> J[自身host实际能力回调]
I --> J
```
`I.InputToolbarImpl [1704717,1709947)` 在 toolbar 左侧第一个位置渲染 memo 后的 `hU(AddMenuImpl)`,此处直接 `<hU />`,没有向 AddMenu 传额外 disabled/onAction。主组件 `[1601493,1605376)` 有三项 state:`isOpen`、`activeSubmenuId`、trigger tooltip open;各项来自 input providers。
- 主入口 click toggle Popover。打开时关 tooltip,调用 skill.onAddMenuOpen,触发 expert.loadRecentExperts(错误吞掉);关闭时将 activeSubmenuId 清空并调用可选 onClose。
- 主菜单为 controlled `Popover(open=isOpen)`;同一时刻仅 activeSubmenuId 对应的受控 SubMenu open。进入另一项将 ID 换成新项;旧项迟到 close 回调只有 ID 仍匹配时才清,不会把新项一并清掉。
- 任意 item 若没有可见 children,handleItemClick 调用 onAction 并关闭;有 children 则主项 click 不执行子项动作,展开主要靠 SubMenu hover/touch/key handler。
- connector 子菜单首次展开还调用 connector.onEntryOpen('add_menu') 与 onAddMenuOpen;这里 map 回调中的局部 `C` 是“该项为connectors”的布尔值,不要误读成外层主菜单 isOpen。同一 provider 内有自己的 deferred catalog 加载 guard。
- tooltip 默认 200ms hover;主菜单已开时 disableInteractions,并以受控状态保持关闭。默认 trigger ghost/medium/circle/iconOnly;aria-label 为 `chatInput.addMenu.title`,打开时加 open class。
## 入口顺序、显隐和分隔
`I.useAddMenuItems [1599779,1601311)` 严格按下表顺序追加,没有另一个固定“始终显示五项”的数组:
| 顺序 | ID | 进入主菜单条件 |
|---|---|---|
| 1 | `add-files` | attachment 存在,且任一 pick handler / extraItems 非空 / process 存在 |
| 2 | `reference-files` | referenceFiles.onSelect 存在且 items.length > 0 |
| 3 | `mode` | mode provider 存在且 presentation 为 `add-menu` |
| 4 | `experts` | expert provider 非 null(最近列表可空) |
| 5 | `skills` | skill provider 存在(列表可空) |
| 6 | `connectors` | connector.onSummonMore 存在(列表可空) |
主菜单最后一个 files/reference-files 项与后面的非文件项之间插一个 divider;files 与 reference-files 两项之间不插这一分隔。空 skills/connectors 用隐藏 placeholder 保持入口,AddMenu 按 provider 特判仍渲染各自空态子菜单。所有项不可用时 AddMenu 返回 null。
因此未选工作目录时原首页可以只出现 files/mode/experts/skills/connectors;自身已有 cwd 且引用 provider 得到可用结果时出现 reference-files 符合源码条件。仅“选了 workspace”不保证有该项,还要求 provider enabled、结果非空和插入 handler。
## 定位、跨缝与浮层生命周期
`I.Popover [384214,385129)`、`PopoverTrigger [385129,385791)`、`PopoverContent [385791,386466)`、`PopoverLayer [381695,383678)`:
- AddMenu 主层 `placement:top-start`、offset 8、strategy fixed;offset→flip(padding8)→shift(padding8)→size(padding8)。size 将 `availableHeight-10` 与 maxHeight320 比较,下限160px。
- 默认 trackReferenceMove=true,使用 autoUpdate(animationFrame:true),不是仅窗口 resize 时算位置。anchor 为点击按钮 ref。
- `FloatingPortal` 将实际浮层从 toolbar DOM 中分离;显式带 `cr-theme`、data-cr-theme/data-theme,不能依赖原 toolbar 的继承主题仍自动存在。
- `useFloatingLayer [372662,373108)` 按全局 group stack 分配 z-index:默认 base9990、step10;子层同样登记,不是随意写固定 z-index。
- 主层 retained mount 用 150ms opacity transition。关闭先走可见性/transition,再 onExited 清 layerMounted;AddMenu 不直接零延迟卸掉主层。
`I.SubMenu [393807,398417)` 是关键复用组件:
- 通用默认 openDelay80/closeDelay150;**AddMenu 显式覆盖 openDelay0/closeDelay120**。
- placement right-start、offset4、fixed;flip 只指定 fallback left-start;shift padding8/crossAxis:true。size 限宽 `[160,480]`,高 clamp 到默认 max480,默认最低120;available size 更小时按源码 clamp 处理,不能自行换 legacy200下限。
- 主 trigger pointerenter 取消 close timer 并打开;pointerleave 取消 open timer,若 open 且没有 lock/输入焦点才计划 close;进入 floating 取消 close timer;离开 floating 同样计划关闭。
- **此 cr SubMenu 不调用 safePolygon**。跨4px缝的容错靠120ms close timer;不能把常见 safePolygon 行为当已证实现。
- pa context 的 `lock()` 对父子嵌套子菜单计数;子菜单展开时锁住父层,归零才重新尝试 scheduleClose。
- 如果 activeElement 是文本输入、textarea 或受支持 contenteditable,且位于 trigger/floating 内,不因 pointerleave 计划关闭。select/button 等并不在该输入匹配表中。
- touch pointerdown preventDefault、打开并设 touch latch;touch 模式不因 pointerleave 关闭;后续 mouse pointermove 重置 latch。
- 默认 destroyOnClose=true;主菜单关闭卸掉 SubMenu 会清 timer。destroyOnClose=false 才 retained DOM/display:none;AddMenu 没启用该分支。
## 外点击、Escape 与键盘:避免误抄 ContextMenu
父 Popover.useDismiss 在 outsidePress 中明确排除 `[data-cr-submenu-overlay=true]` 和 `.cr-sub-menu__overlay`。SubMenu 同时输出新旧两个 data 属性 `data-cr-sub-menu-overlay` / `data-cr-submenu-overlay`,它自己的 outsidePress 也排除任意这些 submenu overlay。缺少标记会造成点击子菜单被父层当外部点击关闭。
PopoverLayer 还有 capture pointerdown 分支处理存在 `[data-floating-ui-inert]` 的情况:点击其自身、anchor、submenu overlay 或更高 stacking z-index 不关闭;其它目标才 requestClose。这属于主层/其它浮层协作,不是简单 document.click。
SubMenu useDismiss 明确 `referencePress:false,ancestorScroll:true,escapeKey:true`;祖先滚动会关 child。父 Popover 的普通 useDismiss 没启用 ancestorScroll,主层可继续跟踪位置。一般外部 pointerdown 最终关父层和子层。输入法 composition 下通用 useDismiss 有 Escape 抑制,但下面组件级 handler 不能据此声称全部 IME 键盘路径一致。
| 位置 | 源码处理 |
|---|---|
| 主按钮 | 原生 button 支持 click/Enter/Space;PopoverTrigger clone 点击事件并 toggle,role combobox/aria-expanded/listbox。 |
| 父 Menu | `I.Menu [386573,386815)` 仅布局/role。MenuItem `[393251,393766)` tabIndex=-1;**没有父菜单 focus-first 或 Up/Down roving**。 |
| SubMenu trigger | ArrowRight/Enter/Space preventDefault+stopPropagation,打开并 queueMicrotask 尝试 focus 首个未禁用 role=menuitem。 |
| SubMenu overlay | ArrowLeft/Escape 关当前 child、尝试 focus 父 row;Up/Down/Home/End 轮转 query 得到的 menuitem。没有通用 Enter/Space click 分支。 |
| SearchInput | `[399641,400205)` 非空 Escape 先清词并 stopPropagation;空输入 Escape 才到 SubMenu/useDismiss。技能/连接器默认autoFocus=true,专家显式false。 |
| 自定义子行 | SkillListItem 有 tabIndex 和 Enter/Space;files/experts/connectors 的部分 role=menuitem div 没有 tabIndex,通用 `.focus()` 尝试不等于实际一定可聚焦。 |
旁边 `ContextMenuPanel` 有完整主列表 focus-first/Enter/Space/上下键,**AddMenu 不使用它**。恢复时不能把那套当成已验证 cr 加号键盘实现。根将采实际 REA 场景;本轮仅明确代码分支,未声称上表所有焦点动作已产生实际效果。
## 子菜单动作与实际 host 契约
### files
`I.FilesSubmenu [1581651,1583408)` 过滤当前 provider 存在的 handler,固定原顺序:local-files、space-library、wecom-wedrive、netdrive(own组),然后 tencent-docs、ima、lexiang(third-party组);两组均非空时有 divider,随后追加 extraItems。handler 映射常量 `[1583472,...]` 分别为 onPickLocalFiles/onPickSpaceLibrary/onPickWecomWedrive/onPickNetDrive/onPickTencentDocs/onPickIma/onPickTencentLexiang。点击调用 handler,立即 onCloseMenu;不是在菜单里伪造已选文件。
`C.useWbAttachmentProvider [769231,772634)` 默认本地路径建立 `input type=file,multiple=true`;有 onPickLocalFilesOverride 则调用 override。选文件走 process/附件 block;有 store 时先 processing,再 completed/failed,数量上限50(扣已存在附件),并按 transport/文件类型处理大小和上传。各云 handler 来自 attachmentOverrides/宿主真实 picker;不存在就不出现,不可用本地 mock 成功冒充云端选取。
### reference-files
`I.ReferenceFilesSubmenu [1585923,1587169)` 展示 provider.items 的文件/目录图标、base/ext、路径 tooltip,点击 provider.onSelect(item) 后关菜单。没有 openPath 或自动读全文调用。
`C.useWbReferenceFilesProvider [4599343,4600657)`:enabled、cwd、host.searchFiles 可用且未指定外部items,才请求 `{cwd,options:{search:'',resultNum:100}}`;按 id 去重。另读该 cwd 下 `.workbuddy/applications.yaml`,把有效 wbapp_ locator 应用合并。provider 最终还要求结果非空、onInsertBlocks 存在。
`createReferenceFileBlock [4592574,4592870)` 返回 `resource_link`,含 file/folder mention、filePath/path/relativePath/fileName、displayAsPhrase:true/displayAsContext:false;保存为 block 的应用项使用 application:// URI。对 YCode 的最小适配是自身合法 cwd 文件搜索和真实上下文引用插入;应用 locator/云应用目录未实现时不能造一个“已打开应用”。
### mode
`I.resolveModeOptions [1563614,1563708)`:provider.options 优先;add-menu 默认 craft(default)/plan/ask;toolbar 默认 craft/plan/quick,不能混用。`ModeSubmenu [1583710,1584870)` 只渲染非默认模式的 toggle。当前模式来自 draft.selections.mode,缺省默认项;点击已选 plan/ask 回 craft,点击另一个选另一个;先 input.api.setMode({id,label}) 再 provider.onChange。**不关闭菜单**。description 跟随当前模式;provider.disabled 或无 onChange 时禁用;带提示的 ModeToggleRow tooltip 延迟1000ms。
H 首页 onModeChange `[95414,...]` 接受 craft/plan/ask/quick;plan/ask 清输入 expert、app expert 和依赖 gate。C 会话 `useWbModeProvider [6087096,6089698)` 调实际 config.setMode,并维护权限/quick模型快照、忙/已发首条后的模式约束和错误 toast。宿主需接已有真实模式机制,不能仅让 toggle 视觉改变而消息仍按原模式执行。
### experts / skills
`I.ExpertsSubmenu [1574199,1579192)` 使用新 provider search/recents/pins;选择关闭主菜单,再走 provider.onChange,失败恢复旧 expert。详细 summon/default/owner/generation 规则见 [专家使用逻辑核对](C:/Users/Administrator/code/YCode/docs/专家使用逻辑-REA核对-20261016.md)。
`I.SkillsSubmenu [1594691,1599386)` 点技能构造实际 skill block 插入当前 draft,记录 recent,并调用 provider.onSelect 后关闭;onBeforeSelect/prepareSelection 异步有 alive/generation/session-context guard,取消不插入。置顶/secondary action stopPropagation;新增/管理调用宿主后关菜单。不能把来源为插件的所有内部技能无差别塞入全局技能菜单;provider 的 installed 查询排除 plugin skills。
### connectors
`I.ConnectorsSubmenu [1568795,1573334)` 普通行用于连接与能力选择,**不插 resource_link/文字、不打开详情**:未曾连接则 onConnectItem;未曾连接但正在 connecting 且可取消则 onCancelConnect;已连接并非 repairing 则 onToggleItem(item,!isInputConnectorOn)。普通 connect/toggle/repair 不关菜单;管理 footer 调 onSummonMore 后关闭。
`C.useWbConnectorProvider [828180,847538)` 的具体 delegates:
- `Xe/onConnectItem [842292,843874)` 调 connectConnector;成功经 He 将 enabled selection 写给 owner,更新连接状态并 toast;needsAuth/connecting 保持 pending,而非假成功。
- `He [840080,840405)` 维护 selected IDs Set,映射为含 enabled 的 items 调 onSelectionChange;不是上下文文件插入。
- `$e/onToggleItem [845424,846209)` 的 selectionOnlyToggle 分支仅改变该会话能力选择;普通分支关闭时调用 disconnectMcpConnector,失败恢复选择并报错。不能把所有 toggle 都按全局卸载做。
- `Qe/onRepairItem [844689,845423)` 调 repairConnector;`et/onCancelConnect [846210,846947)` 调 cancelMcpConnector;各自 opId guard 防旧结果落地。
- `nt/onSummonMore [846966,847083)` 优先外部管理回调,否则 goto('/connectors')。
YCode 自定义 MCP 已有真实连接/逐工具能力时可映射这些宿主动作;QQ/微信/云连接器目录/授权服务缺失时,应保持不可用状态或不提供对应 provider handler,不能将它们显示为已连接。菜单恢复本身不授权账号、不安装外部 MCP。
## CSS 与最小恢复边界
固定源 CSS:主弹层 `.cr-add-menu__popover` width200、背景/边框/阴影清空、overflow visible;内部 `.cr-menu` padding8、radius16、边框0.5;row32px、font14/line22、radius8、图标16+右间距8;divider margin6px;open trigger 加背景、plus旋转45deg,transform过渡420ms cubic-bezier(.25,.8,.3,1)。SubMenu overlay 只有140ms ease-out opacity keyframe,无 scale 位移。
desktop CSS 还对 `body .cr-add-menu__popover .cr-menu` 和所有具体子菜单覆盖 dropdown background/border/shadow,使用 wb tokens。只复制 cr CSS 不加载该覆盖与主题变量不等于原版。实际几何/pixels还需 root 的同宽原生截图。
建议最小恢复单位为 AddMenuImpl/useAddMenuItems + Popover compound + SubMenu + Menu primitives + source CSS/主题/浮层stack,子菜单接受自身provider。子菜单必须保持两个overlay标记、portal主题和父activeSubmenuId;附件/MCP/专家/技能宿主函数在恢复UI之外接真实现有能力。不要整个执行源 bundle 初始化或以旧 WbInputAddMenu 代替新 cr 控制层。
legacy `L.WbInputAddMenu` 的 initializer `[73372,79654)` 使用独立 groups、onMouseEnter/onMouseMove、refs Map、mobile absolute/desktop fixed、多种fallback placement、relatedTarget包含关系、120ms close。它有 application 入口、新cr无该独立主入口;已选专家 click短路也属legacy。两套都无 safePolygon 文本,几何与生命周期不能互换。
## 验证和剩余状态
本轮仅新增此文档;未改代码/原UI。验证为模块 SHA、Babel AST函数范围、PostCSS精确选中规则+SHA和逐分支阅读。原版主菜单实际当前五项由主任务正在另采,本文不把收到的部分Evidence ID当完整已核验运行时记录。
建议 root REA 场景:单击开/关、hover相邻项、缓慢/快速跨4px缝、搜索聚焦后移开鼠标、非空Esc清搜索/空Esc回父、viewport右边/上方不足翻转、滚动child关闭、外点击及更高浮层点击、touch latch、mode留开/文件选择关全部/connector toggle留开;再比较已选cwd reference-files有结果/无结果两种。父menu及自定义row键盘是否实际聚焦、动画起止画面、每个本地picker取消返回都尚须实测。