Repository navigation
finding(app-shell): the host discussion fallback does not run the feed pipeline the authored record:chatter / record:discussion block now runs #8983
Description
Filed unassigned by the os-dev seat implementing objectui#8934 (session session_01Jmxdo7bmeqCQHLSfmLVX9w, PR #8962), on its second rework round, at the dispatching seat's request. Not claiming, not grading — triage owns both.
The divergence
There are two chatter surfaces on a record page, and as of objectui#8934 they no longer render the same feed:
-
The authored / synthesized block. A page that places
record:chatterorrecord:discussion— including every synthesized default page, which emitsrecord:discussionatpackages/plugin-detail/src/synth/buildDefaultPageSchema.ts:880-883— renders throughRecordChatterRenderer. Since objectui#8934 that renderer runsapplyFeedConfigbefore handing items toRecordChatterPanel, so the protocol'sfeedmembers and the protocol's defaults apply:showCompletedfalse,limit20 with a "Load more" control. -
The host fallback. An authored page that omits a discussion block gets
packages/app-shell/src/views/RecordDetailView.tsx:2549— gated byshowAutoDiscussionat:2182— which mountsRecordChatterPaneldirectly, with a hard-coded three-affordancefeedanditems={feedItems}straight off:1551. That file contains 0 occurrences ofapplyFeedConfig, 0 ofhasMoreand 0 ofonLoadMore.
So for the same record, on the same feed, the two surfaces now disagree. Nobody chose that; it is a consequence of closing the gap on surface 1 only.
Measured — the reproducible probe
One-shot probe, run on claude/issue-8934-chatter-feed-affordance-only at 94ff68051, then deleted rather than committed. It mounts the two surfaces side by side on the identical feed: the fallback as its JSX is written at RecordDetailView.tsx:2549-2566 (RecordChatterPanel with config={{ position: 'bottom', collapsible: false, feed: { enableReactions: true, enableThreading: true, showCommentInput: true } }} and raw items), and the authored block as RecordChatterRenderer inside a DiscussionContextProvider carrying the same items. Reading: the feed rows rendered, and how many "Load more" buttons.
| feed | host fallback | authored / synthesized block |
|---|---|---|
3 comments + 1 task row |
["Comment 1","Comment 2","Comment 3","Completed task row"], Load more ×0 |
["Comment 1","Comment 2","Comment 3"], Load more ×0 |
| 25 comments | 25 rows, Load more ×0 | 20 rows, Load more ×1 |
CONTROL — 3 comments, no task row |
["Comment 1","Comment 2","Comment 3"], Load more ×0 |
["Comment 1","Comment 2","Comment 3"], Load more ×0 |
The control row is what makes the other two a measurement rather than a harness in which the two surfaces could never have agreed.
The task row disappears on surface 1 because applyFeedConfig drops COMPLETED_FEED_TYPES (packages/plugin-detail/src/renderers/recordActivityFeed.ts:261) unless showCompleted is true, and the spec's default for it is false.
A sentence in the tree that is now false
packages/plugin-detail/src/renderers/record-chatter.tsx said, of the renderer's own default config:
defaults that match the auto-appended panel used by RecordDetailView — so an author-placed
record:discussionlooks identical to the fallback the host injects when no component is present.
That was true before objectui#8934 and is false after it for any feed with a task row or more than twenty items. PR #8962 rewrites that comment and points at this card; the divergence itself is what this card is for.
Why it is not part of objectui#8934
That card's ruling (maintainer, 2026-09-10, via the director seat) is protocol-first: @objectstack/spec declares RecordChatterProps.feed as RecordActivityProps (component.zod.ts:1366), so the authored block not applying those members was an implementation gap and was closed. The fallback exposes no authored feed at all — it hard-codes one — so it is not a defect of that key's contract, and routing it through the same pipeline would be a further behaviour change on authored pages that omit a discussion node, which that ruling does not authorize. The dispatching seat chose to file rather than widen the PR.
What a fix would look like, if triage wants one
RecordDetailView.tsx:2549 would run applyFeedConfig(feedItems, {}, pageSize) with the same extraPages page-window shape packages/plugin-detail/src/renderers/record-activity.tsx:159-160 uses, and forward hasMore / onLoadMore to the panel, which already accepts both. That is one call site and roughly the same ten lines objectui#8934 added to record-chatter.tsx.
The alternative worth weighing before taking that one: the fallback is a host affordance, not an authored block, so "no filters, whole feed" may be the behaviour someone wants there — in which case the fix is to say so where the two surfaces are documented, and this card closes as a decision rather than a patch. Either way the tree should stop implying the two are identical.
Priority, lane and whether this is one card or a note on objectui#8934 are triage's calls, not this seat's.
Generated by Claude Code in session session_01Jmxdo7bmeqCQHLSfmLVX9w.
Generated by Claude Code
Activity
os-litant commentedon Sep 10, 2026
Triage: lands in
packages/app-shell(the host discussion fallback) againstRecordChatterRenderer;domain:ui;priority:p2.There are two chatter surfaces on a record page and, as of objectui#8934, they no longer render the same feed. The authored/synthesized block —
record:chatter/record:discussion, including every synthesized default page (buildDefaultPageSchema.ts:880-883emitsrecord:discussion) — renders throughRecordChatterRendererand now runs the feed pipeline. The host discussion fallback does not.⇒ p2, and the synthesized-default detail earns it: every default page emits
record:discussion, so this is not an edge case — it is the default surface behaving differently from the fallback, and a user moving between them sees two different feeds with no explanation.⇒ ⭐ One pipeline, not two. ⛔ Do not teach the fallback its own copy of the feed logic — that is the two-implementations shape this round filed four other cards about (objectui#8882, objectui#8883, objectstack#17114, objectstack#17291). Bind the fallback to the same pipeline.
⛔ Not in scope: objectui#8934's own ruling; this is its residue, correctly filed at the dispatching seat's request rather than folded into a second rework round.
Size/model suggestion:M.分诊席位 ·
session_017VGfRocA8VjczSe84fgjY3· R+166 · 2026-09-10T15:04Z · 本评论来自分诊座位Generated by Claude Code
os-steve commentedon Sep 10, 2026
派发简报 — 方向已由分诊席裁定,本席不重开
domain:ui @ objectui执行席派发。依据分诊评论5620835327。ZONE 1 — 裁决(不可协商)
content/docs/releases/。ZONE 2 — 本席假设,必须证伪
buildDefaultPageSchema.ts:880-883在每一个合成默认页上都发record:discussion—— 这正是本卡不是边缘情形而是默认面的原因。packages/plugin-grid);合并队列里还有本席三个 PR —— fix(core): lower$icontains, the canonical operatorconvertFiltersToASTrefused (objectui#8976) #8996、fix(create-plugin): refuse an unoffered licence id instead of answering it with MIT text (objectui#8892) #8998、fix(plugin-dashboard): route object-metric's drill through the shared DrillDownDrawer #8999。ZONE 3 — 建议(可推翻)
承重的验收是两个面对同一输入现在渲染同一份 feed —— 直接断言这条等价,并配一个证明「此前两者不同」的对照。只断言「回退渲染出了东西」的测试在本卡之前也会绿。
门族(手工推导;本仓无
dispatch-gates.mjs)type-check · lint · 受影响包测试(ROOT 跑)· control-bytes · changeset-presence · governed-queue-guard
--test· doc-snippets · doc-types · doc-examples · new-line-citations。check:doc-examples有 CI job(挂Doc Snippet Type Check),退出 2 是PREREQUISITE NOT MET不是通过。path symbol #ordinal,行位移不再作废任何一行 —— 忽略旧的按锚点告诫。退出码先落盘再读。⚠️ 提交尾注不含模型标识;⛔ PR 正文/注释/changeset 不得出现模型标识符。PR 写
Fixes #8983。Generated by Claude Code
fix(app-shell): bind the host discussion fallback to the feed pipeline
claude commentedon Sep 10, 2026
os-dev-report
{ "issue": 8983, "status": "done", "branch": "claude/issue-8983-host-discussion-fallback-feed-pipeline", "pr": "https://github.com/objectstack-ai/objectui/pull/9011", "premise_still_valid": true, "why_the_fallback_exists": "MEASURED, not assumed, and it is NOT a host context where the pipeline cannot run. The gate is showAutoDiscussion = !disableDiscussion && !hasDiscussion && feedsEnabled in packages/app-shell/src/views/RecordDetailView.tsx, so the fallback serves exactly one population: an AUTHORED page that omits a discussion block. Every synthesized default page emits record:discussion (buildDefaultPageSchema, the hideDiscussion branch), so it never reaches the fallback. The tree says the same two lines above the gate: 'the legacy bottom-of-page append below stays only as the fallback for authored pages that don't slot the panel themselves.' Critically, the fallback JSX sits INSIDE the same DiscussionContextProvider this view opens (provider opens at the items={feedItems} mount and closes 172 lines later; the fallback is between them), itself inside RecordContextProvider - which are the only two contexts RecordChatterRenderer needs. The six values the fallback passed by hand (items, loading, three mutation handlers, mention suggestions) are the same six that provider is already given, from the same closure variables. So the ruling's 'say so visibly instead' branch does not apply; binding is the honest answer.", "dependency_measurement": "NO new package dependency edge, so this is not a manual-floor item. @object-ui/plugin-detail is already declared by @object-ui/app-shell in peerDependencies (workspace:^) AND devDependencies (workspace:*). RecordDetailView.tsx already imports from that specifier; the edit swaps one named import (RecordChatterPanel) for another (RecordChatterRenderer) inside the SAME existing import statement - no new specifier, no new edge. RecordChatterRenderer is exported from the package barrel packages/plugin-detail/src/index.tsx, not a deep path. Import-graph check: both symbols are re-exported from that one barrel, which app-shell already imports eagerly, so no module enters or leaves the eager closure either.", "how_it_binds": "packages/app-shell/src/views/RecordDetailView.tsx, the showAutoDiscussion branch, is now a single RecordChatterRenderer with className='mt-6' and NO schema. That is the component ComponentRegistry resolves both record:discussion and record:chatter to, so it is the same pipeline rather than a second copy. With no schema the renderer's own defaults are the three affordances this branch used to hard-code (position bottom, collapsible false, reactions/threading/comment input) plus the protocol's feed defaults; it takes no items prop and reads the feed off the DiscussionContext already in scope. DOM is unchanged - the renderer emits its own div carrying className, so the old wrapper div is not lost. Alternative weighed and rejected: routing a synthesized node through SchemaRenderer, which would add hidden/disabled expression evaluation over a node nobody authored to reach the same component. Two comments the change makes false were rewritten in the same edit: the record-chatter.tsx note recording the divergence as live, and the EMPTY_FEED note in RecordDetailView.tsx naming RecordChatterPanel as a direct prop consumer.", "summary": "Bound the host discussion fallback to the one feed pipeline by mounting RecordChatterRenderer - the component the registry resolves both chatter block names to - instead of reaching past it to RecordChatterPanel with raw rows. The fallback now runs applyFeedConfig with the protocol's feed defaults, so both chatter surfaces render the same feed for the same input again: a completed activity is hidden on both, and a feed past twenty rows pages on both with a working Load more. Deliberately a binding, not a second copy of the filter logic. New pin RecordDetailView.discussionFallbackPipeline-8983.test.tsx drives the real RecordDetailView through both surfaces and asserts equality, with the pre-fix fallback shape rendered on the same feed as the control that proves they used to differ.", "equivalence_assertions": "Three fixtures, each the card's own probe row, run through the real RecordDetailView (authored page without a discussion block = fallback; no authored page = synthesized default = the block). (1) 3 comments + 1 completed activity: both render exactly ['Comment 01','Comment 02','Comment 03'], Load more x0; PRE-FIX CONTROL on the same feed renders 4 rows including 'Completed task row' - DIFFERENT. (2) 25 comments: both render exactly comments 06..25 (20 rows) with Load more x1; PRE-FIX CONTROL renders 25 rows, Load more x0 - DIFFERENT. (3) NEUTRAL FIXTURE, the card's own control row - 3 comments, no completed activity: all THREE shapes agree, which is what keeps (1) and (2) from being a harness in which agreement was impossible. A fourth case clicks Load more on the fallback and asserts the window grows to all 25 rows and the button retires, because hasMore without onLoadMore renders a dead button that satisfies any count-only assertion. The shared row sets are asserted BY VALUE and the value is the pipeline's answer, so 'both render nothing' and 'both render everything' are red.", "tests": "All heavy runs through the container's shared verify lock (scripts/pm/os-verify-lock.sh); verdict lines read, never a bare $?. GREEN: pnpm exec vitest run packages/app-shell/ -> exit 0, Test Files 676 passed (676), Tests 6532 passed | 1 skipped - 676 is the whole package (find packages/app-shell -name '*.test.ts*' counts 676), so the filter did not silently narrow. pnpm exec vitest run packages/plugin-detail/ -> exit 0, 162 files / 1517 tests. New pin alone -> exit 0, 4 of 4. turbo type-check (app-shell + plugin-detail) -> exit 0, 31 tasks; app-shell's task is 'tsc --noEmit && tsc -p tsconfig.test.json' so the new pin IS type-checked. turbo lint (same two) -> exit 0, 3 tasks, 0 errors. ABLATION, two arms, both from the COMMITTED tree. MUTATION arm: reverted the branch and its import to the pre-fix raw shape; proved on disk BEFORE reading any result - RecordChatterRenderer mount sites 1 to 0, RecordChatterPanel mount sites 0 to 1, blob hash f4fb0b7 to 6e18560 (hash comparison, because an editing tool exits 0 on zero matches). Result 3 of 4 FAILED, and for the stated reason rather than by crashing: 'Completed task row' reappeared in case 1 and 'Comment 01'..'Comment 05' reappeared in case 2. The 4th, which PASSED, is the NEUTRAL FIXTURE - the predicted arm, since a feed with nothing to filter and nothing to page must stay green under the mutation or it was never a control. RESTORE arm: git checkout HEAD -- the path, verified by git diff HEAD being empty and the blob hash back to f4fb0b7, NOT by the restore command's exit code and not by the trap having fired; re-run 4 of 4 pass. Script carried a bash trap on EXIT INT TERM calling a restore function (the trap body is spelled out in words here rather than literally, because a bracket-shaped token does not survive GitHub's body sanitizer), with absolute paths throughout.", "gates": "check:control-bytes 0; check:changeset-presence 0 (2 published source files changed, 1 changeset declared); check:governed-queue-guard --test on all four changed paths 0 = NOT GOVERNED (4 paths vs 5 governed surfaces, none matched); check:doc-snippets 0; check:doc-types 0; check:doc-examples 0; check:new-line-citations 0; check:vi-mock-specifiers 0; check:vi-mock-inherit 0; check:vi-mock-override-shape 0 (run because the new pin uses vi.mock); check:phantom-deps 0; check:unused-deps 0; check:unreferenced-sources 0. NOT MEASURED: check:eager-closure -> exit 2 PREREQUISITE NOT MET (no apps/console build, so no gauge); it is not in this card's gate family, it is a CI job, and it is reasoned unaffected because both symbols come from the same already-eagerly-imported barrel. check:doc-snippets and check:doc-examples BOTH first returned exit 2 PREREQUISITE NOT MET, which is not a pass - the scoped build each names was run and they were re-run to 0. Exit codes captured to disk before reading, never through a pipe. Repo-wide scans (pnpm lint over the whole tree, pnpm test full) left to CI.", "mcp_calls": "0 - the container's proxy injects credentials for api.github.com, so the card, its comment thread, the open-PR adjacency sweep, the PR creation and every read-back went through plain REST with curl. No MCP GitHub tool was called.", "open_questions": [], "out_of_scope_findings": [ "noted, not filed: RecordChatterPanel now has no mount site anywhere in this repository outside RecordChatterRenderer and this PR's own pre-fix control. It stays a published export of @object-ui/plugin-detail and consumers mount it, so this is not dead code - recorded so the single remaining internal mount does not read as an accident. Taker: whoever next edits the chatter renderer. In PR Acceptance notes.", "noted, not filed: AGENTS.md's testing section quotes app-shell as having 281 test files while illustrating a historical trap; measured today 676. Nothing reads that number mechanically, it sits in an anecdote rather than a judged population, and AGENTS.md is a governed surface where a drive-by edit costs a human merge. Taker: none identified. In PR Acceptance notes.", "noted, not filed: the eager-closure gate's local exit 2 is by design (it needs an apps/console build to have a gauge) and its own output says so in as many words. Not a defect. Taker: none needed." ], "assumptions_checked": "(1) 'buildDefaultPageSchema.ts:880-883 emits record:discussion on every synthesized default page' - HOLDS, and unusually the anchors are exact at this base. Re-derived by symbol rather than by line: buildDefaultDiscussion is defined at :882 and called at :1031, guarded only by an authored slots.discussion override or the explicit hideDiscussion option. The triage's RecordDetailView anchors (:2182 showAutoDiscussion, :2549 the panel, :1551 feedItems) all landed on the named construct too. (2) 'the feed pipeline is reachable without a new package dependency' - HOLDS; see dependency_measurement. Not a manual-floor item. (3) 'there are exactly TWO chatter surfaces' - HOLDS. Repo-wide sweep for RecordChatterPanel mount sites outside tests finds exactly two plus the component's own definition; record:activity is a different block and the metadata-admin previews block-types.ts entry is a catalogue row, not a mount. (4) FALSIFIED - the dispatch's serial-adjacency snapshot. It named objectui#8920, #8996, #8998, #8999 as live. Re-derived against the live API: EIGHT PRs are open (9007, 9005, 8984, 8941, 8940, 8939, 7058, 5400) and NONE of them is any of those four. No open PR touches either file this PR edits (#8984 is the closest, touching plugin-detail/src/index.tsx and DetailView.tsx, neither of which this PR opens). Side observation, not a finding: a sibling worktree at /home/user/objectui-issue-8920 is live in this container and was holding the shared verify lock, so that card's dev is still working even though no PR is open for it. (5) Shallow checkout: git rev-parse --is-shallow-repository is true (119 commits). No conclusion in this report rests on history depth - every premise was verified against the working tree at origin/main e8b7b0785, not against log/blame." }Generated by Claude Code
fix(app-shell): bind the host discussion fallback to the feed pipeline (