Skip to content

feat: Feed/Chatter protocol — unified activity timeline data model and UI component schemas#732

Merged
hotlong merged 2 commits intomainfrom
copilot/enhance-feed-chatter-protocol
Feb 19, 2026
Merged

feat: Feed/Chatter protocol — unified activity timeline data model and UI component schemas#732
hotlong merged 2 commits intomainfrom
copilot/enhance-feed-chatter-protocol

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Feb 19, 2026

feeds: true / activities: true capability switches existed on objects but had no backing data model. record:chatter was mapped to EmptyProps. This PR fills those gaps with Airtable/Salesforce-style Feed, Mention, Reaction, and Subscription protocols.

Data Protocol — src/data/feed.zod.ts

  • FeedItemType — 13 unified activity types: comment, field_change, task, event, email, call, note, file, record_create, record_delete, approval, sharing, system
  • FeedItemSchema — timeline entry with actor, body, mentions, field changes, reactions, threading (parentId/replyCount), visibility, edit tracking
  • MentionSchemauser/team/record targets with character offset+length for rich text rendering
  • FieldChangeEntrySchema — old/new values with display representations
  • ReactionSchema — emoji reactions with user ID tracking
  • FeedFilterModeall, comments_only, changes_only, tasks_only

Data Protocol — src/data/subscription.zod.ts

  • RecordSubscriptionSchema — record-level notification subscription (bell icon pattern)
  • SubscriptionEventTypecomment/mention/field_change/task/approval/all
  • NotificationChannelin_app/email/push/slack

UI Protocol — component.zod.ts

RecordActivityProps enhanced with unified timeline support:

  • types now accepts full FeedItemType enum (was hardcoded to 5 activity types)
  • Added filterMode, unifiedTimeline, showCommentInput, enableMentions, enableReactions, enableThreading, showSubscriptionToggle

RecordChatterProps replaces EmptyProps:

  • position: sidebar | inline | drawer
  • width, collapsible, defaultCollapsed
  • feed: embedded RecordActivityProps for full configuration delegation
// record:chatter now supports declarative configuration
{
  type: 'record:chatter',
  properties: {
    position: 'sidebar',
    width: '350px',
    collapsible: true,
    feed: {
      types: ['comment', 'field_change'],
      filterMode: 'all',
      unifiedTimeline: true,
      enableMentions: true,
      enableReactions: true,
    },
  },
}

Backward Compatibility

Existing types: ['task', 'event', 'email', 'call', 'note'] usage (e.g., CRM example) remains valid — all are members of the expanded FeedItemType enum. Default values preserve prior behavior (limit changed from 10→20 to match Airtable defaults).

Tests

62 new tests across feed.test.ts (35), subscription.test.ts (11), component.test.ts (16). Full suite: 5705 passing.

Original prompt

This section details on the original issue you should resolve

<issue_title>协议增强:实现 Airtable 风格的 Comments & Activity Timeline(Feed/Chatter 协议标准)</issue_title>
<issue_description>### 背景

Airtable 等现代 SaaS 平台广泛采用了右侧 Activity Feed 面板,将字段变更、评论(支持@提及)、系统任务活动等融合为统一时间线,这为协同、审核、历史追踪带来极大便利。

当前 @objectstack/spec 协议已经有

  • feeds: true / activities: true / trackHistory: true 等能力开关(Data Protocol)
  • 字段级的审计开关和变更跟踪能力
  • UI 层有 record:activity 组件,但 record:chatter 未定义(空壳),也缺乏 Feed/Comment 数据结构

目标

  1. 对标 Airtable/Notion/Salesforce,补全和标准化 Comment/Feed 协议,支持 audit + comment + mention + reaction + 通知订阅(一致时间线)
  2. 让 UI 配置模式下能声明式插入"所有活动"侧边栏,兼容过滤、操作、@提及等能力

