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:
parent
8da7e1f8e2
commit
17775274a0
6 changed files with 64 additions and 25 deletions
|
|
@ -862,6 +862,7 @@ export class TeamDataService {
|
||||||
from: leadName,
|
from: leadName,
|
||||||
text: parts.join('\n'),
|
text: parts.join('\n'),
|
||||||
summary: `New task #${task.id} assigned`,
|
summary: `New task #${task.id} assigned`,
|
||||||
|
source: 'system_notification',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -906,6 +907,7 @@ export class TeamDataService {
|
||||||
from: leadName,
|
from: leadName,
|
||||||
text: parts.join('\n'),
|
text: parts.join('\n'),
|
||||||
summary: `Task #${task.id} started`,
|
summary: `Task #${task.id} started`,
|
||||||
|
source: 'system_notification',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -961,6 +963,7 @@ export class TeamDataService {
|
||||||
from: last.actor,
|
from: last.actor,
|
||||||
text: `Task #${task.id} "${task.subject}" has been started by ${last.actor}.`,
|
text: `Task #${task.id} "${task.subject}" has been started by ${last.actor}.`,
|
||||||
summary: `Task #${task.id} started`,
|
summary: `Task #${task.id} started`,
|
||||||
|
source: 'system_notification',
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn(`[TeamDataService] notifyLeadOnTeammateTaskStart failed: ${String(error)}`);
|
logger.warn(`[TeamDataService] notifyLeadOnTeammateTaskStart failed: ${String(error)}`);
|
||||||
|
|
@ -1072,6 +1075,7 @@ export class TeamDataService {
|
||||||
from: leadName,
|
from: leadName,
|
||||||
text: parts.join('\n'),
|
text: parts.join('\n'),
|
||||||
summary: `Comment on #${taskId}`,
|
summary: `Comment on #${taskId}`,
|
||||||
|
source: 'system_notification',
|
||||||
});
|
});
|
||||||
} else if (task && owner && this.isLeadOwner(owner, leadName)) {
|
} else if (task && owner && this.isLeadOwner(owner, leadName)) {
|
||||||
// Notify lead about user's comment on their own task.
|
// Notify lead about user's comment on their own task.
|
||||||
|
|
@ -1088,6 +1092,7 @@ export class TeamDataService {
|
||||||
from: 'user',
|
from: 'user',
|
||||||
text: parts.join('\n'),
|
text: parts.join('\n'),
|
||||||
summary: `Comment on #${taskId}`,
|
summary: `Comment on #${taskId}`,
|
||||||
|
source: 'system_notification',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -1208,6 +1213,7 @@ export class TeamDataService {
|
||||||
`node "${toolPath}" --team ${teamName} review request-changes ${taskId} --comment "..."\n` +
|
`node "${toolPath}" --team ${teamName} review request-changes ${taskId} --comment "..."\n` +
|
||||||
AGENT_BLOCK_CLOSE,
|
AGENT_BLOCK_CLOSE,
|
||||||
summary: `Review request for #${taskId}`,
|
summary: `Review request for #${taskId}`,
|
||||||
|
source: 'system_notification',
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await this.kanbanManager
|
await this.kanbanManager
|
||||||
|
|
@ -1307,6 +1313,7 @@ export class TeamDataService {
|
||||||
for (const msg of messages) {
|
for (const msg of messages) {
|
||||||
if (!msg.messageId || !msg.summary || msg.from === 'user') continue;
|
if (!msg.messageId || !msg.summary || msg.from === 'user') continue;
|
||||||
if (msg.source === 'lead_session' || msg.source === 'lead_process') continue;
|
if (msg.source === 'lead_session' || msg.source === 'lead_process') continue;
|
||||||
|
if (msg.source === 'system_notification') continue;
|
||||||
if (isAutomatedCommentNotification(msg)) continue;
|
if (isAutomatedCommentNotification(msg)) continue;
|
||||||
|
|
||||||
const textKey = `${msg.from}\0${msg.text}`;
|
const textKey = `${msg.from}\0${msg.text}`;
|
||||||
|
|
@ -1490,6 +1497,7 @@ export class TeamDataService {
|
||||||
`${patch.comment?.trim() || 'Reviewer requested changes.'}\n\n` +
|
`${patch.comment?.trim() || 'Reviewer requested changes.'}\n\n` +
|
||||||
`Please fix and mark it as completed when ready.`,
|
`Please fix and mark it as completed when ready.`,
|
||||||
summary: `Fix request for #${taskId}`,
|
summary: `Fix request for #${taskId}`,
|
||||||
|
source: 'system_notification',
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await this.taskWriter
|
await this.taskWriter
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ export class TeamInboxWriter {
|
||||||
summary: request.summary,
|
summary: request.summary,
|
||||||
messageId,
|
messageId,
|
||||||
attachments: attachmentMeta?.length ? attachmentMeta : undefined,
|
attachments: attachmentMeta?.length ? attachmentMeta : undefined,
|
||||||
|
...(request.source && { source: request.source }),
|
||||||
};
|
};
|
||||||
|
|
||||||
await withInboxLock(inboxPath, async () => {
|
await withInboxLock(inboxPath, async () => {
|
||||||
|
|
|
||||||
|
|
@ -1416,28 +1416,25 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
headerExtra={
|
headerExtra={
|
||||||
<Tooltip>
|
<>
|
||||||
<TooltipTrigger asChild>
|
<Tooltip>
|
||||||
<Button
|
<TooltipTrigger asChild>
|
||||||
variant="ghost"
|
<Button
|
||||||
size="sm"
|
variant="ghost"
|
||||||
className="pointer-events-auto size-6 p-0 text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
|
size="sm"
|
||||||
onClick={(e) => {
|
className="pointer-events-auto size-6 p-0 text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
|
||||||
e.stopPropagation();
|
onClick={(e) => {
|
||||||
void window.electronAPI.openExternal(
|
e.stopPropagation();
|
||||||
'https://github.com/777genius/claude-notifications-go'
|
void window.electronAPI.openExternal(
|
||||||
);
|
'https://github.com/777genius/claude-notifications-go'
|
||||||
}}
|
);
|
||||||
>
|
}}
|
||||||
<Bell size={12} />
|
>
|
||||||
</Button>
|
<Bell size={12} />
|
||||||
</TooltipTrigger>
|
</Button>
|
||||||
<TooltipContent side="top">Desktop notifications plugin</TooltipContent>
|
</TooltipTrigger>
|
||||||
</Tooltip>
|
<TooltipContent side="top">Desktop notifications plugin</TooltipContent>
|
||||||
}
|
</Tooltip>
|
||||||
defaultOpen
|
|
||||||
action={
|
|
||||||
<div className="flex items-center gap-2 pl-2">
|
|
||||||
{messagesUnreadCount > 0 && (
|
{messagesUnreadCount > 0 && (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
|
|
@ -1455,6 +1452,11 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele
|
||||||
<TooltipContent side="bottom">Mark all as read</TooltipContent>
|
<TooltipContent side="bottom">Mark all as read</TooltipContent>
|
||||||
</Tooltip>
|
</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">
|
<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)]" />
|
<Search size={12} className="shrink-0 text-[var(--color-text-muted)]" />
|
||||||
<input
|
<input
|
||||||
|
|
|
||||||
|
|
@ -290,10 +290,13 @@ export const ActivityTimeline = ({
|
||||||
const currSessionId = getItemSessionId(item);
|
const currSessionId = getItemSessionId(item);
|
||||||
if (prevSessionId && currSessionId && prevSessionId !== currSessionId) {
|
if (prevSessionId && currSessionId && prevSessionId !== currSessionId) {
|
||||||
sessionSeparator = (
|
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)]" />
|
<div className="h-px flex-1 bg-[var(--color-border-emphasis)]" />
|
||||||
<span className="whitespace-nowrap text-[11px] text-[var(--color-text-muted)]">
|
<span className="whitespace-nowrap text-[11px] text-[var(--color-text-muted)]">
|
||||||
Новая сессия
|
New session
|
||||||
</span>
|
</span>
|
||||||
<div className="h-px flex-1 bg-[var(--color-border-emphasis)]" />
|
<div className="h-px flex-1 bg-[var(--color-border-emphasis)]" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -195,7 +195,7 @@ export interface InboxMessage {
|
||||||
summary?: string;
|
summary?: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
messageId?: string;
|
messageId?: string;
|
||||||
source?: 'inbox' | 'lead_session' | 'lead_process' | 'user_sent';
|
source?: 'inbox' | 'lead_session' | 'lead_process' | 'user_sent' | 'system_notification';
|
||||||
attachments?: AttachmentMeta[];
|
attachments?: AttachmentMeta[];
|
||||||
/** Lead session ID that produced this message (for session boundary detection). */
|
/** Lead session ID that produced this message (for session boundary detection). */
|
||||||
leadSessionId?: string;
|
leadSessionId?: string;
|
||||||
|
|
@ -207,6 +207,7 @@ export interface SendMessageRequest {
|
||||||
summary?: string;
|
summary?: string;
|
||||||
from?: string;
|
from?: string;
|
||||||
attachments?: AttachmentPayload[];
|
attachments?: AttachmentPayload[];
|
||||||
|
source?: InboxMessage['source'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SendMessageResult {
|
export interface SendMessageResult {
|
||||||
|
|
|
||||||
|
|
@ -130,4 +130,28 @@ describe('TeamInboxWriter', () => {
|
||||||
expect(persisted).toHaveLength(2);
|
expect(persisted).toHaveLength(2);
|
||||||
expect(persisted.map((row) => row.text).sort()).toEqual(['first', 'second']);
|
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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue