feat(team): add support for task comment notifications

- Introduced 'task_comment_notification' message kind to enhance message handling in the team services.
- Updated TeamDataService, TeamInboxReader, and TeamSentMessagesStore to accommodate the new message kind.
- Modified filtering logic to exclude task comment notifications from the displayed messages.
- Added tests to ensure correct handling and filtering of task comment notifications.
This commit is contained in:
iliya 2026-03-29 01:29:13 +02:00
parent 46355d87df
commit 7ea8289c5b
9 changed files with 74 additions and 7 deletions

View file

@ -307,6 +307,7 @@ pnpm dist # macOS + Windows + Linux
- [ ] `createTasksBatch` — IPC/service API to create many team tasks in one call (playbooks, markdown checklist import, scripts); complements single `createTask`
- [ ] Command palette — extend Cmd/Ctrl+K beyond project/session search to runnable actions (quick commands, navigation shortcuts, team/task operations) in a keyboard-first flow
- [ ] Custom kanban columns
- [ ] Run terminal commands
---

View file

@ -30,6 +30,7 @@ import * as path from 'path';
import { gitIdentityResolver } from '../parsing/GitIdentityResolver';
import { atomicWriteAsync } from './atomicWrite';
import { extractLeadSessionMessagesFromJsonl } from './leadSessionMessageExtractor';
import { buildTaskChangePresenceDescriptor } from './taskChangePresenceUtils';
import { TeamConfigReader } from './TeamConfigReader';
import { TeamInboxReader } from './TeamInboxReader';
@ -42,7 +43,6 @@ import { TeamSentMessagesStore } from './TeamSentMessagesStore';
import { TeamTaskCommentNotificationJournal } from './TeamTaskCommentNotificationJournal';
import { TeamTaskReader } from './TeamTaskReader';
import { TeamTaskWriter } from './TeamTaskWriter';
import { extractLeadSessionMessagesFromJsonl } from './leadSessionMessageExtractor';
import type { PersistedTaskChangePresenceIndex } from './cache/taskChangePresenceCacheTypes';
import type { TaskChangePresenceRepository } from './cache/TaskChangePresenceRepository';
@ -1806,6 +1806,7 @@ export class TeamDataService {
text: notification.text,
summary: notification.summary,
source: TASK_COMMENT_NOTIFICATION_SOURCE,
messageKind: 'task_comment_notification',
leadSessionId: notification.leadSessionId,
taskRefs: [notification.taskRef],
messageId: notification.messageId,

View file

@ -132,7 +132,9 @@ export class TeamInboxReader {
}))
: undefined,
messageKind:
row.messageKind === 'slash_command' || row.messageKind === 'slash_command_result'
row.messageKind === 'slash_command' ||
row.messageKind === 'slash_command_result' ||
row.messageKind === 'task_comment_notification'
? row.messageKind
: row.messageKind === 'default'
? 'default'
@ -144,7 +146,7 @@ export class TeamInboxReader {
typeof row.slashCommand.command === 'string'
? {
name: row.slashCommand.name,
command: row.slashCommand.command as `/${string}`,
command: row.slashCommand.command,
args: typeof row.slashCommand.args === 'string' ? row.slashCommand.args : undefined,
knownDescription:
typeof row.slashCommand.knownDescription === 'string'

View file

@ -99,7 +99,9 @@ export class TeamSentMessagesStore {
}))
: undefined,
messageKind:
row.messageKind === 'slash_command' || row.messageKind === 'slash_command_result'
row.messageKind === 'slash_command' ||
row.messageKind === 'slash_command_result' ||
row.messageKind === 'task_comment_notification'
? row.messageKind
: row.messageKind === 'default'
? 'default'
@ -111,7 +113,7 @@ export class TeamSentMessagesStore {
typeof row.slashCommand.command === 'string'
? {
name: row.slashCommand.name,
command: row.slashCommand.command as `/${string}`,
command: row.slashCommand.command,
args: typeof row.slashCommand.args === 'string' ? row.slashCommand.args : undefined,
knownDescription:
typeof row.slashCommand.knownDescription === 'string'

View file

@ -18,7 +18,7 @@ export function filterTeamMessages(
): InboxMessage[] {
const { timeWindow, filter, searchQuery } = options;
let list = messages;
let list = messages.filter((m) => m.messageKind !== 'task_comment_notification');
if (timeWindow) {
list = list.filter((m) => {
const ts = new Date(m.timestamp).getTime();

View file

@ -166,7 +166,11 @@ export interface SourceMessageSnapshot {
}[];
}
export type InboxMessageKind = 'default' | 'slash_command' | 'slash_command_result';
export type InboxMessageKind =
| 'default'
| 'slash_command'
| 'slash_command_result'
| 'task_comment_notification';
export interface SlashCommandMeta {
name: string;

View file

@ -789,6 +789,7 @@ describe('TeamDataService', () => {
from: 'alice',
summary: 'Comment on #abcd1234',
source: 'system_notification',
messageKind: 'task_comment_notification',
leadSessionId: 'lead-1',
taskRefs: [{ taskId: 'task-1', displayId: 'abcd1234', teamName: 'my-team' }],
messageId: 'task-comment-forward:my-team:task-1:comment-1',
@ -1446,6 +1447,7 @@ describe('TeamDataService', () => {
expect.objectContaining({
from: 'bob',
summary: 'Comment on #abcd1234',
messageKind: 'task_comment_notification',
messageId: 'task-comment-forward:my-team:task-1:comment-2',
})
);

View file

@ -150,4 +150,32 @@ describe('TeamInboxReader', () => {
expect(supported).toBeDefined();
expect(supported!.messageId).toBe('m-1');
});
it('preserves task comment notification semantic kind', async () => {
hoisted.files.set(
'/mock/teams/my-team/inboxes/alice.json',
JSON.stringify([
{
from: 'bob',
to: 'team-lead',
text: 'Notification payload',
timestamp: '2026-01-01T02:00:00.000Z',
read: false,
messageId: 'm-task-comment',
source: 'system_notification',
messageKind: 'task_comment_notification',
summary: 'Comment on #abcd1234',
},
])
);
const messages = await reader.getMessagesFor('my-team', 'alice');
expect(messages).toHaveLength(1);
expect(messages[0]).toMatchObject({
messageId: 'm-task-comment',
source: 'system_notification',
messageKind: 'task_comment_notification',
summary: 'Comment on #abcd1234',
});
});
});

View file

@ -105,4 +105,31 @@ describe('filterTeamMessages', () => {
expect(result).toHaveLength(1);
expect(result[0].messageId).toBe('msg-2');
});
it('hides task comment notifications by semantic kind instead of text matching', () => {
const messages = [
makeMessage({
messageId: 'task-comment-1',
source: 'system_notification',
messageKind: 'task_comment_notification',
summary: 'Comment on #abcd1234',
text: 'Some future wording that may change completely.',
}),
makeMessage({
messageId: 'msg-2',
source: 'system_notification',
summary: 'Task #abcd1234 started',
text: 'Visible system notification',
}),
];
const result = filterTeamMessages(messages, {
timeWindow: null,
filter: { from: new Set(), to: new Set(), showNoise: true },
searchQuery: '',
});
expect(result).toHaveLength(1);
expect(result[0].messageId).toBe('msg-2');
});
});