主要差距和需求

  • 缺少 FeedItem/Comment/Mention 数据协议(应统一 comments���field_change、task、note、approval 等为 FeedItemType 并支持 @Mentions、Reactions)
  • record:chatter 组件 props 必须补全,支持评论输入/过滤/订阅等配置
  • RecordActivity/Chatter 组件应支持过滤(all/changes/comments/... 过滤)、unifiedTimeline 等属性
  • Feed 评论支持 @提及(mention)、回复(线程)、reaction、编辑删除、附件
  • 补全 API 协议(GET/POST/PUT/DELETE /feed、/reactions、/subscribe 等端点约定)

推荐实现步骤

  1. 创建 src/data/feed.zod.ts,定义 FeedItemType、FeedItemSchema、MentionSchema、ReactionSchema
  2. 增强 component.zod.ts,扩展 RecordActivityProps/RecordChatterProps 属性,并修正 ComponentPropsMap
  3. 定义 src/data/subscription.zod.ts(通知订阅能力,可选)
  4. 明确 API 端点模型约定

相关参考

任务参考实现(可分多个 PR 完善)

  • 定义 feed 协议基础 zod schema
  • 增强 UI 层协议与 props
  • API 约定与安全
  • 完善测试
    test

对标 Airtable 截图中的 右侧 Activity 面板(评论 + 字段变更历史 + @ Mentions)


🔍 一、截图分析:Airtable 的 Comments & Activity 功能拆解

从你提供的 Airtable 截图 image1 可以看到:

功能区 Airtable 实现 关键能力
Activity 侧边栏 记录详情页右侧固定面板 独立区域、可折叠
Activity Feed "All activity" 下拉过滤 + 时间线 字段变更 + 评论混合排列
字段变更历史 显示字段名、旧值→新值、时间戳 REGION → Asia-Pacific, STATUS → Active
评论 (Comments) 底部 "Leave a comment" 输入框 富文本、@mentions
通知铃铛 面板顶部通知开关 订阅/取消该记录的变更通知
操作者信息 "You edited using Omni 1d ago" 用户头像、来源、时间
扩展历史 "Upgrade to extend your history!" 付费门控的历史深度

🟢 二、当前协议已有的基础设施

A. Data Protocol — 能力开关(✅ 良好)

export const ObjectCapabilities = z.object({
  trackHistory: z.boolean().default(false).describe('Enable field history tracking for audit compliance'),
  feeds: z.boolean().default(false).describe('Enable social feed, comments, and mentions (Chatter-like)'),
  activities: z.boolean().default(false).describe('Enable standard tasks and events tracking'),
  // ...
});

评估: feeds: trueactivities: truetrackHistory: true 这三个开关已经存在,定义了启用的意图,但 没有对应��� Feed/Comment 数据模型协议

B. Field Protocol — 字段级变更追踪(✅ 存在)

trackFeedHistory: z.boolean().optional().describe('Track field changes in Chatter/activity feed (Salesforce pattern)'),
auditTrail: z.boolean().default(false).describe('Enable detailed audit trail for this field'),

评估: 字段级别的 trackFeedHistory 可控制哪些字段的变更出现在 Feed 中(类似 Airtable 截图中只显示了 REGION, OPPORTUNITIES, STATUS 等关键字段变更)。

C. UI Protocol — Activity 组件(🟡 骨架存在)

export const RecordActivityProps = z.object({
  types: z.array(z.enum(['task', 'event', 'email', 'call', 'note'])).optional(),
  limit: z.number().int().positive().default(10),
  showCompleted: z.boolean().default(false),
  aria: AriaPropsSchema.optional(),
});

以及 ComponentPropsMap 中的注册:

// Record
'record:details': RecordDetailsProps,
'record:related_list': RecordRelatedListProps,
'record:highlights': RecordHighlightsProps,
'record:activity': RecordActivityProps,
'record:chatter': EmptyProps,          // ← 空壳!
'record:path': RecordPathProps,

D. Page Protocol — Lead Detail 示例用法(✅ 演示存在)

