17 KiB
当前 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 不包含。
当前组件图与状态
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 规则见 专家使用逻辑核对。
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取消返回都尚须实测。