refactor: update comment formatting and notification handling in maintenance and tasks

- Modified comment notification formatting in tasks.js to enhance clarity by using bold text for task references.
- Updated the automated comment notification detection in maintenance.js to align with the new formatting.
- Adjusted test cases to reflect the updated comment structure for consistency in notifications.
This commit is contained in:
iliya 2026-03-14 22:10:16 +02:00
parent 92dd8f445f
commit a29d8403d6
8 changed files with 37 additions and 24 deletions

View file

@ -82,7 +82,7 @@ function isAutomatedCommentNotification(message) {
if (!text) return false; if (!text) return false;
if (text.includes('Reply to this comment using:')) return true; if (text.includes('Reply to this comment using:')) return true;
if (text.startsWith('Comment on task #')) return true; if (text.startsWith('**Comment on task')) return true;
if (text.startsWith('New comment from user on your task #')) return true; if (text.startsWith('New comment from user on your task #')) return true;
return false; return false;
} }

View file

@ -65,7 +65,8 @@ function buildAssignmentMessage(context, task, options = {}) {
function buildCommentNotificationMessage(context, task, comment) { function buildCommentNotificationMessage(context, task, comment) {
const taskLabel = `#${task.displayId || task.id}`; const taskLabel = `#${task.displayId || task.id}`;
return [ return [
`Comment on task ${taskLabel} "${task.subject}":`, `**Comment on task ${taskLabel}**`,
`> ${task.subject}`,
``, ``,
comment.text, comment.text,
``, ``,

View file

@ -416,7 +416,7 @@ describe('agent-teams-controller API', () => {
timestamp: '2026-02-23T11:00:00.000Z', timestamp: '2026-02-23T11:00:00.000Z',
read: false, read: false,
text: text:
`Comment on task #${task.displayId} "Ship migration":\n\nHeads up\n\n` + `**Comment on task #${task.displayId}**\n> Ship migration\n\nHeads up\n\n` +
'<agent-block>\nReply to this comment using:\nnode "tool.js" --team my-team task comment 1 --text "..." --from "bob"\n</agent-block>', '<agent-block>\nReply to this comment using:\nnode "tool.js" --team my-team task comment 1 --text "..." --from "bob"\n</agent-block>',
}, },
], ],

View file

@ -259,6 +259,7 @@ export class TeamMemberLogsFinder {
if (s) results.push(s); if (s) results.push(s);
} }
const totalFiles = candidates.length; const totalFiles = candidates.length;
const step2Count = results.length; // count before step 3 (owner fallback)
const tScan = performance.now(); const tScan = performance.now();
const normalizedOwner = const normalizedOwner =
@ -350,13 +351,11 @@ export class TeamMemberLogsFinder {
const tTotal = performance.now(); const tTotal = performance.now();
console.log( console.log(
`[perf] findLogsForTask(${taskId}@${teamName}) ` + `[findLogsForTask] task=${taskId}@${teamName} | ` +
`total=${(tTotal - t0).toFixed(0)}ms | ` + `step2=${step2Count} (scan ${mentionHits}/${totalFiles} files) | ` +
`discovery=${(tDiscovery - t0).toFixed(0)}ms | ` + `step3=${sorted.length - step2Count} (owner=${normalizedOwner ?? 'none'}, includeOwner=${includeOwnerSessions}) | ` +
`lead=${(tLead - tDiscovery).toFixed(0)}ms | ` + `total=${sorted.length} | ` +
`scan=${(tScan - tLead).toFixed(0)}ms (${totalFiles} files, ${mentionHits} hits) | ` + `${(tTotal - t0).toFixed(0)}ms`
`owner=${(tOwner - tScan).toFixed(0)}ms | ` +
`sessions=${sessionIds.length} | results=${sorted.length}`
); );
return sorted; return sorted;
@ -975,6 +974,12 @@ export class TeamMemberLogsFinder {
const b = block as Record<string, unknown>; const b = block as Record<string, unknown>;
if (b.type !== 'tool_use') continue; if (b.type !== 'tool_use') continue;
// Skip read-only task tools — they reference taskId but don't indicate
// that this session actually WORKED on the task. Agents commonly call
// task_get to check dependencies from other tasks, producing false matches.
const toolName = typeof b.name === 'string' ? b.name : '';
if (toolName === 'task_get' || toolName === 'mcp__agent-teams__task_get') continue;
const input = b.input as Record<string, unknown> | undefined; const input = b.input as Record<string, unknown> | undefined;
if (!input) continue; if (!input) continue;

View file

@ -812,6 +812,7 @@ export const MarkdownViewer: React.FC<MarkdownViewerProps> = ({
components={components} components={components}
urlTransform={allowCustomProtocols} urlTransform={allowCustomProtocols}
allowElement={isAllowedElement} allowElement={isAllowedElement}
unwrapDisallowed
> >
{content} {content}
</ReactMarkdown> </ReactMarkdown>

View file

@ -104,8 +104,8 @@ type KanbanViewMode = 'grid' | 'columns';
const COLUMNS: { id: KanbanColumnId; title: string }[] = [ const COLUMNS: { id: KanbanColumnId; title: string }[] = [
{ id: 'todo', title: 'TODO' }, { id: 'todo', title: 'TODO' },
{ id: 'in_progress', title: 'IN PROGRESS' }, { id: 'in_progress', title: 'IN PROGRESS' },
{ id: 'done', title: 'DONE' },
{ id: 'review', title: 'REVIEW' }, { id: 'review', title: 'REVIEW' },
{ id: 'done', title: 'DONE' },
{ id: 'approved', title: 'APPROVED' }, { id: 'approved', title: 'APPROVED' },
]; ];

View file

@ -23,7 +23,7 @@ const DEFAULT_ITEM_HEIGHT = Math.max(
); );
const DEFAULT_MIN_HEIGHT = 10; const DEFAULT_MIN_HEIGHT = 10;
const DEFAULT_MIN_WIDTH = 3; const DEFAULT_MIN_WIDTH = 3;
const GRID_SCOPE_KEY = 'kanban-grid-layout:global'; const GRID_SCOPE_KEY = 'kanban-grid-layout:global:v2';
const SKELETON_HIDE_DELAY_MS = 500; const SKELETON_HIDE_DELAY_MS = 500;
const RESIZE_HANDLES: ResizeHandleAxis[] = ['s', 'w', 'e', 'n', 'sw', 'nw', 'se', 'ne']; const RESIZE_HANDLES: ResizeHandleAxis[] = ['s', 'w', 'e', 'n', 'sw', 'nw', 'se', 'ne'];
const WidthAwareGridLayout = WidthProvider(ReactGridLayout); const WidthAwareGridLayout = WidthProvider(ReactGridLayout);
@ -54,16 +54,19 @@ interface LoadingKanbanGridLayoutProps {
readonly visibleItems: PersistedGridLayoutItem[]; readonly visibleItems: PersistedGridLayoutItem[];
} }
const ITEMS_PER_FIRST_ROW = 3;
const SECOND_ROW_ITEM_WIDTH = 6;
function buildDefaultItems(itemIds: string[]): PersistedGridLayoutItem[] { function buildDefaultItems(itemIds: string[]): PersistedGridLayoutItem[] {
return itemIds.map((id, index) => ({ return itemIds.map((id, index) => {
id, const isSecondRow = index >= ITEMS_PER_FIRST_ROW;
x: (index % 3) * DEFAULT_ITEM_WIDTH, const w = isSecondRow ? SECOND_ROW_ITEM_WIDTH : DEFAULT_ITEM_WIDTH;
y: Math.floor(index / 3) * DEFAULT_ITEM_HEIGHT, const x = isSecondRow
w: DEFAULT_ITEM_WIDTH, ? (index - ITEMS_PER_FIRST_ROW) * SECOND_ROW_ITEM_WIDTH
h: DEFAULT_ITEM_HEIGHT, : index * DEFAULT_ITEM_WIDTH;
minW: DEFAULT_MIN_WIDTH, const y = isSecondRow ? DEFAULT_ITEM_HEIGHT : 0;
minH: DEFAULT_MIN_HEIGHT, return { id, x, y, w, h: DEFAULT_ITEM_HEIGHT, minW: DEFAULT_MIN_WIDTH, minH: DEFAULT_MIN_HEIGHT };
})); });
} }
function toReactGridLayoutItem(item: PersistedGridLayoutItem): LayoutItem { function toReactGridLayoutItem(item: PersistedGridLayoutItem): LayoutItem {

View file

@ -188,6 +188,9 @@ export const MentionSuggestionList = ({
> >
<HighlightedName name={isTask ? `#${s.name}` : s.name} query={query} /> <HighlightedName name={isTask ? `#${s.name}` : s.name} query={query} />
</span> </span>
{!isTask && !isFileOrFolder && s.subtitle ? (
<span className="truncate text-[var(--color-text-muted)]">{s.subtitle}</span>
) : null}
{isTask && s.ownerName ? ( {isTask && s.ownerName ? (
<MemberBadge name={s.ownerName} color={s.ownerColor} size="xs" disableHoverCard /> <MemberBadge name={s.ownerName} color={s.ownerColor} size="xs" disableHoverCard />
) : null} ) : null}
@ -213,12 +216,12 @@ export const MentionSuggestionList = ({
title={s.isOnline ? 'Online' : 'Offline'} title={s.isOnline ? 'Online' : 'Offline'}
/> />
) : null} ) : null}
{s.subtitle && !isTask ? ( {s.subtitle && isFileOrFolder ? (
<span <span
className="truncate text-[var(--color-text-muted)]" className="truncate text-[var(--color-text-muted)]"
style={isFileOrFolder ? { direction: 'rtl', textAlign: 'left' } : undefined} style={{ direction: 'rtl', textAlign: 'left' }}
> >
{isFileOrFolder ? '\u200E' + s.subtitle : s.subtitle} {'\u200E' + s.subtitle}
</span> </span>
) : null} ) : null}
</li> </li>