Repository navigation
fix(app-shell,plugin-detail): the record page counts each comment's replies on read, and a comment with none offers the reply input (objectui#12144) - #12147
Merged
objectstack-fleet[bot] merged 4 commits intoOct 11, 2026
Conversation
… on read (objectui#12144) Red on the base: through the synthesized default record page, a stored reply shows nowhere and no comment read from the server offers the reply input. The threading-off control is green. Claude-Session: https://claude.ai/code/session_016djJF12Qt14ejKR5Vjt4sK Co-authored-by: Claude <noreply@anthropic.com>
…eplies on read, and a comment with none offers the reply input (objectui#12144) The record page mapped each sys_comment row to a feed item with its parentId and never a replyCount, while the timeline takes every reply out of the top-level list and draws a comment's replies, and the reply input with them, only under a replyCount above zero. So a stored reply showed nowhere, and no comment read from the server offered the reply input. - RecordDetailView counts each comment's children in the rows its sys_comment read returned and sets replyCount beside parentId, as the sys_comment object definition prescribes (count parent_id children at read time). - RecordActivityTimeline offers the reply input on every comment row the host hands onAddReply for, replies or not, and on no other row. The reply list keeps its one replyCount gate. Claude-Session: https://claude.ai/code/session_016djJF12Qt14ejKR5Vjt4sK Co-authored-by: Claude <noreply@anthropic.com>
…ctui#12144) Claude-Session: https://claude.ai/code/session_016djJF12Qt14ejKR5Vjt4sK Co-authored-by: Claude <noreply@anthropic.com>
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…rd page (objectui#12144) The header of RecordDetailView.commentWriteFailure-10899.test.tsx said the reply writer's input was not reachable from a loaded feed, because no read set replyCount. The record page now offers the reply input on every comment it reads, so the header says that, and the file pins the reply writer the way it pins the comment writer: a rejected sys_comment create adds no reply, raises a localized error and keeps the draft. Claude-Session: https://claude.ai/code/session_016djJF12Qt14ejKR5Vjt4sK Co-authored-by: Claude <noreply@anthropic.com>
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
objectstack-fleet
Bot
deleted the
claude/issue-12144-reply-count-on-read
branch
October 11, 2026 16:50
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #12144
Clause-②: no
On the record page's discussion, a reply stored on the server now shows under its comment, and every comment read from the server offers the reply input wherever the caller may post one.
Measured first, on the base (
158c75d)The pin
RecordDetailView.replyCountOnRead-12144.test.tsx(@object-ui/app-shell) rendersRecordDetailViewwith no authored page.buildDefaultPageSchemathen composesrecord:discussion, whose renderer (record-chatter.tsx) turnsfeed.enableThreadingon. A fake data source answers the record'ssys_commentandsys_activityreads, and the test drives the real panel. Run on commit7a19eaa, which adds only the pin to the base:Tests 6 failed | 1 passed (7). The one green case is the threading-off control.Unable to find an accessible element with the role "button" and name /1 reply/).Unable to find an element with the placeholder text of: Reply…). So the first reply on a comment cannot be posted from the page.Cause, read on the base:
sys_commentread mapsparentId: c.parent_idand never setsreplyCount. OnlyhandleAddReplybumpsreplyCount, and it runs only after a reply is posted through the input.RecordActivityTimelinetakes every item with aparentIdout of the root list. It drewThreadedReplies, the only place the reply list and the reply input render, only whenitem.replyCountis above zero.The fix
@object-ui/app-shellRecordDetailView. In the same step that mapsparentId, thesys_commentread counts each comment's children among the rows it returned and setsreplyCount. This follows the platform's own contract: objectstack'ssys_commentobject definition (plugin-audit,sys-comment.object.ts) retired the storedreply_countand prescribes "countparent_idchildren at read time". The count groups by a truthyparent_idin aMap, the same way the timeline groups replies, so the two cannot disagree.@object-ui/plugin-detailRecordActivityTimeline. The reply input is offered on everycommentrow the host handsonAddReplyfor, with or without replies, and on no other row (module-privatecanTakeReply). The reason matches objectui#11035'scanStoreReaction: a reply is asys_commentwhose parent is a comment. An activity row's id would store a comment under an activity, and no read shows it there. The reply list keeps its onereplyCountgate: a row whosereplyCountis zero is handed no replies. So whether replies show never depends on whether the caller may post one, and no second threading model is added.No prop, export, type member or locale key is added. The two new identifiers,
canTakeReplyandNO_REPLIES, are module-private, so the claim'sClause-②: noholds.Dispatch hypotheses
158c75d, by thegit grepand reads above.sys_commentread and mapping, and the real panel. The page pin above is the measurement. A pin of the timeline alone is kept separate and is not counted as the host measurement.onAddReplyreaches the panel only under thefeedCommentgrant (objectui#12140). With the create grant onsys_comment, every comment offers the input, a zero-reply comment included. With a read-only grant, no row offers it, and the stored reply still shows under its parent.needs_decision.Pins (both new)
packages/app-shell/src/views/RecordDetailView.replyCountOnRead-12144.test.tsx, through the default record page, 7 cases:2 replies;sys_activityrow offers none;parent_idand then shows under it;record:discussionwithfeed.enableThreading: false), the list is unchanged: three rows in read order, no toggle, no input.packages/plugin-detail/src/__tests__/RecordActivityTimeline.zeroReplyInput-12144.test.tsx, the timeline's render condition alone, 6 cases:onAddReplycontrol;replyCountgate in both directions.Ablations (one-shot, not left in the tree)
Each mutation went through
ablation-replace.mjs, which reports the anchor hit count, the replacement count and the blob change. Each was restored with blob equal to HEAD and an emptygit diff HEAD. vitest resolves@object-ui/plugin-detailtosrcthrough the rootvitest.config.mtsalias table, so nodist/is in the path.replyCount: undefined): the page pin goes red on (a), (a2) and (d),3 failed | 4 passed. The input cases stay green, because the input no longer depends on the count.replyCount(the base's condition):6 failed | 7 passedacross both pins, which is the three input cases in each file.2 failed | 11 passed, the two activity-row cases.replyCountgate:1 failed | 12 passed, the timeline's list-gate case.Verification (all on
f7f477a;mainunmoved at158c75dwhen the PR opened)turbo run build --filter='@object-ui/app-shell^...' --filter='@object-ui/plugin-detail^...' --concurrency=2gaveTasks: 28 successful, 28 total.pnpm --filter @object-ui/plugin-detail --filter @object-ui/app-shell run type-check: both packages echoedtype-checkand printedDone.tsc -p tsconfig.test.json --listFileslists both new pins.pnpm exec vitest run packages/plugin-detail/:Test Files 256 passed | 1 skipped (257),Tests 2491 passed | 8 skipped.pnpm exec vitest run packages/app-shell/:Test Files 1270 passed | 1 skipped (1271),Tests 12306 passed | 9 skipped.node scripts/check-changeset-presence.mjs,pnpm check:changeset-claims,pnpm check:pending-changeset-literals,pnpm check:control-bytes,pnpm check:new-line-citationsandpnpm check:phantom-deps: all exited 0.node scripts/check-governed-queue-guard.mjs --teston the diff answered NOT GOVERNED.eslint --format jsonon the four changed source files read 4 files and 0 errors.RecordDetailView.tsxcarries 109 warnings at both base and head. The config enables no type-aware linting (noparserOptionsineslint.config.js), and no rule undereslint-rules/reads another file, so this diff cannot move the verdict on an untouched file.../objectstackcheckout in this container has nonode_modulesand no builtspec/clidist, so the per-agent backend cannot boot within the foreground budget. The host render above is the required measurement.Acceptance notes
sys_commentread carries no$top, so it is one page of whatever size the server answers by default. A reply whose parent is not in that page counts toward no item. With threading on, it is still shown nowhere, as before. No second fetch was added.record:discussion's window (feed.limit, default 20) takes the last N items of the merged feed, and a reply counts toward it. A reply whose parent is older than the window shows nowhere until "Load more" reaches the parent. That is the paging as it already stands, unchanged here.RecordDetailView.commentWriteFailure-10899.test.tsxsays the reply writer's input "is not reachable from a loaded feed … no read sets that". That is no longer true. It is outside the claim's file surface, so it is left for the seat. The reply writer's failure path is now reachable through the page.content/docs/plugins/plugin-detail.mdx, the "Console Integration" sample still uses camelCasesys_commentfield names (threadId,_id,author). TheThreadedRepliestable listsshowReplyInput's default asfalse, where the code defaults totrue. Neither describes the threading this change touches, so the docs page is unchanged.Dev session:
https://claude.ai/code/session_016djJF12Qt14ejKR5Vjt4sK(domain:ui seat 1 dispatch,os-dev).Generated by Claude Code