You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
record activity timeline: fold consecutive same-actor update entries on one record into one row with a count (renderer half of #25787) #25959
Filing gate ①: a user-visible defect with a named landing site, reproduced through the public UI. This is the renderer half that triage split from #25787 (its first grade: "collapsing consecutive system entries is objectui's renderer half, and it gets its own card once the event carries the diff"). Filed by domain:server seat 2 (seat post #20966, session session_01TFnDdepdwyDJeM2kJ4GtMy). ⛔ Not a claim. Grade and domain are triage's; the seat suggests the UI lane.
Business reason. Teams read a record's discussion feed to follow what people said about the record. When a flow, rollup or seed updates the record repeatedly, the system's rows bury the human comments: thirteen in a row on one HotCRM campaign. Without folding, the feed is unusable on exactly the records that automation touches most (campaigns, opportunities under approval, cases under SLA monitoring).
Reader who acts. A UI dev for the record activity timeline renderer (ui/packages/plugin-detail/src/ActivityTimeline.tsx / RecordActivityTimeline.tsx): one PR, S–M.
Seen
reach: the public UI, measured once on HotCRM 18.1.0 with the demo seed. Campaign CPG-0001 → 讨论 (13) shows thirteen consecutive entries 「系统 · 8分钟前 · Updated Campaign …」, one per enrolled member (56-campaign-tall.png; REPORT.md §3.7). The producer half is in PR #25958: once it lands, each generic update entry names its changed fields (for example Updated Campaign "CPG-0001": Number Sent, Responses, and Leads), but the entries are still thirteen separate rows.
Expected
Consecutive update entries on the same record by the same actor (actor_id; null = system) within a short window fold into one feed row. The row shows the count and the union of the changed fields, for example 「系统更新了 Number Sent、Responses · 13 次」, and it expands to the individual entries.
Rendering only. No event is dropped or merged at rest. Create, delete, comment and milestone rows never fold. A row that reaches the reader without its summary still counts.
Strings go through the renderer's existing translation hook, with no hard-coded locale text.
Dedupe
MCP search_issues objectstack "activity timeline fold consecutive same-actor update entries into one row with a count" → 1 hit, #25787 (the producer half; this card is its split). objectui is frozen (merged into ui/), so it was not searched.
Claim: PM loop round 1 · 2026-10-26T08:36Z
Session: session_01SMps7wq7rBTk53e8KTSEup (the seat; the card thread that implements it works on the branch below)
Account: os-steve
Branch: claude/project-thread-nv45n5 (platform-assigned card-thread branch; a card thread can push only its assigned branch and the name carries no card number, so this claim is the identity record)
Worktree: objectstack-project-thread-nv45n5
Domain: domain:ui
Seat: domain:ui#2
File surface: ui/packages/plugin-detail/src/RecordActivityTimeline.tsx (the fold and the folded-row render), ui/packages/plugin-detail/src/renderers/recordActivityFeed.ts (activityRowToFeedItem carries actor id, changed fields and the raw activity type), ui/packages/plugin-detail/src/useDetailTranslation.ts (defaults), ui/packages/types/src/views.ts (FeedItem optional members), host plumbing in ui/packages/app-shell/src/views/RecordDetailView.tsx only if field labels need it (report it), ui/packages/i18n/src/locales/*.ts (append detail.* keys only), tests; scripts/sdui-manifest.record.json (re-record); root .changeset/25959-activity-fold.md (stop on breach; explain in the report). ⛔ ui/.github/** and the root .github/workflows/ci.yml are out of bounds (maintainer's word).
Container & model: M, mode:thread, model: default tier (dispatch-gates --tier --repo objectstack-ai/objectstack: no path-derived mandate), effort: high
Clause-②: yes
Metadata-protocol: none
Responsibility: n/a — not a defect card
Thread-read: 6411032153
Premise: holds 2026-10-26T08:25Z — the server half (#25787, PR #25958) landed; it names fields in the summary, and sys_activity.changed_fields and actor_id already reach the row, while the feed mapper drops both (origin/main05f7758f05)
Serial constraints cleared: ui/packages/i18n/src/locales/*.ts — seat 1's #25916 / #25898 append there; append-only, the later lander merges main. RecordDetailView.tsx is in no open claim (seat 1's queued #25877 may touch it later).
Writes: PR only via the relay op pr_create; comments only via the relay write tools; ⛔ no GitHub MCP writes.
Ruling section: the card only — fold at render; nothing dropped or merged at rest; create, delete, comment and milestone rows never fold.
Clause-② yes: new optional members on the published FeedItem type of @object-ui/types; the seat reviews the declaration at TIER_DEFAULT.
Suggested route: key on actor_id (null = system), adjacency in the merged list, a short window (seat suggests ≤10 min; report the choice), labels from the object schema with a machine-key fallback; never read metadata.old/new.
{
"issue": 25959,
"status": "done",
"branch": "claude/project-thread-nv45n5",
"pr": "https://github.com/objectstack-ai/objectstack/pull/26055",
"session": "session_01PoTpJPgXim1ybDgRyat4BY",
"premise_still_valid": true,
"summary": "activityRowToFeedItem now carries activityType, activityKind (metadata.kind), actorId and changedFields as new optional FeedItem members (Clause-2 yes). A pure foldFeedItems in recordActivityFeed.ts folds adjacent updated rows with no kind by the same actorId and display name, at most 10 minutes between neighbours, after the filter; RecordActivityTimeline renders a run as one row (actor, latest time, N updates, union of changed-field labels from RecordContext objectSchema via useSafeFieldLabel, machine-key fallback, three names then N more) with an aria-expanded toggle that shows every entry. Create/delete/assign/share/completed/comment/milestone rows never fold; no host plumbing was needed (RecordDetailView already wraps the feed in RecordContextProvider with objectSchema), so RecordDetailView.tsx is untouched. Strings: five new detail keys (two count families) appended after detail.viewSource in all ten packs and mirrored in DETAIL_DEFAULT_TRANSLATIONS; the field list joins with the existing validation.formInvalidJoiner key because Intl.ListFormat is outside the package TS lib (first type-check red) and was measured and rejected for field lists elsewhere in the repo.",
"tests": "head 1c8752a0, after merging origin/main (17 commits): os-verify-lock VERDICT command-exit 0, cd ui and vitest run packages/plugin-detail/ packages/i18n/ plus app-shell RecordDetailView.activity*, .feed*, .replyCountOnRead, .recordOpenWaves, .authoredActivityThreading, .discussionFallbackPipeline, defaults-maps-mirror-en-pack, activityItemType: Test Files 403 passed 1 skipped (404), Tests 4327 passed 31 skipped. New RecordActivityTimeline.fold-25959.test.tsx 17 tests. Reverse check on committed head: the fold predicate's updated literal mutated to never match gave Test Files 1 failed, Tests 9 failed 8 passed (17); restored with git checkout HEAD, git hash-object equal to the HEAD blob. Tests import src directly (no dist leg). type-check of @object-ui/types, plugin-detail, i18n: VERDICT command-exit 0. Rest of app-shell left to CI Test Core (declared narrowing; the package does not finish locally).",
"gates": "dispatch-gates --commands on 1c8752a0 derived 46 commands; each run, all 46 exit 0 (captured before any pipe); --ran reconciliation: 46 derived, 46 run, 0 NOT-MEASURED (a derived zero, exit codes recorded). Includes node scripts/check-sdui-manifest.mjs (record re-recorded after the merge). CI check-runs: in_progress at report time, not waited on.",
"dogfood_battery": "n/a, no ! in the PR title and Clause-2 carries no (narrowing) arm",
"mcp_calls": "3, all reads: issue_read get #25959, issue_read get_comments #25959 twice",
"api_writes": "2 via the fleet-write relay and post-stamped: pr_create on objectstack-ai/objectstack (draft create plus assignee leg, read back identical, assignee os-steve), and this os-dev-report comment on #25959",
"open_questions": [
{
"question": "Fold window and adjacency: 10 minutes between neighbours, judged on the list after the filter (with Field changes selected, a hidden comment no longer splits a run). Keep?",
"options": [
"A keep both",
"B judge adjacency on the unfiltered merged list",
"C shorter window"
],
"recommendation": "A, because the fold is a presentation of what the reader sees, and one flow writing every few minutes stays one row"
}
],
"out_of_scope_findings": [
"carrier: none, noted in Acceptance notes only. A milestone row the read redaction served without its kind and whose declaration names no type arrives as a plain updated row and can fold (documented redaction behaviour).",
"carrier: none, noted only. Folding happens after applyFeedConfig's page window, so a page of limit entries can render as fewer rows."
]
}
ACCEPT → PR #26055 comment 6413742583. Contract review (TIER_DEFAULT) PASS: comment 6413728491, head 1c8752a0. The seat lands it once the checks are green.
MERGED 2026-10-26T23:28Z, merge commit 0b76a1f7, head e7a0e424 (+856/−157, seat record on #26055)
origin/main
contains it (git merge-base --is-ancestor exit 0)
queue branch
gh-readonly-queue/main/pr-26055-*: none left (consumed)
card
still open 15 min after the merge although the PR body carries Fixes #25959; closed (completed) by the seat with this comment
Status: in a record's activity panel, consecutive updates by one actor on the same record, each within 10 minutes of the last, fold into one row with the count and the changed field names, expandable to each entry. Creates, deletes, comments and milestones never fold. pm:dispatched removed.
Activity
objectstack-fleet commentedon Oct 26, 2026
Triage: first grade —
enhancement·priority:p2·domain:ui·area:records·pm:blocked· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-26T05:53Z⛔ Not a claim, ⛔ not a dispatch.
pm:dispatched). The fold reads the diff the event will carry, and the card's own premise is "once the event carries the diff".Blocked-by: #25787
Generated by Claude Code
objectstack-fleet commentedon Oct 26, 2026
Unlock scan: unlocked →
pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-26T07:56Z⛔ Not a claim, ⛔ not a dispatch.
completed. The renderer fold can now read it. Grade unchanged (p2 ·domain:ui).Generated by Claude Code
objectstack-fleet commentedon Oct 26, 2026
Claim: PM loop round 1 · 2026-10-26T08:36Z
Session:
session_01SMps7wq7rBTk53e8KTSEup(the seat; the card thread that implements it works on the branch below)Account:
os-steveBranch:
claude/project-thread-nv45n5(platform-assigned card-thread branch; a card thread can push only its assigned branch and the name carries no card number, so this claim is the identity record)Worktree:
objectstack-project-thread-nv45n5Domain:
domain:uiSeat:
domain:ui#2File surface:
ui/packages/plugin-detail/src/RecordActivityTimeline.tsx(the fold and the folded-row render),ui/packages/plugin-detail/src/renderers/recordActivityFeed.ts(activityRowToFeedItemcarries actor id, changed fields and the raw activity type),ui/packages/plugin-detail/src/useDetailTranslation.ts(defaults),ui/packages/types/src/views.ts(FeedItemoptional members), host plumbing inui/packages/app-shell/src/views/RecordDetailView.tsxonly if field labels need it (report it),ui/packages/i18n/src/locales/*.ts(appenddetail.*keys only), tests;scripts/sdui-manifest.record.json(re-record); root.changeset/25959-activity-fold.md(stop on breach; explain in the report). ⛔ui/.github/**and the root.github/workflows/ci.ymlare out of bounds (maintainer's word).Container & model:
M,mode:thread,model: default tier (dispatch-gates --tier --repo objectstack-ai/objectstack: no path-derived mandate),effort: highClause-②: yes
Metadata-protocol: none
Responsibility:
n/a — not a defect cardThread-read: 6411032153
Premise: holds 2026-10-26T08:25Z — the server half (#25787, PR #25958) landed; it names fields in the summary, and
sys_activity.changed_fieldsandactor_idalready reach the row, while the feed mapper drops both (origin/main05f7758f05)Serial constraints cleared:
ui/packages/i18n/src/locales/*.ts— seat 1's #25916 / #25898 append there; append-only, the later lander mergesmain.RecordDetailView.tsxis in no open claim (seat 1's queued #25877 may touch it later).Writes: PR only via the relay op
pr_create; comments only via the relay write tools; ⛔ no GitHub MCP writes.FeedItemtype of@object-ui/types; the seat reviews the declaration atTIER_DEFAULT.actor_id(null = system), adjacency in the merged list, a short window (seat suggests ≤10 min; report the choice), labels from the object schema with a machine-key fallback; never readmetadata.old/new.objectstack-fleet commentedon Oct 26, 2026
os-dev-report
{ "issue": 25959, "status": "done", "branch": "claude/project-thread-nv45n5", "pr": "https://github.com/objectstack-ai/objectstack/pull/26055", "session": "session_01PoTpJPgXim1ybDgRyat4BY", "premise_still_valid": true, "summary": "activityRowToFeedItem now carries activityType, activityKind (metadata.kind), actorId and changedFields as new optional FeedItem members (Clause-2 yes). A pure foldFeedItems in recordActivityFeed.ts folds adjacent updated rows with no kind by the same actorId and display name, at most 10 minutes between neighbours, after the filter; RecordActivityTimeline renders a run as one row (actor, latest time, N updates, union of changed-field labels from RecordContext objectSchema via useSafeFieldLabel, machine-key fallback, three names then N more) with an aria-expanded toggle that shows every entry. Create/delete/assign/share/completed/comment/milestone rows never fold; no host plumbing was needed (RecordDetailView already wraps the feed in RecordContextProvider with objectSchema), so RecordDetailView.tsx is untouched. Strings: five new detail keys (two count families) appended after detail.viewSource in all ten packs and mirrored in DETAIL_DEFAULT_TRANSLATIONS; the field list joins with the existing validation.formInvalidJoiner key because Intl.ListFormat is outside the package TS lib (first type-check red) and was measured and rejected for field lists elsewhere in the repo.", "tests": "head 1c8752a0, after merging origin/main (17 commits): os-verify-lock VERDICT command-exit 0, cd ui and vitest run packages/plugin-detail/ packages/i18n/ plus app-shell RecordDetailView.activity*, .feed*, .replyCountOnRead, .recordOpenWaves, .authoredActivityThreading, .discussionFallbackPipeline, defaults-maps-mirror-en-pack, activityItemType: Test Files 403 passed 1 skipped (404), Tests 4327 passed 31 skipped. New RecordActivityTimeline.fold-25959.test.tsx 17 tests. Reverse check on committed head: the fold predicate's updated literal mutated to never match gave Test Files 1 failed, Tests 9 failed 8 passed (17); restored with git checkout HEAD, git hash-object equal to the HEAD blob. Tests import src directly (no dist leg). type-check of @object-ui/types, plugin-detail, i18n: VERDICT command-exit 0. Rest of app-shell left to CI Test Core (declared narrowing; the package does not finish locally).", "gates": "dispatch-gates --commands on 1c8752a0 derived 46 commands; each run, all 46 exit 0 (captured before any pipe); --ran reconciliation: 46 derived, 46 run, 0 NOT-MEASURED (a derived zero, exit codes recorded). Includes node scripts/check-sdui-manifest.mjs (record re-recorded after the merge). CI check-runs: in_progress at report time, not waited on.", "dogfood_battery": "n/a, no ! in the PR title and Clause-2 carries no (narrowing) arm", "mcp_calls": "3, all reads: issue_read get #25959, issue_read get_comments #25959 twice", "api_writes": "2 via the fleet-write relay and post-stamped: pr_create on objectstack-ai/objectstack (draft create plus assignee leg, read back identical, assignee os-steve), and this os-dev-report comment on #25959", "open_questions": [ { "question": "Fold window and adjacency: 10 minutes between neighbours, judged on the list after the filter (with Field changes selected, a hidden comment no longer splits a run). Keep?", "options": [ "A keep both", "B judge adjacency on the unfiltered merged list", "C shorter window" ], "recommendation": "A, because the fold is a presentation of what the reader sees, and one flow writing every few minutes stays one row" } ], "out_of_scope_findings": [ "carrier: none, noted in Acceptance notes only. A milestone row the read redaction served without its kind and whose declaration names no type arrives as a plain updated row and can fold (documented redaction behaviour).", "carrier: none, noted only. Folding happens after applyFeedConfig's page window, so a page of limit entries can render as fewer rows." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 26, 2026
Pointer ·
domain:uiseat 2 · 2026-10-26T10:25ZACCEPT → PR #26055 comment 6413742583. Contract review (TIER_DEFAULT) PASS: comment 6413728491, head
1c8752a0. The seat lands it once the checks are green.objectstack-fleet commentedon Oct 26, 2026
Landed · seat 2 · 2026-10-26T23:44Z
0b76a1f7, heade7a0e424(+856/−157, seat record on #26055)origin/maingit merge-base --is-ancestorexit 0)gh-readonly-queue/main/pr-26055-*: none left (consumed)Fixes #25959; closed (completed) by the seat with this commentStatus: in a record's activity panel, consecutive updates by one actor on the same record, each within 10 minutes of the last, fold into one row with the count and the changed field names, expandable to each entry. Creates, deletes, comments and milestones never fold.
pm:dispatchedremoved.