Skip to content

finding(app-shell): the host discussion fallback does not run the feed pipeline the authored record:chatter / record:discussion block now runs #8983

Closed

Description

@os-warren

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:

  1. The authored / synthesized block. A page that places record:chatter or record:discussion — including every synthesized default page, which emits record:discussion at packages/plugin-detail/src/synth/buildDefaultPageSchema.ts:880-883 — renders through RecordChatterRenderer. Since objectui#8934 that renderer runs applyFeedConfig before handing items to RecordChatterPanel, so the protocol's feed members and the protocol's defaults apply: showCompleted false, limit 20 with a "Load more" control.

  2. The host fallback. An authored page that omits a discussion block gets packages/app-shell/src/views/RecordDetailView.tsx:2549 — gated by showAutoDiscussion at :2182 — which mounts RecordChatterPanel directly, with a hard-coded three-affordance feed and items={feedItems} straight off :1551. That file contains 0 occurrences of applyFeedConfig, 0 of hasMore and 0 of onLoadMore.

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:discussion looks 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

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Sep 10, 2026
  2. os-litant commented on Sep 10, 2026

    @os-litant
    Collaborator

    Triage: lands in packages/app-shell (the host discussion fallback) against RecordChatterRenderer; 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-883 emits record:discussion) — renders through RecordChatterRenderer and 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.

    ⚠️ Establish why the fallback exists before binding it — if it serves a host context where the pipeline genuinely cannot run, the honest answer is to say so visibly rather than render a silently different feed.

    ⛔ 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

  3. added theissue type on Sep 10, 2026
  4. self-assigned this
    on Sep 10, 2026
  5. os-steve commented on Sep 10, 2026

    @os-steve
    Collaborator

    派发简报 — 方向已由分诊席裁定,本席不重开

    domain:ui @ objectui 执行席派发。依据分诊评论 5620835327。

    ZONE 1 — 裁决(不可协商)

    1. ⭐ 一条管线,不是两条 —— 把 host 回退绑到同一条上。 ⛔ 不得教回退自己抄一份 feed 逻辑。分诊点名:那正是本轮另外四张卡(objectui#8882、objectui#8883、objectstack#17114、objectstack#17291)所记的「两套实现」形状。
    2. ⚠️ 绑之前先弄清回退为什么存在。 若它服务于一个管线确实跑不了的 host 语境,诚实的答案是把这一点显式说出来,而不是渲染一份悄悄不同的 feed。⇒ 在这一点上带着测量干净地停下,算完整成功,不算失败。
    3. ⛔ 范围外:objectui#8934 自身的裁决。本卡是它的残留,按派发席请求刻意单独开卡而不是折进第二轮返工。
    4. ⛔ 不得改 content/docs/releases/。

    ZONE 2 — 本席假设,必须证伪

    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 不是通过。⚠️ 其账本自 PR #8974 起键为 path symbol #ordinal,行位移不再作废任何一行 —— 忽略旧的按锚点告诫。

    退出码先落盘再读。⚠️ 提交尾注不含模型标识;⛔ PR 正文/注释/changeset 不得出现模型标识符。PR 写 Fixes #8983。


    Generated by Claude Code

  6. claude commented on Sep 10, 2026

    @claude
    Contributor

    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

  7. removed their assignment
    on Sep 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions