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.
This commit is contained in:
iliya 2026-03-05 22:16:57 +02:00
parent 8da7e1f8e2
commit 17775274a0
6 changed files with 64 additions and 25 deletions

View file

@ -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

View file

@ -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 () => {

View file

@ -1416,28 +1416,25 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele
: undefined
}
headerExtra={
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className="pointer-events-auto size-6 p-0 text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
onClick={(e) => {
e.stopPropagation();
void window.electronAPI.openExternal(
'https://github.com/777genius/claude-notifications-go'
);
}}
>
<Bell size={12} />
</Button>
</TooltipTrigger>
<TooltipContent side="top">Desktop notifications plugin</TooltipContent>
</Tooltip>
}
defaultOpen
action={
<div className="flex items-center gap-2 pl-2">
<>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className="pointer-events-auto size-6 p-0 text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
onClick={(e) => {
e.stopPropagation();
void window.electronAPI.openExternal(
'https://github.com/777genius/claude-notifications-go'
);
}}
>
<Bell size={12} />
</Button>
</TooltipTrigger>
<TooltipContent side="top">Desktop notifications plugin</TooltipContent>
</Tooltip>
{messagesUnreadCount > 0 && (
<Tooltip>
<TooltipTrigger asChild>
@ -1455,6 +1452,11 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele
<TooltipContent side="bottom">Mark all as read</TooltipContent>
</Tooltip>
)}
</>
}
defaultOpen
action={
<div className="flex items-center gap-2 pl-2">
<div className="flex w-36 items-center gap-1.5 rounded-md border border-[var(--color-border)] bg-transparent px-2 py-1">
<Search size={12} className="shrink-0 text-[var(--color-text-muted)]" />
<input

View file

@ -290,10 +290,13 @@ export const ActivityTimeline = ({
const currSessionId = getItemSessionId(item);
if (prevSessionId && currSessionId && prevSessionId !== currSessionId) {
sessionSeparator = (
<div className="flex items-center gap-3 py-4">
<div
className="flex items-center gap-3"
style={{ paddingTop: 30, paddingBottom: 30 }}
>
<div className="h-px flex-1 bg-[var(--color-border-emphasis)]" />
<span className="whitespace-nowrap text-[11px] text-[var(--color-text-muted)]">
Новая сессия
New session
</span>
<div className="h-px flex-1 bg-[var(--color-border-emphasis)]" />
</div>

View file

@ -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 {

View file

@ -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<string, unknown>[];
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<string, unknown>[];
expect(persisted).toHaveLength(1);
expect(persisted[0]).not.toHaveProperty('source');
});
});