{
  type: 'record:activity',
  id: 'lead_activity',
  label: 'Activity Timeline',
  properties: {
    types: ['task', 'event', 'email', '...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes objectstack-ai/spec#731

<!-- START COPILOT CODING AGENT TIPS -->
---

💬 We'd love your input! Share your thoughts on Copilot coding agent in our [2 minute survey](https://gh.io/copilot-coding-agent-survey).

@vercel
Copy link
Copy Markdown

vercel bot commented Feb 19, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
objectstack-play Ready Ready Preview, Comment Feb 19, 2026 8:12am
spec Ready Ready Preview, Comment Feb 19, 2026 8:12am

Request Review

…ubscription, enhanced Activity/Chatter components)

- Create src/data/feed.zod.ts with FeedItemType, FeedItemSchema, MentionSchema, ReactionSchema, FieldChangeEntrySchema, FeedActorSchema, FeedFilterMode
- Create src/data/subscription.zod.ts with RecordSubscriptionSchema, SubscriptionEventType, NotificationChannel
- Enhance RecordActivityProps with unifiedTimeline, filterMode, showCommentInput, enableMentions, enableReactions, enableThreading, showSubscriptionToggle
- Implement RecordChatterProps replacing EmptyProps with sidebar/inline/drawer position, width, collapsible, embedded feed config
- Update ComponentPropsMap to use RecordChatterProps
- Add comprehensive tests (feed.test.ts, subscription.test.ts, enhanced component.test.ts)
- Update ROADMAP.md

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copilot AI changed the title [WIP] Implement Airtable style comments and activity timeline feat: Feed/Chatter protocol — unified activity timeline data model and UI component schemas Feb 19, 2026
Copilot AI requested a review from hotlong February 19, 2026 07:57
@hotlong hotlong marked this pull request as ready for review February 19, 2026 08:00
Copilot AI review requested due to automatic review settings February 19, 2026 08:00
@hotlong hotlong merged commit 42d5a89 into main Feb 19, 2026
3 of 5 checks passed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds missing Feed/Chatter backing protocols to @objectstack/spec so existing feeds: true / activities: true capabilities and record:chatter UI component have a concrete, unified activity timeline model (Airtable/Salesforce-style), plus record-level subscription support.

Changes:

  • Introduces FeedItem / Mention / Reaction / FieldChangeEntry schemas and filtering enum in src/data/feed.zod.ts.
  • Adds RecordSubscriptionSchema + channel/event enums in src/data/subscription.zod.ts and exports both via src/data/index.ts.
  • Enhances RecordActivityProps and replaces record:chatter props from EmptyProps to RecordChatterProps, with corresponding test coverage.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
packages/spec/src/ui/component.zod.ts Extends record activity configuration and defines RecordChatterProps; wires record:chatter into ComponentPropsMap.
packages/spec/src/ui/component.test.ts Adds tests covering the new/expanded component prop schemas.
packages/spec/src/data/feed.zod.ts Adds the unified feed/timeline data protocol schemas and enums.
packages/spec/src/data/feed.test.ts Adds schema validation tests for feed protocol.
packages/spec/src/data/subscription.zod.ts Adds record-level subscription protocol (events + channels + schema).
packages/spec/src/data/subscription.test.ts Adds schema validation tests for subscription protocol.
packages/spec/src/data/index.ts Re-exports the new feed/subscription protocols via the data domain entrypoint.
ROADMAP.md Updates protocol deliverables list to include feed/timeline and record subscription.

Comment on lines +58 to +62
export const ReactionSchema = z.object({
emoji: z.string().describe('Emoji character or shortcode (e.g., "👍", ":thumbsup:")'),
userIds: z.array(z.string()).describe('Users who reacted'),
count: z.number().int().min(1).describe('Total reaction count'),
});
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ReactionSchema can become internally inconsistent because it requires both userIds and count but does not enforce that count matches userIds.length (and it also prevents cases where you only know the aggregate count). Consider either deriving count from userIds, making userIds optional with count required, or adding a refinement to keep them consistent.

Suggested change
export const ReactionSchema = z.object({
emoji: z.string().describe('Emoji character or shortcode (e.g., "👍", ":thumbsup:")'),
userIds: z.array(z.string()).describe('Users who reacted'),
count: z.number().int().min(1).describe('Total reaction count'),
});
export const ReactionSchema = z
.object({
emoji: z.string().describe('Emoji character or shortcode (e.g., "👍", ":thumbsup:")'),
userIds: z.array(z.string()).describe('Users who reacted'),
count: z.number().int().min(1).describe('Total reaction count'),
})
.refine(
(reaction) => reaction.userIds.length === reaction.count,
{
message: 'Reaction count must match number of userIds',
path: ['count'],
},
);

Copilot uses AI. Check for mistakes.
Comment on lines +114 to +153
export const FeedItemSchema = z.object({
/** Unique identifier */
id: z.string().describe('Feed item ID'),

/** Feed item type */
type: FeedItemType.describe('Activity type'),

/** Target record reference */
object: z.string().describe('Object name (e.g., "account")'),
recordId: z.string().describe('Record ID this feed item belongs to'),

/** Actor (who performed the action) */
actor: FeedActorSchema.describe('Who performed this action'),

/** Content (for comments/notes) */
body: z.string().optional().describe('Rich text body (Markdown supported)'),

/** @Mentions */
mentions: z.array(MentionSchema).optional().describe('Mentioned users/teams/records'),

/** Field changes (for field_change type) */
changes: z.array(FieldChangeEntrySchema).optional().describe('Field-level changes'),

/** Reactions */
reactions: z.array(ReactionSchema).optional().describe('Emoji reactions on this item'),

/** Reply threading */
parentId: z.string().optional().describe('Parent feed item ID for threaded replies'),
replyCount: z.number().int().min(0).default(0).describe('Number of replies'),

/** Visibility */
visibility: FeedVisibility.default('public')
.describe('Visibility: public (all users), internal (team only), private (author + mentioned)'),

/** Timestamps */
createdAt: z.string().datetime().describe('Creation timestamp'),
updatedAt: z.string().datetime().optional().describe('Last update timestamp'),
editedAt: z.string().datetime().optional().describe('When comment was last edited'),
isEdited: z.boolean().default(false).describe('Whether comment has been edited'),
});
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FeedItemSchema allows invalid combinations for edit tracking (e.g., isEdited: true without editedAt, or editedAt present while isEdited is false). Add a refinement/discriminated state so the two fields stay consistent.

Suggested change
export const FeedItemSchema = z.object({
/** Unique identifier */
id: z.string().describe('Feed item ID'),
/** Feed item type */
type: FeedItemType.describe('Activity type'),
/** Target record reference */
object: z.string().describe('Object name (e.g., "account")'),
recordId: z.string().describe('Record ID this feed item belongs to'),
/** Actor (who performed the action) */
actor: FeedActorSchema.describe('Who performed this action'),
/** Content (for comments/notes) */
body: z.string().optional().describe('Rich text body (Markdown supported)'),
/** @Mentions */
mentions: z.array(MentionSchema).optional().describe('Mentioned users/teams/records'),
/** Field changes (for field_change type) */
changes: z.array(FieldChangeEntrySchema).optional().describe('Field-level changes'),
/** Reactions */
reactions: z.array(ReactionSchema).optional().describe('Emoji reactions on this item'),
/** Reply threading */
parentId: z.string().optional().describe('Parent feed item ID for threaded replies'),
replyCount: z.number().int().min(0).default(0).describe('Number of replies'),
/** Visibility */
visibility: FeedVisibility.default('public')
.describe('Visibility: public (all users), internal (team only), private (author + mentioned)'),
/** Timestamps */
createdAt: z.string().datetime().describe('Creation timestamp'),
updatedAt: z.string().datetime().optional().describe('Last update timestamp'),
editedAt: z.string().datetime().optional().describe('When comment was last edited'),
isEdited: z.boolean().default(false).describe('Whether comment has been edited'),
});
export const FeedItemSchema = z
.object({
/** Unique identifier */
id: z.string().describe('Feed item ID'),
/** Feed item type */
type: FeedItemType.describe('Activity type'),
/** Target record reference */
object: z.string().describe('Object name (e.g., "account")'),
recordId: z.string().describe('Record ID this feed item belongs to'),
/** Actor (who performed the action) */
actor: FeedActorSchema.describe('Who performed this action'),
/** Content (for comments/notes) */
body: z.string().optional().describe('Rich text body (Markdown supported)'),
/** @Mentions */
mentions: z.array(MentionSchema).optional().describe('Mentioned users/teams/records'),
/** Field changes (for field_change type) */
changes: z.array(FieldChangeEntrySchema).optional().describe('Field-level changes'),
/** Reactions */
reactions: z.array(ReactionSchema).optional().describe('Emoji reactions on this item'),
/** Reply threading */
parentId: z.string().optional().describe('Parent feed item ID for threaded replies'),
replyCount: z.number().int().min(0).default(0).describe('Number of replies'),
/** Visibility */
visibility: FeedVisibility.default('public')
.describe('Visibility: public (all users), internal (team only), private (author + mentioned)'),
/** Timestamps */
createdAt: z.string().datetime().describe('Creation timestamp'),
updatedAt: z.string().datetime().optional().describe('Last update timestamp'),
editedAt: z.string().datetime().optional().describe('When comment was last edited'),
isEdited: z.boolean().default(false).describe('Whether comment has been edited'),
})
.superRefine((data, ctx) => {
if (data.isEdited && !data.editedAt) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['editedAt'],
message: 'editedAt is required when isEdited is true',
});
}
if (!data.isEdited && data.editedAt) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['isEdited'],
message: 'isEdited must be true when editedAt is set',
});
}
});

