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.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;
return false;
}

View file

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

View file

@ -416,7 +416,7 @@ describe('agent-teams-controller API', () => {
timestamp: '2026-02-23T11:00:00.000Z',
read: false,
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>',
},
],

View file

@ -259,6 +259,7 @@ export class TeamMemberLogsFinder {
if (s) results.push(s);
}
const totalFiles = candidates.length;
const step2Count = results.length; // count before step 3 (owner fallback)
const tScan = performance.now();
const normalizedOwner =
@ -350,13 +351,11 @@ export class TeamMemberLogsFinder {
const tTotal = performance.now();
console.log(
`[perf] findLogsForTask(${taskId}@${teamName}) ` +
`total=${(tTotal - t0).toFixed(0)}ms | ` +
`discovery=${(tDiscovery - t0).toFixed(0)}ms | ` +
`lead=${(tLead - tDiscovery).toFixed(0)}ms | ` +
`scan=${(tScan - tLead).toFixed(0)}ms (${totalFiles} files, ${mentionHits} hits) | ` +
`owner=${(tOwner - tScan).toFixed(0)}ms | ` +
`sessions=${sessionIds.length} | results=${sorted.length}`
`[findLogsForTask] task=${taskId}@${teamName} | ` +
`step2=${step2Count} (scan ${mentionHits}/${totalFiles} files) | ` +
`step3=${sorted.length - step2Count} (owner=${normalizedOwner ?? 'none'}, includeOwner=${includeOwnerSessions}) | ` +
`total=${sorted.length} | ` +
`${(tTotal - t0).toFixed(0)}ms`
);
return sorted;
@ -975,6 +974,12 @@ export class TeamMemberLogsFinder {
const b = block as Record<string, unknown>;
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;
if (!input) continue;

View file

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

View file

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

View file

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

View file

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