fix(frontend): desktop 消息右键菜单死条目收口——pin/unpin/recall 接上已有 mutation、forward/regenerate 按 handler fail-closed、派发器 7 处静默 break 改为一次可感知反馈 (#2154) - #2238
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Team Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
CI 结果(run 33658185803,HEAD
|
| 门禁 | 结果 |
|---|---|
Frontend coverage baseline (@agenthub/workbench) |
✅ pass |
Frontend coverage baseline (agenthub-desktop) |
✅ pass |
Frontend coverage baseline (agenthub-web) / (@agenthub/shared) / (agenthub-mobile-rn) |
✅ pass |
Frontend (web) |
✅ pass |
frontend-desktop (1) / frontend-desktop (2) |
✅ pass |
Native Windows frontend (agenthub-desktop) / (agenthub-web) / windows-frontend |
✅ pass |
ui-required / validate / backend-required / go-hub / go-edge / windows-go |
✅ pass |
Visual QA shell (web) / Visual QA Desktop shell / Web stubbed-hub E2E |
✅ pass |
(coverage 阈值由 CI 判定这条我本地无法证,现在有了:workbench + desktop 两个包的 coverage baseline 都过。)
唯一红:Vuln scan (pnpm audit prod+full) — 依赖面,非本 PR 引入
- 失败信息(job 100341979789 annotation):
pnpm audit (scope: prod) found vulnerable package(s) not in exception registry: GHSA-6gmq-8vp8-gcm6 — failing build - 该 advisory:@xmldom/xmldom,
XML fragment injection via invalid EntityReference.nodeName during requireWellFormed serialization,severity medium,published 2026-09-02T15:18:20Z(就在今天),patched 版本0.8.15/0.9.12(xmldom <= 0.6.0无修复)。 - 与本 PR 无关的证据:① 本 PR diff 只有 8 个 FE 源码/测试文件,没有碰
package.json/pnpm-lock.yaml/ overrides / 例外登记;② 该 job 是 path-filtered —— 在 based1dc97fd与最新 masterdc7df53d上它的结论都是 skipped(master 自身不带 FE 路径改动就不跑),所以这是一条新 advisory 打进 master 依赖树后的潜伏红,任何碰app/**的 PR 都会踩到。 - 按
scripts/verify/vulnerability-exceptions.json的政策("仅允许登记上游无修复版本的漏洞;有修复版本的必须通过 overrides 升级解决,禁止登记"),这条不能登记例外,需要一条fix(deps)lane 把@xmldom/xmldom提到>=0.8.15/>=0.9.12(形态同 fix(deps): postcss-selector-parser>=7.1.3 + brace-expansion@1>=1.1.18 override 升级,全量 audit 5→2 (#2154) #2224 的 postcss-selector-parser override)。不在本 lane 写集内,未动。
base 说明
push 前已按排程指令 git fetch origin && git rebase origin/master(base d1dc97fd,零冲突)。之后 master 又前进到 dc7df53d(#2236),git diff --stat d1dc97fd..dc7df53d 全是 edge-server/** + pkg/safego/**,与本 PR 写集零相交 ⇒ 没有再次 rebase/force-push,避免白跑一轮 CI。合并时机与是否再 rebase 由主机侧决定。
…6,prod audit 4→2 / 全量 8→2 (#2154) - GHSA-6gmq-8vp8-gcm6 / CVE-2026-83610(medium,xmldom requireWellFormed 序列化时无效 EntityReference.nodeName 造成 XML 片段注入,published 2026-09-02T15:18Z):影响 >=0.7.0 <=0.8.14 与 >=0.9.0 <=0.9.11,lockfile 实锁 0.8.13(@expo/plist@0.7.0、mammoth@1.12.0)与 0.9.10(plist@3.1.1),两域均低于 patched → 新增两条**版本域限定** override `"@xmldom/xmldom@0.8": ">=0.8.15 <0.9"` 与 `"@xmldom/xmldom@0.9": ">=0.9.12 <0.10"`,各自重解到 0.8.15 / 0.9.12。不跨 minor 强推:0.8.15 满足上游声明 ^0.8.8(@expo/plist)与 ^0.8.6(mammoth),0.9.12 满足 ^0.9.10(plist),三者 engines(node>=10.0.0 / >=14.6)与零依赖面均未变 - 同批扫出 GHSA-5jgf-p345-68v8 / GHSA-f65p-4m7j-42xc / GHSA-fph4-wmhf-6fwf / GHSA-jqff-g426-hqxp(**high** ×4,fast-uri host 混淆 / IPv6 与百分号解码 SSRF,published 2026-09-02T15:41-15:44Z):既有 override `">=3.1.5 <4"` 钉在已过期下限,lockfile 实锁 3.1.5 中 4 枚 high(路径 .>stylelint>table>ajv>fast-uri)→ 提到 `">=3.1.6 <4"`,重解到 3.1.7。CI 的 prod 步先 exit 1,全量步从未跑到,所以这 4 条在 #2238 的红灯里完全不可见——只修 xmldom 会把红从 prod 步搬到 full 步 - overrides SSOT 在 app/pnpm-workspace.yaml(#1713,不在任何 package.json —— app/package.json 的 `pnpm` 字段只有 hoistedDependencies,先核过 #2224 的实际落点);按 d7c326c 同式修改,写集只有这两个文件,未动任何 package.json;lockfile 仅 30 行变动,只动 3 个包(0.8.13→0.8.15 / 0.9.10→0.9.12 / 3.1.5→3.1.7),无其他包版本漂移 - 实测(直连 registry.npmjs.org,本机默认华为云镜像不支持 audit POST):`pnpm audit --prod` 通告数 4 → 2,全量 `pnpm audit` 8 → 2;剩余 2 条即 #1663 已登记的 image-size 不可修项(GHSA-w3rx-r6r6-pgpr / GHSA-5p2g-fcmc-qvqq,patched=<0.0.0),scripts/verify/vulnerability-exceptions.json **未触碰**(按政策有修复版本者禁止登记,故本次全部走 override 升级) - 门禁判定复现:verify-vulnerability-gates.sh pnpm-audit prod/full 修复前均 exit 1(prod 点名 GHSA-6gmq-8vp8-gcm6;full 点名 GHSA-6gmq-8vp8-gcm6 + 4 枚 fast-uri GHSA),修复后均 exit 0 且降级为 warning(tracked);自检 verify-vulnerability-gates.Tests.sh 23 passed / 0 failed - `pnpm install --frozen-lockfile --lockfile-only` 一致性通过;三个新版本 lock 里的 integrity 与 registry.npmjs.org `dist.integrity` 逐字节一致;--lockfile-only 未生成 node_modules,无多余副本 - 未跑(本切片不改任何源码,交 CI 前端 job):vitest / coverage / tsc / docker Co-authored-by: DeliciousBuding <DeliciousBuding@users.noreply.github.com> Co-authored-by: Cursor <cursor@vectorcontrol.tech>
…6,prod audit 4→2 / 全量 8→2 (#2154) (#2154)(#2240) - GHSA-6gmq-8vp8-gcm6 / CVE-2026-83610(medium,xmldom requireWellFormed 序列化时无效 EntityReference.nodeName 造成 XML 片段注入,published 2026-09-02T15:18Z):影响 >=0.7.0 <=0.8.14 与 >=0.9.0 <=0.9.11,lockfile 实锁 0.8.13(@expo/plist@0.7.0、mammoth@1.12.0)与 0.9.10(plist@3.1.1),两域均低于 patched → 新增两条**版本域限定** override `"@xmldom/xmldom@0.8": ">=0.8.15 <0.9"` 与 `"@xmldom/xmldom@0.9": ">=0.9.12 <0.10"`,各自重解到 0.8.15 / 0.9.12。不跨 minor 强推:0.8.15 满足上游声明 ^0.8.8(@expo/plist)与 ^0.8.6(mammoth),0.9.12 满足 ^0.9.10(plist),三者 engines(node>=10.0.0 / >=14.6)与零依赖面均未变 - 同批扫出 GHSA-5jgf-p345-68v8 / GHSA-f65p-4m7j-42xc / GHSA-fph4-wmhf-6fwf / GHSA-jqff-g426-hqxp(**high** ×4,fast-uri host 混淆 / IPv6 与百分号解码 SSRF,published 2026-09-02T15:41-15:44Z):既有 override `">=3.1.5 <4"` 钉在已过期下限,lockfile 实锁 3.1.5 中 4 枚 high(路径 .>stylelint>table>ajv>fast-uri)→ 提到 `">=3.1.6 <4"`,重解到 3.1.7。CI 的 prod 步先 exit 1,全量步从未跑到,所以这 4 条在 #2238 的红灯里完全不可见——只修 xmldom 会把红从 prod 步搬到 full 步 - overrides SSOT 在 app/pnpm-workspace.yaml(#1713,不在任何 package.json —— app/package.json 的 `pnpm` 字段只有 hoistedDependencies,先核过 #2224 的实际落点);按 d7c326c 同式修改,写集只有这两个文件,未动任何 package.json;lockfile 仅 30 行变动,只动 3 个包(0.8.13→0.8.15 / 0.9.10→0.9.12 / 3.1.5→3.1.7),无其他包版本漂移 - 实测(直连 registry.npmjs.org,本机默认华为云镜像不支持 audit POST):`pnpm audit --prod` 通告数 4 → 2,全量 `pnpm audit` 8 → 2;剩余 2 条即 #1663 已登记的 image-size 不可修项(GHSA-w3rx-r6r6-pgpr / GHSA-5p2g-fcmc-qvqq,patched=<0.0.0),scripts/verify/vulnerability-exceptions.json **未触碰**(按政策有修复版本者禁止登记,故本次全部走 override 升级) - 门禁判定复现:verify-vulnerability-gates.sh pnpm-audit prod/full 修复前均 exit 1(prod 点名 GHSA-6gmq-8vp8-gcm6;full 点名 GHSA-6gmq-8vp8-gcm6 + 4 枚 fast-uri GHSA),修复后均 exit 0 且降级为 warning(tracked);自检 verify-vulnerability-gates.Tests.sh 23 passed / 0 failed - `pnpm install --frozen-lockfile --lockfile-only` 一致性通过;三个新版本 lock 里的 integrity 与 registry.npmjs.org `dist.integrity` 逐字节一致;--lockfile-only 未生成 node_modules,无多余副本 - 未跑(本切片不改任何源码,交 CI 前端 job):vitest / coverage / tsc / docker Co-authored-by: DeliciousBuding <DeliciousBuding@users.noreply.github.com> Co-authored-by: Cursor <cursor@vectorcontrol.tech>
Co-authored-by: Cursor <cursor@vectorcontrol.tech>
…bleAction 不再静默 break (#2154) Co-authored-by: Cursor <cursor@vectorcontrol.tech>
…port——写集外 2 行,旧夹具正是被修掉的假事实 (#2154) Co-authored-by: Cursor <cursor@vectorcontrol.tech>
…s——菜单条目从点了没反应变成真派发 (#2154) Co-authored-by: Cursor <cursor@vectorcontrol.tech>
…ell 不再渲染转发条目(写集外) (#2154) Co-authored-by: Cursor <cursor@vectorcontrol.tech>
… t 分支 (#2154) Co-authored-by: Cursor <cursor@vectorcontrol.tech>
094ba8a to
321992c
Compare
主机侧合并前补记:第二次 rebase(
|
…tionUnavailable zh/en 键落地(未接入动作不再谎称「请重试」) + 消息动作 port 假注释订正 (#2241) round-64 wave 批 2(tracker #2241),lane B。批 1(#2242)已合入 master `4a7af8c6`;本分支已 rebase 到该 master,无冲突(批 1 只碰 Go 与 `api/openapi.yaml`,本批只碰 `app/**`)。 原 commit 粒度(均带 (#2241)): - fix(i18n): 补 toast.actionUnavailable zh/en 键,未接入动作的提示不再说"请重试" - docs(workbench): 订正消息动作 port 的 doc 注释——#2238 之后它已是假话 - feat(desktop): 接上 Hub 转发 mutation,desktop 右键"转发"条目重新渲染并真派发 三件事的因果链:`workbenchTranscriptChromeActionMappers.ts:545` 早已导出 `UNAVAILABLE_ACTION_TOAST_KEY = 'toast.actionUnavailable'` 并在 7 个调用点使用,但 chatview bundle 里没有这个键 ⇒ i18next 回落判据 `resolved !== KEY` 不成立 ⇒ `announceUnavailableAction` 落到 `effect.failureMessage`,用户看到「…失败,请重试」这句对「这个端根本没接线」的错误指导(#2238 登记的尾巴 ①)。补键后,其中 desktop 的 forward 不再属于「未接线」——因为本批同时补上了 `useHubForwardMessage` port 并接线(#2238 当初是按 handler 存在性 fail-closed 关掉的,缺的正是这个 port,尾巴 ③)。`AgentHubWorkbenchTypes.ts:165-171` 的 doc 注释在 #2238 之后已是假话(尾巴 ②),一并订正。 **`regenerate` 明确不接线**:`regenerateAgentTask` 在 Desktop 的 `DesktopHubTaskBridge` / agent task 语义下未经验证,接了等于把未证语义推给用户 ⇒ 保持 fail-closed,`onRegenerate` / `onAddMessageReaction` 的 undefined 断言原样保留。 两处需要 reviewer 知情的取舍: 1. 删了一条既有断言 `expect(props.onForwardMessage).toBeUndefined()`——它的前提写在它自己的注释里("forward needs a `useHubForwardMessage` port"),本批交付物正是补上该 hook,接线后该断言与事实相反。只删这一条;同 test 内 `onRegenerate`/`onAddMessageReaction` 两条 fail-closed 断言原样保留,另在「chatActions 缺失」test 里新增 `expect(props.onForwardMessage).toBeUndefined()` 保住 #2154 不变量。测试数只增不减(3 → 4)。 2. `App.v4.test.tsx` 加了 1 行 `vi.mock` 夹具导出(白名单式 mock 不提供新 hook 会直接红)——**该文件任何断言一字未改**;抽掉这 1 行实测 12+ 用例全红,补回 14 passed。 证据与未验证项详见 PR #2249 描述(含红→绿双向证据、主机侧 rebase 后独立复跑 desktop 18 passed / workbench 5 passed、3 个 i18n 门禁数值、tsc×4 = 0 error、eslint 0 新增 problem 且 2 条 pre-existing 经 master blob 探针确认、以及 `real_tested=false` 的明确标注)。
一句话
Desktop 右键菜单里 pin/unpin/recall 是点了没反应(平台层有 mutation 但没转发进 workbench deps),forward/regenerate 是渲染了但根本没有 port;派发器 7 处
if (!handler) break;让这些点击零反馈消失。本 PR:能接的接上(3 个),接不上的按 handler 存在性 fail-closed 不渲染(2 个),并把静默break全部换成一次可感知反馈。1. 锚点核实结论(主机侧 4 条,逐条复核)
hubMessageActions: Boolean(deps.sessionId),判据是"有没有 sessionId"而非"handler 存不存在"AgentHubWorkbenchHelpers.ts:157把props.activeConversationId直接当 sessionId 传下去(注释:#1383 REST message actions: activeConversationId doubles as the session id)。Desktop 在 Hub IM 会话下activeConversationId= hub session id ⇒ 门禁恒真 ⇒ pin/unpin/recall 照渲染。改前 516-517 行注释宣称"Desktop/demo shells get an honest, shorter menu (#1818)",与事实相反(Desktop 有 session id)if (!handler) break;regenerateHandler)/ 654 approval / 667 pin / 680 unpin / 693 forward / 706 recall / 719 react。全部零反馈、零日志/platform/app/desktop/src/platform/useDesktopWorkbenchModel.ts的DesktopChatActions有sendMessage/recallMessage/editMessage/pinMessage/unpinMessage/markRead(479-487 行接useHubRecallMessage/useHubPinMessage/useHubUnpinMessage,hook 在app/desktop/src/api/sessionQueries.ts)。但只有这 3 个能用:desktop api 层没有 forward hook(sharedhubClient.forwardMessage存在,desktop 未包)、desktop 全仓 grep 不到 regenerate、shared hubClient 也没有 addReactiononPinMessage|onUnpinMessage|onRecallMessageonPinMessage/onUnpinMessage/onRecallMessage。改前 desktop App.tsx 只转发onEditMessage(674-682)与onApprovalDecision。onForwardMessage/onRegenerate/onAddMessageReaction同样没有,但属"平台层没有对应 mutation",不是漏转发前提全部成立 ⇒ 按"优先接真 mutation + 其余 fail-closed + 派发器不再静默"执行,没有另造修法。
2. 改了什么
app/workbench/src/workbenchTranscriptChromeActionMappers.tshubMessageActions?: boolean→capabilities?: TranscriptMenuActionCapabilities(pin/unpin/recall/forward/regenerate五个独立布尔,缺省全 false = fail-closed)。pin 与 unpin 分开:条目按block.pinned二选一,只接了一个方向的 shell 不再渲染死的那一半。forward 仍需conversations(选择器是唯一真实转发路径,hub/agentteam: 投影层抽取 — GetTeamRunState 210 行 god function 且在读路径隐藏 DB 写 #1385),recall 仍限author.role === 'human',regenerate 仍限 agent 文本块。UNAVAILABLE_ACTION_TOAST_KEY = 'toast.actionUnavailable'+announceUnavailableAction(),替掉全部 7 处静默break:恰好一次 toast、绝不播报成功文案、绝不产生 softHide/pulse/composer 等假副作用。键未落地时回落到 effect 自带的failureMessage(已本地化),因此既不会静默也不会露出裸键。app/workbench/src/workbenchTranscriptChromeHelpers.tscontextMenuGroups由 handler 存在性算 capabilities:pin/unpin/recall = Boolean(sessionId) && deps.onXxx !== undefined(planner 没有 sessionId 造不出 effect,[P1] 假内容与假交互清理:任务页/上下文菜单/设置项/demo 身份 #1818),forward = deps.onForwardMessage !== undefined,regenerate = deps.onRegenerate !== undefined。sessionId的 doc 注释(原文断言"Absent on Desktop/demo shells",是假的)。app/desktop/src/App.tsxonPinMessage/onUnpinMessage/onRecallMessage到workbench.chatActions.{pinMessage,unpinMessage,recallMessage},沿用既有onEditMessage的hub-message-前缀剥离约定(契约见AgentHubWorkbenchTypes.ts:167:handler 收到的是 raw block id,由 parent 剥前缀);chatActions缺失(demo/Hub 未就绪)时传undefined。app/desktop/src/api/sessionQueries.ts,不在写集)⇒ 靠 fail-closed 让条目消失。测试:
workbenchTranscriptChromeActionMappers.test.ts、workbenchTranscriptChromeHelpers.test.ts、新增app/desktop/src/__tests__/App.messageActions.test.tsx;另有 2 个写集外夹具修正(见 §6.2)。3. 不变量 → 测试映射(全部绿)
renders handler-backed menu entries only when the capability is declared (#2154);helpersomits handler-backed menu entries when no handler is wired, even with a session id (#2154)not.toContain;另覆盖 pin/unpin 半开、recall 作者门、forward 无会话列表renders each wired action and dispatches the click to its handler (#2154);mappersdispatches a declared menu entry to its action string (#2154);desktopforwards the Hub pin/unpin/recall ports with the block-id prefix strippedonClick()→ spy handler 被调用(helpers 层断言onPinMessage('u1','sess-1')等 5 个 port;desktop 层断言chatActions.pinMessage('m1','sess-1'),即前缀已剥)announces every unwired action exactly once instead of dropping it silently (#2154)(7 个 effect 逐个)+announces Hub REST side effects when handlers are not wired+announces approval effects when no decision handler is wired+prefers the dedicated unwired-action copy…+falls back to the effect failure copy when the dispatcher gets no translate functiontoHaveBeenCalledTimes(1)、不是成功文案、且 softHide/pulse/dispatchComposer 均未被调用leaves the ports Desktop cannot back undefined so the menu hides them、withholds every message port when Hub chat actions are unavailableonForwardMessage/onRegenerate/onAddMessageReaction === undefined;chatActions 缺失时 4 个全 undefined4. 红 → 绿证据
红(实现改之前,tree =
bdbf810+ 红测试,提交为b7b2c785)典型红断言:
expected [ 'context.copy', …(8) ] to not include 'context.regenerate'(有 sessionId 无 handler 时条目照样渲染);pin: expected "vi.fn()" to be called 1 times, but got 0 times(派发器静默)。实现落地后又抓出 2 处"旧断言就是那条假事实"的连带红(均在写集外,见 §6.2):
绿(HEAD
094ba8ae,based1dc97fd)5. 门禁表
两轮:rebase 前(HEAD
1a78581,basebdbf810)与 rebase 后(HEAD094ba8ae,based1dc97fd,已 push)。rebase 只带入 hub-server Go 改动(git diff --stat bdbf810..d1dc97fd全是hub-server/**),FE 树 byte-identical;下表全部为 rebase 后 HEAD094ba8ae实跑结果。pnpm --filter @agenthub/workbench exec vitest run src/workbenchTranscriptChromeActionMappers.test.ts src/workbenchTranscriptChromeHelpers.test.ts src/useWorkbenchTranscriptChrome.test.ts src/__tests__/transcript.test.tsx094ba8aepnpm --filter agenthub-desktop exec vitest run src/__tests__/App.messageActions.test.tsx094ba8aepnpm --filter agenthub-web exec vitest run src/App.test.tsx094ba8aepython3 scripts/verify/verify-doc-ssot.pydoc SSOT ok094ba8aegit diff --check origin/master...HEAD094ba8aepython3 scripts/verify/verify-i18n-callsites.py094ba8aepython3 scripts/verify/verify-frontend-package-boundary.py094ba8aepnpm --filter @agenthub/workbench exec tsc --noEmit094ba8aepnpm --filter agenthub-desktop exec tsc --noEmit -p tsconfig.app.json094ba8aepnpm --filter agenthub-desktop exec tsc --noEmit -p tsconfig.json094ba8aepnpm exec eslint <8 个改动文件>094ba8ae按指令未跑:全量 vitest、coverage、
pnpm -r build、全量tsc(CI 是权威)。命令坑记录:desktop 包名是agenthub-desktop不是@agenthub/desktop;pnpm --filter X vitest run会ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT,必须exec vitest run。6. 证据等级 / 未验证项 / 可能错的地方
证据等级
chatActions且剥掉hub-message-前缀。未验证 / 可能错
toast.actionUnavailable(zh 建议"该操作在当前端未接入",en "This action is not wired in this client")。资源面app/shared/src/chatview/i18n/resources.ts不在写集 ⇒ 未加。当前行为:键缺失时回落到该 effect 的failureMessage(如"置顶失败,请重试")——不静默、不假成功,但"请重试"语义不准(该端永远不会成功)。键一落地自动切换到专用文案,无需再改代码。94972decuseWorkbenchTranscriptChrome.test.ts(夹具补onRegenerate/onRecallMessage两行,断言一字未改)、79ecae30__tests__/transcript.test.tsx(菜单条目数 6→5 + "转发"改断言不存在)。理由:这两处旧断言正是本 PR 要消灭的假事实("有 sessionId 就有 handler 条目"/"有 conversations 就有转发条目"),不改则 CI 必红。若主机侧判定越界,请 drop 这两个 commit 并由写集内 lane 重做。AgentHubWorkbenchTypes.ts:165-170的 doc 注释现在是假的(仍写"Desktop/demo shells omit them and pin/unpin/recall/react stay hidden ([P1] 假内容与假交互清理:任务页/上下文菜单/设置项/demo 身份 #1818)")。该文件不在写集 ⇒ 未改,登记为后续 1 行注释修正。app/desktop/src/api/sessionQueries.ts新增useHubForwardMessage(sharedhubClient.forwardMessage已有)+DesktopChatActions扩字段 + App.tsx 转发。api 层不在写集 ⇒ 未做,改为不渲染条目。createHubClient(...).regenerateAgentTask(messageId),desktop 技术上可照抄,但我没有验证 desktop 的 Hub 任务语义下 regenerateAgentTask 是否正确(desktop 另有 DesktopHubTaskBridge/agent task 路径),所以选择 fail-closed 不渲染而不是接一个语义未证的 port。announceUnavailableAction的 t 有/无两路、capabilities ?? {}默认值、5 个 capability 计算)都有对应用例,但包级阈值是否被拉低只有 CI 能判。context.forward|context.pinMessage|context.unpin|context.recall|context.regenerate|转发|置顶|撤回|重新生成)扫过 app/{workbench,web,desktop,shared} 与 e2e,认为没有第二处,但 grep 不是权威。chatActions缺失(Hub 未就绪/demo)时"转发/置顶/撤回"也消失。这是 fail-closed 的直接后果——消失的正是原先点了没反应的条目。workbenchTranscriptChromeActionMappers.ts:5'AppError' is defined but never used(error)、desktop/src/App.tsx:260useMemo missing dependency: 'tIm'(warning)。探针做法:git show origin/master:<file> >临时同目录文件再 eslint,结果与改动后一致;临时文件已删,git status干净。未顺手修(与本 lane 无关,且可能有棘轮基线归属)。.worktrees/fe-ctx-menu单写者作业;worktree 的app/node_modules是软链到主 checkout、各包node_modules是cp -a复制(内部@agenthub/*为相对符号链接,已核实指向 worktree 自己的 workbench/shared,跨包测试确实跑的是本分支代码)。未动其他 worktree,未合并任何分支。7. 剩余 blocker(需主机侧决策,非技术阻塞)
toast.actionUnavailable键由谁落(i18n 资源面 lane)。