重构/抽取带引用功能的输入框 #464
Reference in New Issue
Block a user
Delete Branch "refactor/extract-dep-from-ref-inputer"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
为了方便实现粘贴反向解析,
把待选项在使用处注入
picker 附件 作为insert行为
- ProjectChatComponentProps 删除 activeVersionId:它只在策划输入盒的 @ 面板里生效,而策划输入盒的 @ 触发钮早就不渲染,参数从壳传进聊天后无人消费 - WorkspaceLauncher 不再把这个参数透传给 ProjectChat;工作台壳自己那份 activeVersionId 保留,仍供 ProjectDevelopmentView 的游戏运行版本使用 - App 的 AppProps 与解构同步删除该参数 - styles.css 里那条「ResourceReferenceInput 收到 showTriggerButton={!directCodex}」的注释改成当前口径:@ 触发钮在控制排的 .project-chat-reference-trigger,输入区不注入 ResourceReferencePickerAction- skillReferenceProvider 的 loadSkillCatalog 失败分支补 console.warn('[skill-reference] …'),带上是哪一次重试、错在哪,排障时不再只能看到「敲 $ 没反应」 - referenceSourceProviders 用例断言这条日志(并用 vi.restoreAllMocks 收尾),把「失败可重试」与「失败留痕」两件事一起钉住review.txt 处理结果(2026-09-22)
图例:
- [x]= 已处理(含「核对后判定不采纳」);- [ ]= 留给你决定。结论速览:评审 8 项已全部收口——1 / 2(日志部分)/ 3+6 / 4 / 5 已修(各一个提交),2 的冷却建议与 7 / 8 核对后判定不采纳;
另外我在核对时发现评审漏掉的同类问题一处(9),已一并修掉。
需要你点头的只剩三个可选项(见文末「留给你的」,都不是 bug)。
1. ResourceCanvasAssetGenerationPanelView.tsx:269-276 ——
useMemo从不命中缓存referenceAssets = resourceCanvasAssetGenerationReferenceAssets(assets ?? [])是.filter()产物,每次渲染都是新数组;
useMemo(..., [referenceAssets])因此每帧都重建referenceProviders。ResourceReferenceInput里useEffect([editor, providers, refreshReferenceLabels])就会重跑refreshReferenceLabels()并重新注册一次Lexical update listener。画布生成面板每次按键(
onChange → setContent)都会走到这里。28bacfc12。改用assetsSignature(referenceAssets)做依赖,并把「为什么不能依赖数组身份」写进注释。安全性:签名相同 ⇒ 内容相同,provider 闭包持有的旧数组与当前清单内容一致(工厂内部本来就按签名缓存数据),不会读到过期清单。
2. skillReferenceProvider.ts:78-81 —— 目录读取失败被静默吞掉
Promise.all([list_agc_skill_catalog, list_client_extensions])任一失败 →.catch里requestedRef.current = false; setSkills([]),既不打日志也不提示用户。39ac33756(console.warn('[skill-reference] …', error),并补用例断言这条日志)。冷却/节流部分不采纳:那是「改变重试时机」的行为改动,而现有注释明确写了「一次瞬时失败不能让这个输入区此后永远拿不到技能候选」;
在没有真实故障频次数据前加节流,等于用一个猜出来的时间窗换掉可预期的重试语义。要做的话建议给个明确窗口(例如同一轮菜单打开内只重试一次)。
3. skillReferenceProvider.ts:87-88 ——
match在渲染期发起异步读取ProviderMentionMenu用useMemo在渲染阶段调provider.match(query);skill provider 的match里调
ensureCatalog(),首次敲$时发起 Tauri invoke,并在渲染期写requestedRef.current。d5fb34209:ReferenceProvider新增可选onMenuQueryChange?(query)(菜单关闭时收到null),由ProviderMentionMenu在
useEffect里回调;契约里写明match必须是纯函数;match搬进onMenuQueryChange(query !== null时才ensureCatalog()),「第一次敲出
$才读、挂载即查询不产生后端访问」的边界保留;match不触发任何 Tauri invoke、菜单关闭(null)也不触发;4. useDirectProjectChatController.ts:270 —— 并发导入可以突破 8 个附件上限
uploadFiles(files, draftAttachmentCount)用宿主在导入开始时读到的快照算预算,控制器自己不记「在飞的条数」;ComposerAttachmentMenu在导入中不禁用,宿主对返回结果无条件insertReferences。setAttachments(current => [...current, ...next].slice(0, MAX)),硬上限成立;改成「导入成功即入正文」后再没有这一道收口,两次并发各带 5 个文件就能插进去 10 个芯片。
3fdde6f51。控制器加attachmentsPendingCountRef:已放行、还没落成芯片的条数先在预算里占名额,放行的部分在
finally还回去。语义取舍:预算不够时拒绝这次(沿用既有「最多同时携带 8 个附件,请先移除已有附件」文案),而不是「先收下再在插入时砍掉」——后者会让「已上传 N 个文件」的提示与实际芯片数对不上。
同时补了
tests/chatComposerAttachmentCap.test.tsx(两次并发各 5 个 → 放行 5 + 3),并确认这条用例在没修之前是红的。5. ResourceReferenceInput.tsx:123-128 —— 候选菜单的 key 用了显示 token
ReferenceMentionOption用super(chatReferenceMentionToken(reference)),即@显示名/$名称当 key。git show HEAD~4查了重构前的实现:资源是
super(reference.resourceId)、Skill 是super(skill.name),都是唯一身份。现在
characters/hero.png与enemies/hero.png都展开成@hero,同一候选列表里会出现重复 key(Lexical 的
MenuOption契约本身写着 key 必须唯一),React 复用 DOM 会让键盘高亮/选中落到另一条候选上。fbfc7e5e6。导出resourceReferences.chatReferenceKey(只认稳定身份、不含显示名)复用它做 key,显示 token 只留给
label;referenceSourceProviders用例补一批同名素材,断言「显示 token 相同、身份键不同」。说明:DOM 级守护(重复 key 告警)没做成用例——jsdom 里候选浮层需要
ResizeObserver与Range.getBoundingClientRect两个垫片才能渲染,目前仓里也没有任何用例渲染过带候选的
@浮层。要的话我补垫片再加一条。6. ResourceReferenceInput.tsx:796-801 —— 与第 3 项同源,已随 A 方案一起修
match的问题已由onMenuQueryChange收口;match现在是纯函数,ProviderMentionMenu只剩「把 query 变化回调出去」与「用已就绪的数据生成候选」两件事。提交同d5fb34209。7. ResourceReferenceInput.tsx:339-342 —— 渲染期写
providersRef/submitSuppressedRefprovidersRef.current = providers; submitSuppressedRef.current = submitSuppressed;写在渲染体里。写入的是 props 派生值、幂等;改成
useEffect同步会在「commit 完但 effect 还没跑」的窗口里留下过期值,而读它的既有 Lexical 命令回调与
useImperativeHandle句柄都不是 effect。React 19.2.4 确实有useEffectEvent,但它的语义是「只能在 effect 里调用」,用在这里不成立。
8. ResourceReferencePicker.tsx:159-162 —— 渲染期写
onOpenChangeRef/onInsertRefopen()由宿主经命令式句柄调用,回调必须是调用那一刻的最新值,用 effect 同步只会引入过期窗口。
9. 【评审没提,我核对第 1 项时发现的同类问题】PlanningChatView 与 DirectProjectComposer 的 provider memo 也从不命中
PlanningChatView的chatProjectAssets由工作台壳(App.tsx的manifest.assets.filter(...))每次渲染重新派生;DirectProjectComposer的assets由useDirectProjectManifest每次渲染重新.filter()派生。两处useMemo依赖同样是数组身份。aa973ea34。两处都改成按assetsSignature(...)记忆。第 2 项的冷却/节流:要不要给 Skill 目录失败加重试节流窗口(会改重试时机,见第 2 项)。
第 5 项的可选补强:jsdom 垫片(
ResizeObserver+Range.getBoundingClientRect)之后补一条「候选浮层无重复 key 告警」的用例。文档可选微调:功能说明里「单次上限按草稿里已有的附件芯片数计算」可以补一句「并发导入时已放行的条数也先占名额」,
我这次没动文档(避免为一句实现细节再开提交),要的话我补。