From 00e320d2156f995fa2adf2ae146055a8ba1cb9a2 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Thu, 3 Sep 2026 11:18:39 -0700 Subject: [PATCH 1/2] fix(activity): make the page match the ruled canvas, not just its tokens Phase A (#1518) painted the new tokens over the old layout, so the live page kept the big composer heading, a bordered textarea with a 44px ink block beside it, 19px section headings with eyebrows, circle marks, and a reply box squeezed into the right column. Sam: not close to the preview. This is the canvas's after-side as structure: - composer is one radius-lg card: uppercase 11px label + pod chip on top, borderless textarea, a foot row with the hint and a 30px ink Send; focus is the halo on the card - section headings are 15/20 with an ink count badge and the description as muted meta on the same line; eyebrows gone - rows: name + meta on the top line, body in ink, the reply line UNDER the message (input 32px, ink Send, quiet Acknowledge / Open thread); square marks; pending cards radius-lg with the strong ring - the producer now stores actorName (resolved from the User row when a PG message carries only user_id), so a mention row is headed by who said it instead of 'Someone mentioned you' Frontend: 86 tests green (Activity, invariants, i18n keys), tsc clean. Backend: attentionItemService 9/9 incl. the author-lookup case. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01UHfcrzjN6MpeuCCAap5Qnb --- .../services/attentionItemService.test.js | 15 +- backend/models/AttentionItem.ts | 2 + backend/services/attentionItemService.ts | 21 +- frontend/src/i18n/locales/en.json | 7 +- frontend/src/i18n/locales/zh-CN.json | 7 +- frontend/src/v2/components/V2ActivityPage.tsx | 55 ++--- frontend/src/v2/v2.css | 218 ++++++++++++------ 7 files changed, 214 insertions(+), 111 deletions(-) diff --git a/backend/__tests__/unit/services/attentionItemService.test.js b/backend/__tests__/unit/services/attentionItemService.test.js index df3cb6cd8..da3bf0c67 100644 --- a/backend/__tests__/unit/services/attentionItemService.test.js +++ b/backend/__tests__/unit/services/attentionItemService.test.js @@ -7,7 +7,8 @@ const mockUserFind = jest.fn(); jest.mock('../../../models/AttentionItem', () => ({ updateOne: mockUpdateOne, updateMany: mockUpdateMany, find: mockFind })); jest.mock('../../../models/Pod', () => ({ findById: mockPodFindById, find: mockPodFind })); -jest.mock('../../../models/User', () => ({ find: mockUserFind })); +const mockUserFindById = jest.fn(); +jest.mock('../../../models/User', () => ({ find: mockUserFind, findById: mockUserFindById })); const chain = (value) => ({ select: () => ({ lean: async () => value }) }); const AttentionItemService = require('../../../services/attentionItemService'); @@ -34,6 +35,18 @@ describe('attentionItemService', () => { expect(mockUpdateOne.mock.calls[0][1].$setOnInsert).toMatchObject({ kind: 'mention', title: 'Ada mentioned you', messageId: '42' }); }); + it('names the author from the User row when the message carries only user_id (PG rows)', async () => { + mockPodFindById.mockReturnValue(chain({ _id: 'pod-1', name: 'Ship room', createdBy: 'owner', members: [{ userId: 'sam' }] })); + mockUserFind.mockReturnValue(chain([ + { _id: 'owner', username: 'owner', isBot: false }, + { _id: 'sam', username: 'Sam', isBot: false }, + ])); + mockUserFindById.mockReturnValue(chain({ _id: 'owner', username: 'ada', botMetadata: { displayName: 'Ada Lovelace' } })); + await AttentionItemService.recordMentionedUsers({ id: 43, pod_id: 'pod-1', user_id: 'owner', content: '@sam one more' }); + expect(mockUserFindById).toHaveBeenCalledWith('owner'); + expect(mockUpdateOne.mock.calls[0][1].$setOnInsert).toMatchObject({ title: 'Ada Lovelace mentioned you', actorName: 'Ada Lovelace' }); + }); + it('does not read pod membership for a message with no mention marker', async () => { await AttentionItemService.recordMentionedUsers({ id: 42, podId: 'pod-1', userId: 'owner', username: 'Ada', content: 'ordinary status update', diff --git a/backend/models/AttentionItem.ts b/backend/models/AttentionItem.ts index db23941a3..92f9198a8 100644 --- a/backend/models/AttentionItem.ts +++ b/backend/models/AttentionItem.ts @@ -11,6 +11,7 @@ export interface IAttentionItem extends Document { title: string; detail?: string; podName?: string; + actorName?: string; messageId?: string; threadRootId?: string; options?: Array<{ label: string; description?: string; recommended?: boolean }>; @@ -37,6 +38,7 @@ const attentionItemSchema = new Schema({ title: { type: String, required: true }, detail: { type: String }, podName: { type: String }, + actorName: { type: String }, messageId: { type: String }, threadRootId: { type: String }, options: [optionSchema], diff --git a/backend/services/attentionItemService.ts b/backend/services/attentionItemService.ts index dbca1b273..604429ad4 100644 --- a/backend/services/attentionItemService.ts +++ b/backend/services/attentionItemService.ts @@ -50,6 +50,7 @@ const recordForRecipients = async ( title: payload.title, detail: payload.detail, podName: payload.podName, + actorName: payload.actorName, messageId: payload.messageId, threadRootId: payload.threadRootId, options: payload.options, @@ -60,6 +61,16 @@ const recordForRecipients = async ( ))); }; +const resolveAuthorName = async (authorId: unknown): Promise => { + if (!authorId) return 'Someone'; + try { + const author = await User.findById(authorId).select('username botMetadata').lean(); + return author?.botMetadata?.displayName || author?.username || 'Someone'; + } catch { + return 'Someone'; + } +}; + export const recordMentionedUsers = async (message: any, options: MentionOptions = {}): Promise => { try { const podId = message?.podId || message?.pod_id; @@ -80,10 +91,12 @@ export const recordMentionedUsers = async (message: any, options: MentionOptions }); if (!recipients.length) return; const pod = await Pod.findById(podId).select('name').lean(); - const authorName = message?.username || message?.userId?.username || 'Someone'; + // PG rows arrive with user_id only, so 'Someone' was what every live + // mention showed. Resolve the author the way chat renders them. + const authorName = message?.username || message?.userId?.username || await resolveAuthorName(authorId); await recordForRecipients(recipients, { podId, kind: 'mention' as Kind, sourceType: 'message' as SourceType, sourceId: sourceKey('message', messageId), - title: `${authorName} mentioned you`, detail: compact(content), podName: pod?.name || 'Pod', + title: `${authorName} mentioned you`, actorName: authorName, detail: compact(content), podName: pod?.name || 'Pod', messageId: String(messageId), threadRootId: String(message?.threadRootId || message?.thread_root_id || messageId), }); } catch (error) { @@ -101,7 +114,7 @@ export const recordApproval = async (approval: any): Promise => { const agentName = approval?.agentMetadata?.agentName; await recordForRecipients(recipients, { podId, kind: 'approval' as Kind, sourceType: 'approval' as SourceType, sourceId: sourceKey('approval', id), - title: agentName ? `${agentName} requests approval` : 'Approval requested', detail: compact(approval?.content, 180), podName: pod?.name || 'Pod', + title: agentName ? `${agentName} requests approval` : 'Approval requested', actorName: agentName || undefined, detail: compact(approval?.content, 180), podName: pod?.name || 'Pod', }); } catch (error) { console.warn('[attention] approval materialization failed:', (error as Error).message); @@ -213,7 +226,7 @@ export const getOpenQueue = async (recipientUserId: unknown): Promise<{ items: a if (row.kind === 'mention' && mentionCount >= 8) continue; if (row.kind === 'mention') mentionCount += 1; picked.push({ - id: String(row.source.id), attentionItemId: String(row._id), kind: row.kind, title: row.title, detail: row.detail || '', + id: String(row.source.id), attentionItemId: String(row._id), kind: row.kind, title: row.title, actorName: row.actorName || undefined, detail: row.detail || '', podId: String(row.podId), podName: (allowed.get(String(row.podId)) as any)?.name || row.podName || 'Pod', messageId: row.messageId, threadRootId: row.threadRootId, options: row.options || [], createdAt: row.createdAt, }); diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 69cf9df64..44806cf4a 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -47,7 +47,9 @@ "send": "Send", "sendAriaLabel": "Send message", "working": "Sending…", - "actionFailed": "Your message could not be sent. Try again." + "actionFailed": "Your message could not be sent. Try again.", + "label": "Tell your agents", + "hint": "Posts as you · ⌘↵ to send" }, "reply": { "placeholder": "Reply in thread…", @@ -69,7 +71,8 @@ "approval": "Approval", "decision": "Decision needed", "press": "Ready for your press" - } + }, + "countLabel": "{{count}} waiting on you" }, "dayZero": { "kind": "Get started", diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index 87fd2b001..07e504415 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -47,7 +47,9 @@ "send": "发送", "sendAriaLabel": "发送消息", "working": "发送中…", - "actionFailed": "无法发送消息,请重试。" + "actionFailed": "无法发送消息,请重试。", + "label": "告诉你的智能体", + "hint": "以你的身份发布 · ⌘↵ 发送" }, "reply": { "placeholder": "在话题中回复…", @@ -69,7 +71,8 @@ "approval": "审批", "decision": "待你决策", "press": "待你合并" - } + }, + "countLabel": "{{count}} 项等你处理" }, "dayZero": { "kind": "开始使用", diff --git a/frontend/src/v2/components/V2ActivityPage.tsx b/frontend/src/v2/components/V2ActivityPage.tsx index 16cb612d2..4447d6772 100644 --- a/frontend/src/v2/components/V2ActivityPage.tsx +++ b/frontend/src/v2/components/V2ActivityPage.tsx @@ -28,6 +28,7 @@ interface AgentRecap { interface NeedsYouItem { id: string; attentionItemId?: string; + actorName?: string; kind: 'mention' | 'approval' | 'decision'; title: string; detail: string; @@ -334,11 +335,8 @@ const V2ActivityPage: React.FC = () => { {!loading && !error && recap && ( <>
-
-
-
{t('activity.compose.eyebrow')}
-

{t('activity.compose.title')}

-
+
+

{t('activity.compose.label')}

-
-