diff --git a/src/renderer/components/team/activity/ActivityItem.tsx b/src/renderer/components/team/activity/ActivityItem.tsx
index 429cbc27..86d75b5b 100644
--- a/src/renderer/components/team/activity/ActivityItem.tsx
+++ b/src/renderer/components/team/activity/ActivityItem.tsx
@@ -6,7 +6,12 @@ import { AttachmentDisplay } from '@renderer/components/team/attachments/Attachm
import { MemberBadge } from '@renderer/components/team/MemberBadge';
import { TaskTooltip } from '@renderer/components/team/TaskTooltip';
import { ExpandableContent } from '@renderer/components/ui/ExpandableContent';
-import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from '@renderer/components/ui/tooltip';
import {
CARD_BG,
CARD_BG_ZEBRA,
@@ -780,7 +785,7 @@ export const ActivityItem = memo(
if (!isCrossTeamAny || !strippedText) return '';
const oneLine = strippedText.replace(/\n+/g, ' ').trim();
if (!oneLine) return '';
- return oneLine.length > 80 ? oneLine.slice(0, 80) + '…' : oneLine;
+ return oneLine;
}, [isCrossTeamAny, strippedText]);
const rawSummary = useMemo(() => {
@@ -806,8 +811,7 @@ export const ActivityItem = memo(
// Fallback: use the beginning of message text as preview for plain-text messages
const plain = getSanitizedInboxMessageText(message).trim();
if (!plain) return '';
- const oneLine = plain.replace(/\n+/g, ' ');
- return oneLine.length > 80 ? oneLine.slice(0, 80) + '…' : oneLine;
+ return plain.replace(/\n+/g, ' ');
}, [
crossTeamPreview,
isSlashCommandMessage,
@@ -819,6 +823,39 @@ export const ActivityItem = memo(
structured,
]);
const summaryText = extractMarkdownPlainText(rawSummary);
+ const compactPreviewText = useMemo(() => {
+ if (idleSemantic?.hasPeerSummary && idleSemantic.peerSummary) {
+ return idleSemantic.peerSummary;
+ }
+ if (isSlashCommandResult && message.commandOutput) {
+ return message.summary || getCommandOutputSummary(message.text);
+ }
+ if (isSlashCommandMessage && slashCommandMeta) {
+ if (slashCommandMeta.args) {
+ const oneLine = slashCommandMeta.args.replace(/\n+/g, ' ').trim();
+ return `${slashCommandMeta.command} ${oneLine}`;
+ }
+ return slashCommandMeta.command;
+ }
+ if (crossTeamPreview) return crossTeamPreview;
+
+ const fullText = strippedText?.trim() ?? '';
+ if (fullText) {
+ return extractMarkdownPlainText(fullText).replace(/\n+/g, ' ').trim();
+ }
+
+ return summaryText || rawSummary;
+ }, [
+ crossTeamPreview,
+ idleSemantic,
+ isSlashCommandMessage,
+ isSlashCommandResult,
+ message,
+ message.commandOutput,
+ rawSummary,
+ slashCommandMeta,
+ summaryText,
+ ]);
const commentTaskRef =
message.messageKind === 'task_comment_notification' ? (message.taskRefs?.[0] ?? null) : null;
const commentTaskDisplayId =
@@ -1178,13 +1215,105 @@ export const ActivityItem = memo(
)}
-
- {summaryContent}
+
+
+
+
+ {compactPreviewText}
+
+
+
+ {compactPreviewText}
+
+
+
+
+ ) : !isExpanded ? (
+
+
+ {isUnread ? (
+
+ ) : null}
+ {showChevron ? (
+
+ ) : null}
+ {crossTeamOrigin ? (
+
+ ) : null}
+ {senderBadge}
+ {!compactHeader && formattedRole && !isSlashCommandResult ? (
+
+ {formattedRole}
+
+ ) : null}
+ {messageTypeBadge}
+ {leadSourceBadge}
+ {statusBadge}
+ {recipientBadge}
+
+
+ {timestamp}
+
+ {onExpand && expandItemKey && (
+
+ )}
+
+
+
+
+
+ {compactPreviewText}
+
+
+
+ {compactPreviewText}
+
+
+
) : (
<>
diff --git a/src/renderer/components/team/activity/LeadThoughtsGroup.tsx b/src/renderer/components/team/activity/LeadThoughtsGroup.tsx
index 6943a68b..31295a3b 100644
--- a/src/renderer/components/team/activity/LeadThoughtsGroup.tsx
+++ b/src/renderer/components/team/activity/LeadThoughtsGroup.tsx
@@ -10,7 +10,12 @@ import {
} from 'react';
import { MemberBadge } from '@renderer/components/team/MemberBadge';
-import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from '@renderer/components/ui/tooltip';
import {
CARD_BG,
CARD_BG_ZEBRA,
@@ -39,6 +44,7 @@ import {
} from './AnimatedHeightReveal';
import { ThoughtBodyContent } from './ThoughtBodyContent';
+import { stripAgentBlocks } from '@shared/constants/agentBlocks';
import type { InboxMessage, ToolCallMeta } from '@shared/types';
export interface LeadThoughtGroup {
@@ -587,9 +593,11 @@ const LeadThoughtsGroupRowComponent = ({
// Try newest first (most relevant), then scan for any text
for (const t of thoughts) {
if (t.text && t.text.trim()) {
- const plain = extractMarkdownPlainText(t.text);
- const firstLine = plain.split('\n').find((l) => l.trim().length > 0) ?? '';
- return firstLine.trim();
+ const plain = extractMarkdownPlainText(stripAgentBlocks(t.text));
+ const normalized = plain.replace(/\n+/g, ' ').trim();
+ if (normalized) {
+ return normalized;
+ }
}
}
return null;
@@ -830,13 +838,108 @@ const LeadThoughtsGroupRowComponent = ({
{compactPreviewText ? (
-
- {compactPreviewText}
+
+
+
+
+ {compactPreviewText}
+
+
+
+ {compactPreviewText}
+
+
+
+ ) : null}
+
+ ) : !isBodyVisible ? (
+
+
+ {canToggleBodyVisibility && !compactHeader ? (
+
+ ) : null}
+ {!compactHeader ? (
+
+

+
+
+ ) : null}
+
+
+ {thoughts.length} thoughts
+
+
+
+ {timestampLabel}
+
+ {onExpand && expandItemKey && (
+
+ )}
+
+ {compactPreviewText ? (
+
+
+
+
+ {compactPreviewText}
+
+
+
+ {compactPreviewText}
+
+
+
) : null}
) : (
@@ -871,26 +974,7 @@ const LeadThoughtsGroupRowComponent = ({
{thoughts.length} thoughts
- {!isBodyVisible && headerTextPreview ? (
-
-
-
- {headerTextPreview}
-
-
- {totalToolSummary ? (
-
-
-
- ) : null}
-
- ) : totalToolSummary ? (
+ {totalToolSummary ? (
diff --git a/src/renderer/components/team/kanban/KanbanTaskCard.test.tsx b/src/renderer/components/team/kanban/KanbanTaskCard.test.tsx
new file mode 100644
index 00000000..4867111d
--- /dev/null
+++ b/src/renderer/components/team/kanban/KanbanTaskCard.test.tsx
@@ -0,0 +1,161 @@
+import React, { act } from 'react';
+import { createRoot } from 'react-dom/client';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+
+vi.mock('@renderer/components/team/MemberBadge', () => ({
+ MemberBadge: ({ name }: { name: string }) => React.createElement('span', null, name),
+}));
+
+vi.mock('@renderer/components/team/UnreadCommentsBadge', () => ({
+ UnreadCommentsBadge: () => null,
+}));
+
+vi.mock('@renderer/components/ui/button', () => ({
+ Button: ({
+ children,
+ className,
+ onClick,
+ disabled,
+ 'aria-label': ariaLabel,
+ }: {
+ children: React.ReactNode;
+ className?: string;
+ onClick?: React.MouseEventHandler;
+ disabled?: boolean;
+ 'aria-label'?: string;
+ }) =>
+ React.createElement(
+ 'button',
+ { className, onClick, disabled, 'aria-label': ariaLabel, type: 'button' },
+ children
+ ),
+}));
+
+vi.mock('@renderer/components/ui/popover', () => ({
+ Popover: ({ children }: { children: React.ReactNode }) =>
+ React.createElement(React.Fragment, null, children),
+ PopoverTrigger: ({ children }: { children: React.ReactNode }) =>
+ React.createElement(React.Fragment, null, children),
+ PopoverContent: ({ children }: { children: React.ReactNode }) =>
+ React.createElement('div', null, children),
+}));
+
+vi.mock('@renderer/components/ui/tooltip', () => ({
+ Tooltip: ({ children }: { children: React.ReactNode }) =>
+ React.createElement(React.Fragment, null, children),
+ TooltipTrigger: ({ children }: { children: React.ReactNode }) =>
+ React.createElement(React.Fragment, null, children),
+ TooltipContent: ({ children }: { children: React.ReactNode }) =>
+ React.createElement('div', null, children),
+}));
+
+vi.mock('@renderer/hooks/useTheme', () => ({
+ useTheme: () => ({ isLight: false }),
+}));
+
+vi.mock('@renderer/hooks/useUnreadCommentCount', () => ({
+ useUnreadCommentCount: () => 0,
+}));
+
+import { KanbanTaskCard } from './KanbanTaskCard';
+
+import type { TeamTaskWithKanban } from '@shared/types/team';
+
+const baseTask: TeamTaskWithKanban = {
+ id: 'task-1',
+ displayId: 'abcd1234',
+ subject: 'Implement safer onboarding flow',
+ owner: 'alice',
+ reviewer: '',
+ status: 'in_progress',
+ changePresence: 'unknown',
+ comments: [],
+ blockedBy: [],
+ blocks: [],
+ workIntervals: [],
+ historyEvents: [],
+ createdAt: '2026-04-18T10:00:00.000Z',
+ updatedAt: '2026-04-18T10:10:00.000Z',
+} as unknown as TeamTaskWithKanban;
+
+const noop = (): void => undefined;
+
+describe('KanbanTaskCard change badge', () => {
+ afterEach(() => {
+ document.body.innerHTML = '';
+ });
+
+ it('does not render a No changes badge when changePresence is no_changes', async () => {
+ vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
+ const host = document.createElement('div');
+ document.body.appendChild(host);
+ const root = createRoot(host);
+
+ await act(async () => {
+ root.render(
+ React.createElement(KanbanTaskCard, {
+ task: { ...baseTask, changePresence: 'no_changes' },
+ teamName: 'my-team',
+ columnId: 'in_progress',
+ hasReviewers: true,
+ compact: false,
+ taskMap: new Map(),
+ memberColorMap: new Map([['alice', 'blue']]),
+ onRequestReview: noop,
+ onApprove: noop,
+ onRequestChanges: noop,
+ onMoveBackToDone: noop,
+ onStartTask: noop,
+ onCompleteTask: noop,
+ onCancelTask: noop,
+ onViewChanges: noop,
+ })
+ );
+ await Promise.resolve();
+ });
+
+ expect(host.textContent).not.toContain('No changes');
+
+ await act(async () => {
+ root.unmount();
+ await Promise.resolve();
+ });
+ });
+
+ it('still renders the Changes action when changePresence is has_changes', async () => {
+ vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
+ const host = document.createElement('div');
+ document.body.appendChild(host);
+ const root = createRoot(host);
+
+ await act(async () => {
+ root.render(
+ React.createElement(KanbanTaskCard, {
+ task: { ...baseTask, changePresence: 'has_changes' },
+ teamName: 'my-team',
+ columnId: 'in_progress',
+ hasReviewers: true,
+ compact: false,
+ taskMap: new Map(),
+ memberColorMap: new Map([['alice', 'blue']]),
+ onRequestReview: noop,
+ onApprove: noop,
+ onRequestChanges: noop,
+ onMoveBackToDone: noop,
+ onStartTask: noop,
+ onCompleteTask: noop,
+ onCancelTask: noop,
+ onViewChanges: noop,
+ })
+ );
+ await Promise.resolve();
+ });
+
+ expect(host.querySelector('[aria-label="Changes"]')).not.toBeNull();
+
+ await act(async () => {
+ root.unmount();
+ await Promise.resolve();
+ });
+ });
+});
diff --git a/src/renderer/components/team/kanban/KanbanTaskCard.tsx b/src/renderer/components/team/kanban/KanbanTaskCard.tsx
index 87933fb6..7c84488d 100644
--- a/src/renderer/components/team/kanban/KanbanTaskCard.tsx
+++ b/src/renderer/components/team/kanban/KanbanTaskCard.tsx
@@ -268,10 +268,6 @@ export const KanbanTaskCard = memo(
onViewChanges!(task.id);
}}
/>
- ) : canDisplay && task.changePresence === 'no_changes' ? (
-
- No changes
-
) : null}
{onDeleteTask ? (
diff --git a/src/renderer/components/team/members/CurrentTaskIndicator.tsx b/src/renderer/components/team/members/CurrentTaskIndicator.tsx
index e4df93f6..d168359a 100644
--- a/src/renderer/components/team/members/CurrentTaskIndicator.tsx
+++ b/src/renderer/components/team/members/CurrentTaskIndicator.tsx
@@ -6,7 +6,6 @@ import type { TeamTaskWithKanban } from '@shared/types';
interface CurrentTaskIndicatorProps {
task: TeamTaskWithKanban;
borderColor: string;
- /** Max characters for the subject before truncating */
maxSubjectLength?: number;
activityLabel?: string;
onOpenTask?: () => void;
@@ -19,21 +18,24 @@ interface CurrentTaskIndicatorProps {
export const CurrentTaskIndicator = ({
task,
borderColor,
- maxSubjectLength = 36,
+ maxSubjectLength,
activityLabel = 'working on',
onOpenTask,
}: CurrentTaskIndicatorProps): React.JSX.Element => {
- const truncated = task.subject.length > maxSubjectLength;
- const subjectText = truncated ? `${task.subject.slice(0, maxSubjectLength)}…` : task.subject;
+ const subjectText =
+ typeof maxSubjectLength === 'number' &&
+ maxSubjectLength > 0 &&
+ task.subject.length > maxSubjectLength
+ ? `${task.subject.slice(0, maxSubjectLength)}…`
+ : task.subject;
return (
- <>
+
{activityLabel}
- >
+
);
};
diff --git a/src/renderer/components/team/members/MemberCard.tsx b/src/renderer/components/team/members/MemberCard.tsx
index bcf7ebc2..5ea2e0fe 100644
--- a/src/renderer/components/team/members/MemberCard.tsx
+++ b/src/renderer/components/team/members/MemberCard.tsx
@@ -1,6 +1,6 @@
import { Badge } from '@renderer/components/ui/badge';
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
-import { getTeamColorSet, getThemedBadge, scaleColorAlpha } from '@renderer/constants/teamColors';
+import { getTeamColorSet } from '@renderer/constants/teamColors';
import { useTheme } from '@renderer/hooks/useTheme';
import { formatAgentRole } from '@renderer/utils/formatAgentRole';
import {
@@ -101,6 +101,7 @@ export const MemberCard = ({
const completed = taskCounts?.completed ?? 0;
const totalTasks = pending + inProgress + completed;
const progressPercent = totalTasks > 0 ? Math.round((completed / totalTasks) * 100) : 0;
+ const roleLabel = formatAgentRole(member.role) ?? formatAgentRole(member.agentType);
const activityTask = currentTask ?? reviewTask ?? null;
const activityTitle = currentTask
? `Current task: #${deriveTaskDisplayId(currentTask.id)}`
@@ -120,18 +121,14 @@ export const MemberCard = ({
!showStartingBadge &&
spawnStatus !== 'error' &&
(Boolean(activityTask) || !isAwaitingReply);
- const cardTint = scaleColorAlpha(getThemedBadge(colors, isLight), 0.5);
return (
-
})
+
+
})
+
-
+
{displayMemberName(member.name)}
@@ -210,20 +215,16 @@ export const MemberCard = ({
style={{ backgroundColor: 'var(--skeleton-base)' }}
/>
- ) : runtimeSummary ? (
-
- {runtimeSummary}
+ ) : runtimeSummary || roleLabel ? (
+
+ {runtimeSummary ? {runtimeSummary} : null}
+ {runtimeSummary && roleLabel ? (
+ •
+ ) : null}
+ {roleLabel ? {roleLabel} : null}
) : null}
- {(() => {
- const roleLabel = formatAgentRole(member.role) ?? formatAgentRole(member.agentType);
- return roleLabel ? (
-
- {roleLabel}
-
- ) : null;
- })()}
{showStartingBadge ? (
({
React.createElement(React.Fragment, null, children),
}));
vi.mock('@renderer/components/ui/tooltip', () => ({
+ TooltipProvider: ({ children }: { children: React.ReactNode }) =>
+ React.createElement(React.Fragment, null, children),
Tooltip: ({ children }: { children: React.ReactNode }) => React.createElement(React.Fragment, null, children),
TooltipTrigger: ({ children }: { children: React.ReactNode }) =>
React.createElement(React.Fragment, null, children),
@@ -45,6 +47,186 @@ import {
} from '@renderer/components/team/activity/ActivityItem';
import type { InboxMessage } from '@shared/types';
+describe('ActivityItem compact header preview', () => {
+ afterEach(() => {
+ document.body.innerHTML = '';
+ vi.unstubAllGlobals();
+ });
+
+ it('uses a two-line clamped preview in compact mode', async () => {
+ vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
+ const host = document.createElement('div');
+ document.body.appendChild(host);
+ const root = createRoot(host);
+ const summary =
+ 'Делегировал alice длинную задачу с заметно более длинным описанием, чтобы превью занимало больше одной строки в компактном режиме.';
+
+ const message: InboxMessage = {
+ from: 'team-lead',
+ text: summary,
+ summary,
+ timestamp: new Date('2026-04-18T16:30:00.000Z').toISOString(),
+ read: true,
+ source: 'lead_process',
+ };
+
+ await act(async () => {
+ root.render(
+ React.createElement(ActivityItem, {
+ message,
+ teamName: 'my-team',
+ compactHeader: true,
+ collapseMode: 'managed',
+ isCollapsed: true,
+ canToggleCollapse: true,
+ collapseToggleKey: 'message-key',
+ })
+ );
+ await Promise.resolve();
+ });
+
+ const preview = host.querySelector('.line-clamp-2');
+ expect(preview).not.toBeNull();
+ expect(preview?.textContent).toBe(summary);
+ expect(preview?.getAttribute('title')).toBeNull();
+ expect(preview?.className).toContain('line-clamp-2');
+ expect(preview?.className).toContain('w-full');
+ expect(preview?.className).toContain('max-w-full');
+ expect(preview?.className).not.toContain('min-h-8');
+ expect(preview?.className).not.toContain('truncate');
+
+ await act(async () => {
+ root.unmount();
+ await Promise.resolve();
+ });
+ });
+
+ it('prefers full message text over a pre-truncated summary in compact mode', async () => {
+ vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
+ const host = document.createElement('div');
+ document.body.appendChild(host);
+ const root = createRoot(host);
+ const fullText =
+ 'Делегировал bob ещё один узкий шаг: собрать fix-batch с учётом landing P0 по render->generate и пройтись по оставшимся edge cases.';
+
+ const message: InboxMessage = {
+ from: 'team-lead',
+ text: fullText,
+ summary: 'Делегировал bob ещё один узкий шаг: собрать fix-batch с у...',
+ timestamp: new Date('2026-04-18T16:29:00.000Z').toISOString(),
+ read: true,
+ source: 'lead_process',
+ };
+
+ await act(async () => {
+ root.render(
+ React.createElement(ActivityItem, {
+ message,
+ teamName: 'my-team',
+ compactHeader: true,
+ collapseMode: 'managed',
+ isCollapsed: true,
+ canToggleCollapse: true,
+ collapseToggleKey: 'message-key-full-text',
+ })
+ );
+ await Promise.resolve();
+ });
+
+ const preview = host.querySelector('.line-clamp-2');
+ expect(preview).not.toBeNull();
+ expect(preview?.textContent).toBe(fullText);
+
+ await act(async () => {
+ root.unmount();
+ await Promise.resolve();
+ });
+ });
+
+ it('strips info_for_agent blocks from compact preview text', async () => {
+ vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
+ const host = document.createElement('div');
+ document.body.appendChild(host);
+ const root = createRoot(host);
+
+ const visibleText = 'New task assigned to you: #3fd70e2 Собрать fix-batch';
+ const message: InboxMessage = {
+ from: 'team-lead',
+ text: `${visibleText}\n\ninternal only\n`,
+ timestamp: new Date('2026-04-18T16:28:00.000Z').toISOString(),
+ read: true,
+ source: 'lead_process',
+ };
+
+ await act(async () => {
+ root.render(
+ React.createElement(ActivityItem, {
+ message,
+ teamName: 'my-team',
+ compactHeader: true,
+ collapseMode: 'managed',
+ isCollapsed: true,
+ canToggleCollapse: true,
+ collapseToggleKey: 'message-key-strip-agent-block',
+ })
+ );
+ await Promise.resolve();
+ });
+
+ const preview = host.querySelector('.line-clamp-2');
+ expect(preview).not.toBeNull();
+ expect(preview?.textContent).toBe(visibleText);
+ expect(preview?.textContent).not.toContain('info_for_agent');
+ expect(preview?.textContent).not.toContain('internal only');
+
+ await act(async () => {
+ root.unmount();
+ await Promise.resolve();
+ });
+ });
+
+ it('uses a two-line preview in collapsed wide mode, not inline one-line summary', async () => {
+ vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
+ const host = document.createElement('div');
+ document.body.appendChild(host);
+ const root = createRoot(host);
+ const fullText =
+ 'Делегировал alice финальную общую сводку и remediation plan по всем findings команды.';
+
+ const message: InboxMessage = {
+ from: 'team-lead',
+ text: fullText,
+ timestamp: new Date('2026-04-18T16:30:00.000Z').toISOString(),
+ read: true,
+ source: 'lead_process',
+ };
+
+ await act(async () => {
+ root.render(
+ React.createElement(ActivityItem, {
+ message,
+ teamName: 'my-team',
+ compactHeader: false,
+ collapseMode: 'managed',
+ isCollapsed: true,
+ canToggleCollapse: true,
+ collapseToggleKey: 'message-key-wide-collapsed',
+ })
+ );
+ await Promise.resolve();
+ });
+
+ const preview = host.querySelector('.line-clamp-2');
+ expect(preview).not.toBeNull();
+ expect(preview?.textContent).toBe(fullText);
+
+ await act(async () => {
+ root.unmount();
+ await Promise.resolve();
+ });
+ });
+});
+
describe('ActivityItem slash command rendering', () => {
afterEach(() => {
document.body.innerHTML = '';
diff --git a/test/renderer/components/team/activity/LeadThoughtsGroup.test.ts b/test/renderer/components/team/activity/LeadThoughtsGroup.test.ts
index cdb504de..1dd46a44 100644
--- a/test/renderer/components/team/activity/LeadThoughtsGroup.test.ts
+++ b/test/renderer/components/team/activity/LeadThoughtsGroup.test.ts
@@ -1,8 +1,36 @@
+import React, { act } from 'react';
+import { createRoot } from 'react-dom/client';
import { describe, expect, it } from 'vitest';
+import { afterEach, beforeEach, vi } from 'vitest';
+
+vi.mock('@renderer/components/team/MemberBadge', () => ({
+ MemberBadge: ({ name }: { name: string }) => React.createElement('span', null, name),
+}));
+vi.mock('@renderer/components/ui/tooltip', () => ({
+ TooltipProvider: ({ children }: { children: React.ReactNode }) => React.createElement(React.Fragment, null, children),
+ Tooltip: ({ children }: { children: React.ReactNode }) => React.createElement(React.Fragment, null, children),
+ TooltipTrigger: ({ children }: { children: React.ReactNode }) =>
+ React.createElement(React.Fragment, null, children),
+ TooltipContent: ({ children }: { children: React.ReactNode }) => React.createElement('div', null, children),
+}));
+vi.mock('../../../../../src/renderer/components/team/activity/AnimatedHeightReveal', () => ({
+ ENTRY_REVEAL_ANIMATION_MS: 220,
+ ENTRY_REVEAL_EASING: 'ease',
+ AnimatedHeightReveal: ({ children }: { children: React.ReactNode }) =>
+ React.createElement(React.Fragment, null, children),
+}));
+vi.mock('../../../../../src/renderer/components/team/activity/ThoughtBodyContent', () => ({
+ ThoughtBodyContent: ({ thought }: { thought: { text: string } }) =>
+ React.createElement('div', null, thought.text),
+}));
+vi.mock('@renderer/utils/memberHelpers', () => ({
+ agentAvatarUrl: () => '/avatar.png',
+}));
import {
groupTimelineItems,
isLeadThought,
+ LeadThoughtsGroupRow,
} from '../../../../../src/renderer/components/team/activity/LeadThoughtsGroup';
import type { InboxMessage } from '../../../../../src/shared/types';
@@ -19,6 +47,29 @@ function makeLeadSessionMsg(text: string, overrides?: Partial): In
}
describe('LeadThoughtsGroup', () => {
+ beforeEach(() => {
+ vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
+ vi.stubGlobal(
+ 'IntersectionObserver',
+ class {
+ observe() {}
+ disconnect() {}
+ }
+ );
+ vi.stubGlobal(
+ 'ResizeObserver',
+ class {
+ observe() {}
+ disconnect() {}
+ }
+ );
+ });
+
+ afterEach(() => {
+ document.body.innerHTML = '';
+ vi.unstubAllGlobals();
+ });
+
it('does not classify slash command results as lead thoughts', () => {
const resultMessage: InboxMessage = {
from: 'team-lead',
@@ -118,4 +169,155 @@ describe('LeadThoughtsGroup', () => {
}
});
});
+
+ it('uses a two-line clamped preview in compact header mode', async () => {
+ const host = document.createElement('div');
+ document.body.appendChild(host);
+ const root = createRoot(host);
+ const preview =
+ 'Это длинный preview текста для lead thoughts, который должен занимать до двух строк в compact header, а не одну.';
+
+ const thought = makeLeadSessionMsg(preview, {
+ messageId: 'thought-1',
+ leadSessionId: 'lead-session-1',
+ });
+
+ await act(async () => {
+ root.render(
+ React.createElement(LeadThoughtsGroupRow, {
+ group: { type: 'lead-thoughts', thoughts: [thought] },
+ collapseMode: 'managed',
+ isCollapsed: true,
+ canToggleCollapse: true,
+ compactHeader: true,
+ })
+ );
+ await Promise.resolve();
+ });
+
+ const previewNode = host.querySelector('.line-clamp-2');
+ expect(previewNode).not.toBeNull();
+ expect(previewNode?.textContent).toBe(preview);
+ expect(previewNode?.getAttribute('title')).toBeNull();
+ expect(previewNode?.className).toContain('line-clamp-2');
+ expect(previewNode?.className).toContain('w-full');
+ expect(previewNode?.className).toContain('max-w-full');
+ expect(previewNode?.className).not.toContain('min-h-8');
+ expect(previewNode?.className).not.toContain('truncate');
+
+ await act(async () => {
+ root.unmount();
+ await Promise.resolve();
+ });
+ });
+
+ it('uses the normalized full thought text instead of only the first line in compact header mode', async () => {
+ const host = document.createElement('div');
+ document.body.appendChild(host);
+ const root = createRoot(host);
+ const firstLine = 'Собрать единый remediation plan.';
+ const secondLine = 'Проверить remaining edge cases по graph и messages.';
+ const preview = `${firstLine} ${secondLine}`;
+
+ const thought = makeLeadSessionMsg(`${firstLine}\n${secondLine}`, {
+ messageId: 'thought-2',
+ leadSessionId: 'lead-session-2',
+ });
+
+ await act(async () => {
+ root.render(
+ React.createElement(LeadThoughtsGroupRow, {
+ group: { type: 'lead-thoughts', thoughts: [thought] },
+ collapseMode: 'managed',
+ isCollapsed: true,
+ canToggleCollapse: true,
+ compactHeader: true,
+ })
+ );
+ await Promise.resolve();
+ });
+
+ const previewNode = host.querySelector('.line-clamp-2');
+ expect(previewNode).not.toBeNull();
+ expect(previewNode?.textContent).toBe(preview);
+
+ await act(async () => {
+ root.unmount();
+ await Promise.resolve();
+ });
+ });
+
+ it('strips info_for_agent blocks from compact thoughts preview', async () => {
+ const host = document.createElement('div');
+ document.body.appendChild(host);
+ const root = createRoot(host);
+ const visibleText = 'Собрать единый remediation plan.';
+
+ const thought = makeLeadSessionMsg(
+ `${visibleText}\n\ninternal note\n`,
+ {
+ messageId: 'thought-3',
+ leadSessionId: 'lead-session-3',
+ }
+ );
+
+ await act(async () => {
+ root.render(
+ React.createElement(LeadThoughtsGroupRow, {
+ group: { type: 'lead-thoughts', thoughts: [thought] },
+ collapseMode: 'managed',
+ isCollapsed: true,
+ canToggleCollapse: true,
+ compactHeader: true,
+ })
+ );
+ await Promise.resolve();
+ });
+
+ const previewNode = host.querySelector('.line-clamp-2');
+ expect(previewNode).not.toBeNull();
+ expect(previewNode?.textContent).toBe(visibleText);
+ expect(previewNode?.textContent).not.toContain('info_for_agent');
+ expect(previewNode?.textContent).not.toContain('internal note');
+
+ await act(async () => {
+ root.unmount();
+ await Promise.resolve();
+ });
+ });
+
+ it('uses a two-line preview in collapsed wide mode for thought groups', async () => {
+ const host = document.createElement('div');
+ document.body.appendChild(host);
+ const root = createRoot(host);
+ const preview =
+ 'Делегировал alice финальную общую сводку и remediation plan по всем findings команды.';
+
+ const thought = makeLeadSessionMsg(preview, {
+ messageId: 'thought-4',
+ leadSessionId: 'lead-session-4',
+ });
+
+ await act(async () => {
+ root.render(
+ React.createElement(LeadThoughtsGroupRow, {
+ group: { type: 'lead-thoughts', thoughts: [thought] },
+ collapseMode: 'managed',
+ isCollapsed: true,
+ canToggleCollapse: true,
+ compactHeader: false,
+ })
+ );
+ await Promise.resolve();
+ });
+
+ const previewNode = host.querySelector('.line-clamp-2');
+ expect(previewNode).not.toBeNull();
+ expect(previewNode?.textContent).toBe(preview);
+
+ await act(async () => {
+ root.unmount();
+ await Promise.resolve();
+ });
+ });
});
diff --git a/test/renderer/components/team/members/CurrentTaskIndicator.test.ts b/test/renderer/components/team/members/CurrentTaskIndicator.test.ts
new file mode 100644
index 00000000..c538792f
--- /dev/null
+++ b/test/renderer/components/team/members/CurrentTaskIndicator.test.ts
@@ -0,0 +1,77 @@
+import React, { act } from 'react';
+import { createRoot } from 'react-dom/client';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+
+import { CurrentTaskIndicator } from '@renderer/components/team/members/CurrentTaskIndicator';
+
+import type { TeamTaskWithKanban } from '@shared/types';
+
+const task: TeamTaskWithKanban = {
+ id: 'task-1',
+ displayId: '9d1915a7',
+ subject: 'Полный аудит актуальности документации и связанных onboarding заметок',
+ status: 'in_progress',
+} as unknown as TeamTaskWithKanban;
+
+describe('CurrentTaskIndicator', () => {
+ afterEach(() => {
+ document.body.innerHTML = '';
+ });
+
+ it('uses all available width for the task pill without early subject truncation', async () => {
+ vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
+ const host = document.createElement('div');
+ document.body.appendChild(host);
+ const root = createRoot(host);
+
+ await act(async () => {
+ root.render(
+ React.createElement(CurrentTaskIndicator, {
+ task,
+ borderColor: '#3b82f6',
+ })
+ );
+ await Promise.resolve();
+ });
+
+ const wrapper = host.firstElementChild as HTMLElement | null;
+ const button = host.querySelector('button');
+
+ expect(wrapper?.className).toContain('flex-1');
+ expect(button?.className).toContain('flex-1');
+ expect(button?.className).toContain('text-left');
+ expect(button?.textContent).toContain(task.subject);
+ expect(button?.style.border).toBe('');
+
+ await act(async () => {
+ root.unmount();
+ await Promise.resolve();
+ });
+ });
+
+ it('still supports an explicit subject ceiling when a compact caller requests it', async () => {
+ vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
+ const host = document.createElement('div');
+ document.body.appendChild(host);
+ const root = createRoot(host);
+
+ await act(async () => {
+ root.render(
+ React.createElement(CurrentTaskIndicator, {
+ task,
+ borderColor: '#3b82f6',
+ maxSubjectLength: 12,
+ })
+ );
+ await Promise.resolve();
+ });
+
+ const button = host.querySelector('button');
+ expect(button?.textContent).toContain('Полный аудит…');
+
+ await act(async () => {
+ root.unmount();
+ await Promise.resolve();
+ });
+ });
+});
diff --git a/test/renderer/components/team/members/MemberCard.test.ts b/test/renderer/components/team/members/MemberCard.test.ts
index 6fa3cd14..e0058c00 100644
--- a/test/renderer/components/team/members/MemberCard.test.ts
+++ b/test/renderer/components/team/members/MemberCard.test.ts
@@ -240,4 +240,38 @@ describe('MemberCard starting-state visuals', () => {
await Promise.resolve();
});
});
+
+ it('shows member color on the avatar ring instead of a colored card rail', async () => {
+ vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
+ const host = document.createElement('div');
+ document.body.appendChild(host);
+ const root = createRoot(host);
+
+ await act(async () => {
+ root.render(
+ React.createElement(MemberCard, {
+ member,
+ memberColor: 'blue',
+ isTeamAlive: true,
+ isTeamProvisioning: false,
+ })
+ );
+ await Promise.resolve();
+ });
+
+ const img = host.querySelector('img');
+ const avatarRing = img?.parentElement;
+ const clickableCard = host.querySelector('[role="button"]') as HTMLElement | null;
+
+ expect(avatarRing).not.toBeNull();
+ expect(avatarRing?.style.borderColor).toBe('#3b82f6');
+ expect(clickableCard?.style.borderLeft).toBe('');
+ expect(clickableCard?.style.background).toBe('');
+ expect(clickableCard?.className).not.toContain('px-');
+
+ await act(async () => {
+ root.unmount();
+ await Promise.resolve();
+ });
+ });
});