From 17775274a0c8b14f7754376ccb33aa05e50618a6 Mon Sep 17 00:00:00 2001 From: iliya Date: Thu, 5 Mar 2026 22:16:57 +0200 Subject: [PATCH] feat: add source field to messages for system notifications - Enhanced TeamDataService to include a 'source' field in message payloads, specifically for system notifications. - Updated InboxMessage type to accommodate the new 'system_notification' source. - Modified TeamInboxWriter to conditionally include the source field in the message payload. - Added tests to verify the inclusion and omission of the source field based on request parameters. --- src/main/services/team/TeamDataService.ts | 8 ++++ src/main/services/team/TeamInboxWriter.ts | 1 + .../components/team/TeamDetailView.tsx | 46 ++++++++++--------- .../team/activity/ActivityTimeline.tsx | 7 ++- src/shared/types/team.ts | 3 +- .../services/team/TeamInboxWriter.test.ts | 24 ++++++++++ 6 files changed, 64 insertions(+), 25 deletions(-) diff --git a/src/main/services/team/TeamDataService.ts b/src/main/services/team/TeamDataService.ts index e44851fd..ec3c8a43 100644 --- a/src/main/services/team/TeamDataService.ts +++ b/src/main/services/team/TeamDataService.ts @@ -862,6 +862,7 @@ export class TeamDataService { from: leadName, text: parts.join('\n'), summary: `New task #${task.id} assigned`, + source: 'system_notification', }); } } catch { @@ -906,6 +907,7 @@ export class TeamDataService { from: leadName, text: parts.join('\n'), summary: `Task #${task.id} started`, + source: 'system_notification', }); } } catch { @@ -961,6 +963,7 @@ export class TeamDataService { from: last.actor, text: `Task #${task.id} "${task.subject}" has been started by ${last.actor}.`, summary: `Task #${task.id} started`, + source: 'system_notification', }); } catch (error) { logger.warn(`[TeamDataService] notifyLeadOnTeammateTaskStart failed: ${String(error)}`); @@ -1072,6 +1075,7 @@ export class TeamDataService { from: leadName, text: parts.join('\n'), summary: `Comment on #${taskId}`, + source: 'system_notification', }); } else if (task && owner && this.isLeadOwner(owner, leadName)) { // Notify lead about user's comment on their own task. @@ -1088,6 +1092,7 @@ export class TeamDataService { from: 'user', text: parts.join('\n'), summary: `Comment on #${taskId}`, + source: 'system_notification', }); } } catch { @@ -1208,6 +1213,7 @@ export class TeamDataService { `node "${toolPath}" --team ${teamName} review request-changes ${taskId} --comment "..."\n` + AGENT_BLOCK_CLOSE, summary: `Review request for #${taskId}`, + source: 'system_notification', }); } catch (error) { await this.kanbanManager @@ -1307,6 +1313,7 @@ export class TeamDataService { for (const msg of messages) { if (!msg.messageId || !msg.summary || msg.from === 'user') continue; if (msg.source === 'lead_session' || msg.source === 'lead_process') continue; + if (msg.source === 'system_notification') continue; if (isAutomatedCommentNotification(msg)) continue; const textKey = `${msg.from}\0${msg.text}`; @@ -1490,6 +1497,7 @@ export class TeamDataService { `${patch.comment?.trim() || 'Reviewer requested changes.'}\n\n` + `Please fix and mark it as completed when ready.`, summary: `Fix request for #${taskId}`, + source: 'system_notification', }); } catch (error) { await this.taskWriter diff --git a/src/main/services/team/TeamInboxWriter.ts b/src/main/services/team/TeamInboxWriter.ts index fd04a317..a0776db4 100644 --- a/src/main/services/team/TeamInboxWriter.ts +++ b/src/main/services/team/TeamInboxWriter.ts @@ -29,6 +29,7 @@ export class TeamInboxWriter { summary: request.summary, messageId, attachments: attachmentMeta?.length ? attachmentMeta : undefined, + ...(request.source && { source: request.source }), }; await withInboxLock(inboxPath, async () => { diff --git a/src/renderer/components/team/TeamDetailView.tsx b/src/renderer/components/team/TeamDetailView.tsx index fe8b7bb9..7b6cb95d 100644 --- a/src/renderer/components/team/TeamDetailView.tsx +++ b/src/renderer/components/team/TeamDetailView.tsx @@ -1416,28 +1416,25 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele : undefined } headerExtra={ - - - - - Desktop notifications plugin - - } - defaultOpen - action={ -
+ <> + + + + + Desktop notifications plugin + {messagesUnreadCount > 0 && ( @@ -1455,6 +1452,11 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele Mark all as read )} + + } + defaultOpen + action={ +
+
- Новая сессия + New session
diff --git a/src/shared/types/team.ts b/src/shared/types/team.ts index a1f00964..06220230 100644 --- a/src/shared/types/team.ts +++ b/src/shared/types/team.ts @@ -195,7 +195,7 @@ export interface InboxMessage { summary?: string; color?: string; messageId?: string; - source?: 'inbox' | 'lead_session' | 'lead_process' | 'user_sent'; + source?: 'inbox' | 'lead_session' | 'lead_process' | 'user_sent' | 'system_notification'; attachments?: AttachmentMeta[]; /** Lead session ID that produced this message (for session boundary detection). */ leadSessionId?: string; @@ -207,6 +207,7 @@ export interface SendMessageRequest { summary?: string; from?: string; attachments?: AttachmentPayload[]; + source?: InboxMessage['source']; } export interface SendMessageResult { diff --git a/test/main/services/team/TeamInboxWriter.test.ts b/test/main/services/team/TeamInboxWriter.test.ts index 5bf0efef..fd44cfd4 100644 --- a/test/main/services/team/TeamInboxWriter.test.ts +++ b/test/main/services/team/TeamInboxWriter.test.ts @@ -130,4 +130,28 @@ describe('TeamInboxWriter', () => { expect(persisted).toHaveLength(2); expect(persisted.map((row) => row.text).sort()).toEqual(['first', 'second']); }); + + it('includes source field in payload when provided in request', async () => { + await writer.sendMessage('my-team', { + member: 'alice', + text: 'task assigned', + summary: 'New task #1 assigned', + source: 'system_notification', + }); + + const persisted = JSON.parse(hoisted.files.get(inboxPath) ?? '[]') as Record[]; + expect(persisted).toHaveLength(1); + expect(persisted[0].source).toBe('system_notification'); + }); + + it('omits source field from payload when not provided in request', async () => { + await writer.sendMessage('my-team', { + member: 'alice', + text: 'hello', + }); + + const persisted = JSON.parse(hoisted.files.get(inboxPath) ?? '[]') as Record[]; + expect(persisted).toHaveLength(1); + expect(persisted[0]).not.toHaveProperty('source'); + }); });