Copilot uses AI. Check for mistakes.
Comment on lines +114 to +139
export const FeedItemSchema = z.object({
/** Unique identifier */
id: z.string().describe('Feed item ID'),

/** Feed item type */
type: FeedItemType.describe('Activity type'),

/** Target record reference */
object: z.string().describe('Object name (e.g., "account")'),
recordId: z.string().describe('Record ID this feed item belongs to'),

/** Actor (who performed the action) */
actor: FeedActorSchema.describe('Who performed this action'),

/** Content (for comments/notes) */
body: z.string().optional().describe('Rich text body (Markdown supported)'),

/** @Mentions */
mentions: z.array(MentionSchema).optional().describe('Mentioned users/teams/records'),

/** Field changes (for field_change type) */
changes: z.array(FieldChangeEntrySchema).optional().describe('Field-level changes'),

/** Reactions */
reactions: z.array(ReactionSchema).optional().describe('Emoji reactions on this item'),

Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FeedItemSchema is not type-discriminated, so it currently permits obviously invalid payloads (e.g., type: "comment" with no body, or type: "field_change" with no changes). Given the docstrings/examples, consider switching to z.discriminatedUnion('type', ...) or adding a superRefine that enforces required fields per type.

Copilot uses AI. Check for mistakes.
Comment on lines +23 to +28
export const NotificationChannel = z.enum([
'in_app',
'email',
'push',
'slack',
]);
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new NotificationChannel enum diverges from the existing System.NotificationChannelSchema (e.g. in_app vs in-app, and missing channels like sms, teams, webhook). This creates two similar-but-incompatible channel vocabularies. Consider reusing/importing NotificationChannelSchema or aligning the string literals to the existing system protocol.

Copilot uses AI. Check for mistakes.
Comment on lines +44 to +47
/** Events to subscribe to */
events: z.array(SubscriptionEventType)
.default(['all'])
.describe('Event types to receive notifications for'),
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RecordSubscriptionSchema.events defaults to ["all"], but the schema also allows mixing "all" with specific event types (e.g. ["all","comment"]), which is ambiguous. Consider constraining events so that either it is exactly ["all"] or it contains only specific event types (excluding "all").

Copilot uses AI. Check for mistakes.
Comment on lines +127 to +129
collapsible: z.boolean().default(true).describe('Whether the panel can be collapsed'),
/** Default collapsed state */
defaultCollapsed: z.boolean().default(false).describe('Whether the panel starts collapsed'),
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RecordChatterProps allows an inconsistent state where collapsible is false but defaultCollapsed can still be true. Consider a refinement so defaultCollapsed must be false when collapsible is disabled.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants