顺路发现于 #3205(那个 PR 只修了 `RecordActivityTimeline` 不读 `loading` 的最后一段渲染分支)。 #3205 修完之后,`loading` 这条链在 **`record:activity`** 上是通的:`renderers/record-activity.tsx` 自己算出 `loading`(自取数期间为 true)→ 传给 timeline → timeline 现在渲染 loading 行而不是空态。 但 **chatter/discussion** 那条链上没有信号源。`RecordChatterPanel` 自己的透传是好的(`RecordChatterPanel.tsx` 两处 `loading={loading}`,sidebar 与 inline 各一),问题在它上面一跳: 1. `packages/plugin-detail/src/renderers/record-chatter.tsx` —— 渲染 `RecordChatterPanel` 时只传 `items` / 各 handler,**没有** `loading={discussion?.loading}`。`DiscussionContextValue` 明明声明了 `loading?: boolean`(`packages/react/src/context/DiscussionContext.tsx`),`record:activity` 也已经在读 `discussion?.loading`。 2. `packages/app-shell/src/views/RecordDetailView.tsx` —— 真正的宿主: - `const [feedItems, setFeedItems] = useState< FeedItem[] >([])`,由两个 `dataSource.find`(`sys_comment` 与 `sys_activity`)异步填充,**期间没有任何 loading state**; - `< DiscussionContextProvider items={feedItems} … >` 不传 `loading`; - 自动追加的那个 `< RecordChatterPanel items={feedItems} … >` 也不传 `loading`。 后果与 #3205 同类:记录详情页打开的头一段时间,讨论面板渲染的是 "No comments yet"(`emptyLabel`),而不是加载态——只是这次缺的不是渲染分支(#3205 已经补上),而是**没人把取数状态算出来**。所以 #3205 的修复在 `record:activity` 上立刻可见,在 chatter 上要等这个 issue。 ## 建议 在 `RecordDetailView` 里为 feed 取数加一个 loading state(两个 `find` 都 settle 之前为 true;注意它们是两条并行的 promise,`.catch(() => {})` 也要收口),然后: - `< DiscussionContextProvider loading={feedLoading} … >`; - 自动追加的 `< RecordChatterPanel loading={feedLoading} … >`; - `renderers/record-chatter.tsx` 补上 `loading={discussion?.loading}`,让作者手工摆放的 `record:chatter` / `record:discussion` 走同一条链。 不要在消费端加宽容兜底(比如 timeline 里猜"items 为空且刚挂载就算 loading")——信号应当由拥有取数的宿主产出,这也是 #3165/#3205 已经确立的形状。 ## 参考 - `packages/app-shell/src/views/RecordDetailView.tsx`(`feedItems` 的 state 与两处 `find`;`DiscussionContextProvider` 与自动追加的 `RecordChatterPanel`) - `packages/plugin-detail/src/renderers/record-chatter.tsx` - `packages/react/src/context/DiscussionContext.tsx`(已声明 `loading`) - `packages/plugin-detail/src/renderers/record-activity.tsx`(已经在读 `discussion?.loading`,可作为参照) - #3205 / #3165
Activity
xuyushun441-sys commentedon Aug 3, 2026
认领:PM 循环第 3 轮
会话:
session_01NVPjPzmmAJ2Ngtvgg5MSRa(objectui 分片 PM,账号xuyushun441-sys,已在 objectstack#4604 登记)分支:
claude/issue-3209-chatter-loading-signalWorktree:
objectui-issue-3209分诊:无需拍板,直接派
本单已经把该定的都定了 —— 信号由拥有取数的宿主产出,这是 #3165 / #3205 已确立的形状,而且原单明确写了不要在消费端加宽容兜底(不许在 timeline 里猜"items 为空且刚挂载就算 loading")。这与本轮已合并的 #3232 是同一条仓规:分歧在生产者收敛,不在消费者用启发式兜住。没有可选方案需要维护者选。
两处容易做错的地方,派发时先钉住
1. 两个
find是并行 promise,收口要收全。 原单已经点出这点。feedLoading必须在两条都 settle 之后才转 false,且.catch(() => {})那条路径也要计入 —— 失败也是 settle,不能让一个 reject 把面板永久钉在 loading 态。这是最容易写出"永远转圈"的地方。2. 三处都要接,少一处就等于没修。 原单列的三个落点是一条链:
RecordDetailView里新增 loading state;<DiscussionContextProvider loading={feedLoading} …>;<RecordChatterPanel loading={feedLoading} …>;plugin-detail/src/renderers/record-chatter.tsx补loading={discussion?.loading},让作者手工摆放的record:chatter/record:discussion走同一条链。record:activity(renderers/record-activity.tsx)已经在读discussion?.loading,照它抄,不要发明第二种写法。验收要求
要有测试钉住"取数期间渲染 loading 而不是
No comments yet",并且 mutation 验证:把loading传递摘掉后测试必须转红。#3205 修的是渲染分支、本单修的是信号源,两者合起来才是完整链路 —— 只断言"panel 收到了 loading prop"不够,要断言用户看到的是加载态而不是空态。Generated by Claude Code
xuyushun441-sys commentedon Aug 3, 2026
PM 验收:ACCEPT — PR #3270,已入合并队列
main、Fixes objectstack-ai/objectui#3209packages/app-shell+packages/plugin-detail+ changesetcontent/docs/releases/零改动我派发时钉的两处易错点,都处理到位了
1. 两个并行 promise 的收口。 用
Promise.allSettled(inFlight),而两条链各自已有.catch(() => {})—— 所以 rejected 也计入 settled。这正是要点:没有部署 audit 插件的环境sys_activity会 404、对象关了enable.feeds时sys_comment会 403,这两种情况必须落到空态而不是永久转圈。changeset 里那句话说得对:「那会把"错说成空"换成"永远转圈",是更糟的 bug」。清理函数里还有cancelled守卫。2. 四个接入点一个不少 —— 新 state、
DiscussionContextProvider、自动追加的RecordChatterPanel、以及record-chatter.tsx的loading={discussion?.loading}。一处超出我要求、而且是对的设计
flag 没做成朴素布尔,而是按
object:recordId键控:理由写在注释里,两条我都认同:
useState(false)+ effect 里setFeedLoading(true)会让 effect 跑之前那一帧先画出空态(一闪而过的 "No comments yet");而切换记录时,朴素布尔会在新 effect 触发前显示上一条记录的已结算状态。派生式写法让一条记录的第一帧就是 loading,且不需要额外的 reset effect。并且它明说这是抄record:activity的canSelfFetch && fetched === null同一套写法 —— 一个惯例,不是两个,正是我要求的。测试断言的是用户看到什么
我要求"不能只断言 panel 收到了 loading prop"。它做到了:全部断言落在
getByTestId('activity-loading')在场 /queryByText('No comments yet')为 null。而且两条投递路径都覆盖了 —— 合成页面(走record:discussion→ DiscussionContext)与作者页面无 discussion 槽(走宿主底部自动追加,没有 context 中转,是独立的一条线)。少测一条,另一条仍然带着 bug 上线。五处独立 mutation 验证,每处只摘一根线:
DiscussionContextProvider的loadingrecord-chatter.tsx的转发loadingallSettled(inFlight)→ 只取第一个allSettled→Promise.all且去掉.catch后两条恰好证明了我担心的两个失效方向都真的被拦住。
needs-user-decisionRecordDetailView在记录切换时从不重置feedItems—— 两个取数回调都是按 id 并入prev,没有任何地方把它设回[],而该视图在记录间不会重新挂载(所有挂载点都没有key)。结果:记录 A 的评论和活动会留在屏幕上,显示在记录 B 下面。 已在本单的测试夹具上复现(rerender 到recordIdOverride="rec-B"后 rec-A 的评论仍在)。它同时掩盖了本单的修复:timeline 的分支是
loading && filtered.length === 0,而残留行让filtered非空,所以记录间导航时 loading 态不显示。我没有让它顺手修,标了
needs-user-decision,因为形态需要维护者定:清空 vs 按objectName:recordId给 feed state 加键,而乐观更新的行(用户刚发的评论)让这个选择有实际后果。Generated by Claude Code
fix(app-shell,plugin-detail): the chatter chain gets a loading signal…
fix(app-shell): key the record feed state by objectName:recordId (obj…