diff --git a/src/renderer/components/team/activity/ActiveTasksBlock.tsx b/src/renderer/components/team/activity/ActiveTasksBlock.tsx
index b9c722cc..a90f12ca 100644
--- a/src/renderer/components/team/activity/ActiveTasksBlock.tsx
+++ b/src/renderer/components/team/activity/ActiveTasksBlock.tsx
@@ -1,4 +1,4 @@
-import { useState } from 'react';
+import { useState, type ReactNode } from 'react';
import { CARD_BG, CARD_BORDER_STYLE, CARD_ICON_MUTED } from '@renderer/constants/cssVariables';
import { getTeamColorSet, getThemedBadge } from '@renderer/constants/teamColors';
@@ -19,6 +19,7 @@ interface ActiveTasksBlockProps {
tasks: TeamTaskWithKanban[];
/** Start collapsed (e.g. when rendered inside the sidebar where MemberList already shows status). */
defaultCollapsed?: boolean;
+ headerRight?: ReactNode;
onMemberClick?: (member: ResolvedTeamMember) => void;
onTaskClick?: (task: TeamTaskWithKanban) => void;
}
@@ -34,6 +35,7 @@ export const ActiveTasksBlock = ({
members,
tasks,
defaultCollapsed = false,
+ headerRight,
onMemberClick,
onTaskClick,
}: ActiveTasksBlockProps): React.JSX.Element | null => {
@@ -70,23 +72,26 @@ export const ActiveTasksBlock = ({
return (
-
+
+
+ {headerRight ?
{headerRight}
: null}
+
{!collapsed &&
entries.map(({ member, task, taskId, kind }) => {
const colors = getTeamColorSet(colorMap.get(member.name) ?? '');
diff --git a/src/renderer/components/team/activity/ActivityItem.tsx b/src/renderer/components/team/activity/ActivityItem.tsx
index 2b80003f..23d2e014 100644
--- a/src/renderer/components/team/activity/ActivityItem.tsx
+++ b/src/renderer/components/team/activity/ActivityItem.tsx
@@ -881,6 +881,132 @@ export const ActivityItem = memo(
onToggleCollapse?.(collapseToggleKey);
}
}, [collapseToggleKey, isHeaderClickable, onToggleCollapse]);
+ const useCompactCollapsedHeader = compactHeader && !isExpanded;
+
+ const senderBadge = isSlashCommandResult ? (
+
+ result
+
+ ) : (
+
+ );
+
+ const messageTypeBadge = systemLabel ? (
+
+ {systemLabel}
+
+ ) : isSlashCommandResult && message.commandOutput ? (
+
+ {message.commandOutput.stream}
+
+ ) : isSlashCommandMessage ? (
+
command
+ ) : messageType ? (
+
+ {messageType}
+
+ ) : null;
+
+ const leadSourceBadge =
+ message.source === 'lead_session' && !isSlashCommandResult ? (
+
+ session
+
+ ) : message.source === 'lead_process' && !isSlashCommandResult ? (
+
+ live
+
+ ) : null;
+
+ const statusBadge = rateLimited ? (
+
+
+ Rate Limited
+
+ ) : isApiError ? (
+
+
+ API Error
+
+ ) : null;
+
+ const recipientBadge =
+ message.to && message.to !== message.from ? (
+ <>
+
+ {crossTeamTarget ? (
+
+ ) : null}
+ {crossTeamSentMemberName || !crossTeamTarget ? (
+
+ ) : null}
+ >
+ ) : null;
+
+ const summaryContent =
+ isSlashCommandResult && message.commandOutput ? (
+
+
+
+ {message.commandOutput.commandLabel}
+
+
+ {message.summary || getCommandOutputSummary(message.text) || rawSummary}
+
+
+ ) : isSlashCommandMessage && slashCommandMeta ? (
+
+
+
+ {slashCommandMeta.command}
+
+ {slashCommandMeta.args ? (
+
+ {slashCommandMeta.args.replace(/\n+/g, ' ')}
+
+ ) : (slashCommandMeta.knownDescription ?? knownSlashCommand?.description) ? (
+
+ {slashCommandMeta.knownDescription ?? knownSlashCommand?.description}
+
+ ) : null}
+
+ ) : onTaskIdClick ? (
+ renderInlineBoldSummary(rawSummary, onTaskIdClick)
+ ) : (
+ renderInlineBoldSummary(rawSummary)
+ );
return (
- {isUnread ? (
-
- ) : null}
- {/* Chevron for collapsible messages */}
- {showChevron ? (
-
- ) : null}
-
- {/* Sender avatar + name badge */}
- {crossTeamOrigin ? (
-
- ) : null}
- {isSlashCommandResult ? (
-
- result
-
- ) : (
-
- )}
-
- {/* Role */}
- {!compactHeader && formattedRole && !isSlashCommandResult ? (
-
- {formattedRole}
-
- ) : null}
-
- {/* Message type label or system label */}
- {systemLabel ? (
-
- {systemLabel}
-
- ) : isSlashCommandResult && message.commandOutput ? (
-
- {message.commandOutput.stream}
-
- ) : isSlashCommandMessage ? (
- command
- ) : messageType ? (
-
- {messageType}
-
- ) : null}
-
- {/* Lead session marker */}
- {message.source === 'lead_session' && !isSlashCommandResult ? (
-
- session
-
- ) : message.source === 'lead_process' && !isSlashCommandResult ? (
-
- live
-
- ) : null}
-
- {/* Rate limit warning badge */}
- {rateLimited ? (
-
-
- Rate Limited
-
- ) : null}
-
- {/* API Error warning badge */}
- {isApiError && !rateLimited ? (
-
-
- API Error
-
- ) : null}
-
- {/* Recipient — arrow + avatar + badge */}
- {message.to && message.to !== message.from ? (
- <>
-
- {crossTeamTarget ? (
-
- ) : null}
- {crossTeamSentMemberName || !crossTeamTarget ? (
-
- ) : null}
- >
- ) : null}
-
- {/* Summary */}
-
- {isSlashCommandResult && message.commandOutput ? (
-
-
-
- {message.commandOutput.commandLabel}
-
-
- {message.summary || getCommandOutputSummary(message.text) || rawSummary}
-
-
- ) : isSlashCommandMessage && slashCommandMeta ? (
-
-
-
- {slashCommandMeta.command}
-
- {slashCommandMeta.args ? (
-
- {slashCommandMeta.args.replace(/\n+/g, ' ')}
+ {useCompactCollapsedHeader ? (
+
+
+
+ {isUnread ? (
+
+ ) : null}
+ {crossTeamOrigin ? (
+
+ ) : null}
+ {senderBadge}
+ {messageTypeBadge}
+ {leadSourceBadge}
+ {statusBadge}
+ {recipientBadge}
+
+
+
+ {timestamp}
- ) : (slashCommandMeta.knownDescription ?? knownSlashCommand?.description) ? (
-
- {slashCommandMeta.knownDescription ?? knownSlashCommand?.description}
-
- ) : null}
-
- ) : onTaskIdClick ? (
- renderInlineBoldSummary(rawSummary, onTaskIdClick)
- ) : (
- renderInlineBoldSummary(rawSummary)
- )}
-
-
- {/* Timestamp / expand */}
-
-
- {timestamp}
-
- {onExpand && expandItemKey && (
-
+
+
-
-
- )}
-
+ {summaryContent}
+
+
+ ) : (
+ <>
+ {isUnread ? (
+
+ ) : null}
+ {showChevron ? (
+
+ ) : null}
+ {crossTeamOrigin ? (
+
+ ) : null}
+ {senderBadge}
+ {!compactHeader && formattedRole && !isSlashCommandResult ? (
+
+ {formattedRole}
+
+ ) : null}
+ {messageTypeBadge}
+ {leadSourceBadge}
+ {statusBadge}
+ {recipientBadge}
+
+ {summaryContent}
+
+
+
+ {timestamp}
+
+ {onExpand && expandItemKey && (
+ {
+ e.stopPropagation();
+ onExpand(expandItemKey);
+ }}
+ onKeyDown={(e) => e.stopPropagation()}
+ >
+
+
+ )}
+
+ >
+ )}
{/* Content — collapsed for system messages, expanded for others */}
diff --git a/src/renderer/components/team/activity/LeadThoughtsGroup.tsx b/src/renderer/components/team/activity/LeadThoughtsGroup.tsx
index 39c0508f..6943a68b 100644
--- a/src/renderer/components/team/activity/LeadThoughtsGroup.tsx
+++ b/src/renderer/components/team/activity/LeadThoughtsGroup.tsx
@@ -751,6 +751,13 @@ const LeadThoughtsGroupRowComponent = ({
});
}, []);
+ const timestampLabel =
+ formatTime(oldest.timestamp) === formatTime(newest.timestamp)
+ ? formatTime(oldest.timestamp)
+ : `${formatTime(oldest.timestamp)}–${formatTime(newest.timestamp)}`;
+ const useCompactCollapsedHeader = compactHeader && !isBodyVisible;
+ const compactPreviewText = headerTextPreview ?? totalToolSummary;
+
return (
- {/* Chevron for collapse mode */}
- {canToggleBodyVisibility && !compactHeader ? (
-
- ) : null}
- {/* Lead avatar with optional live indicator */}
- {!compactHeader ? (
-
-

-
-
- ) : null}
-
-
- {thoughts.length} thoughts
-
- {!isBodyVisible && headerTextPreview ? (
-
-
-
+
+
+
+
+ {thoughts.length} thoughts
+
+
+
+
+ {timestampLabel}
+
+ {onExpand && expandItemKey && (
+ {
+ e.stopPropagation();
+ onExpand(expandItemKey);
+ }}
+ onKeyDown={(e) => e.stopPropagation()}
+ >
+
+
+ )}
+
+
+ {compactPreviewText ? (
+
- {headerTextPreview}
-
-
- {totalToolSummary ? (
-
-
-
+ {compactPreviewText}
+
) : null}
-
- ) : totalToolSummary ? (
-
-
-
- {totalToolSummary}
-
-
-
-
+ ) : (
+ <>
+ {canToggleBodyVisibility && !compactHeader ? (
+
-
-
- ) : null}
-
-
- {formatTime(oldest.timestamp) === formatTime(newest.timestamp)
- ? formatTime(oldest.timestamp)
- : `${formatTime(oldest.timestamp)}–${formatTime(newest.timestamp)}`}
-
- {onExpand && expandItemKey && (
- {
- e.stopPropagation();
- onExpand(expandItemKey);
- }}
- onKeyDown={(e) => e.stopPropagation()}
- >
-
-
- )}
-
+ ) : null}
+ {!compactHeader ? (
+
+

+
+
+ ) : null}
+
+
+ {thoughts.length} thoughts
+
+ {!isBodyVisible && headerTextPreview ? (
+
+
+
+ {headerTextPreview}
+
+
+ {totalToolSummary ? (
+
+
+
+ ) : null}
+
+ ) : totalToolSummary ? (
+
+
+
+ {totalToolSummary}
+
+
+
+
+
+
+ ) : null}
+
+
+ {timestampLabel}
+
+ {onExpand && expandItemKey && (
+ {
+ e.stopPropagation();
+ onExpand(expandItemKey);
+ }}
+ onKeyDown={(e) => e.stopPropagation()}
+ >
+
+
+ )}
+
+ >
+ )}
{/* Scrollable body — live thoughts follow bottom unless user scrolls up */}
diff --git a/src/renderer/components/team/activity/PendingRepliesBlock.tsx b/src/renderer/components/team/activity/PendingRepliesBlock.tsx
index 28a9ea58..62d5496c 100644
--- a/src/renderer/components/team/activity/PendingRepliesBlock.tsx
+++ b/src/renderer/components/team/activity/PendingRepliesBlock.tsx
@@ -16,6 +16,7 @@ import { Loader2, ShieldQuestion, Users } from 'lucide-react';
import { useShallow } from 'zustand/react/shallow';
import type { ResolvedTeamMember } from '@shared/types';
+import type { ReactNode } from 'react';
export interface PendingCrossTeamReply {
teamName: string;
@@ -26,6 +27,7 @@ interface PendingRepliesBlockProps {
members: ResolvedTeamMember[];
pendingRepliesByMember: Record;
pendingCrossTeamReplies?: PendingCrossTeamReply[];
+ headerRight?: ReactNode;
onMemberClick?: (member: ResolvedTeamMember) => void;
}
@@ -33,6 +35,7 @@ export const PendingRepliesBlock = ({
members,
pendingRepliesByMember,
pendingCrossTeamReplies = [],
+ headerRight,
onMemberClick,
}: PendingRepliesBlockProps): React.JSX.Element | null => {
const { isLight } = useTheme();
@@ -70,9 +73,12 @@ export const PendingRepliesBlock = ({
return (
-
- Awaiting replies
-
+
+
+ Awaiting replies
+
+ {headerRight ?
{headerRight}
: null}
+
{pending.map((entry) => {
const since = formatDistanceToNowStrict(entry.sentAtMs, { addSuffix: true });
diff --git a/src/renderer/components/team/messages/MessageComposer.tsx b/src/renderer/components/team/messages/MessageComposer.tsx
index 7b0957a1..b951cc04 100644
--- a/src/renderer/components/team/messages/MessageComposer.tsx
+++ b/src/renderer/components/team/messages/MessageComposer.tsx
@@ -465,7 +465,7 @@ export const MessageComposer = ({
return (
computePendingCrossTeamReplies(messages, nowMs),
[messages, nowMs]
);
-
- /** Whether the Status block has any visible items. */
- const hasItems = useMemo(() => {
- const hasPendingReplies = Object.keys(pendingRepliesByMember).some((name) =>
+ const hasPendingReplies = useMemo(() => {
+ const hasMemberPendingReplies = Object.keys(pendingRepliesByMember).some((name) =>
members.some((m) => m.name === name)
);
- if (hasPendingReplies) return true;
- if (pendingCrossTeamReplies.length > 0) return true;
-
+ return hasMemberPendingReplies || pendingCrossTeamReplies.length > 0;
+ }, [members, pendingRepliesByMember, pendingCrossTeamReplies.length]);
+ const hasActiveTasks = useMemo(() => {
const tMap = new Map(tasks.map((t) => [t.id, t]));
return members.some((m) => {
if (!m.currentTaskId) return false;
@@ -60,7 +58,13 @@ export const StatusBlock = ({
if (task && (task.reviewState === 'approved' || task.status === 'completed')) return false;
return true;
});
- }, [members, tasks, pendingRepliesByMember, pendingCrossTeamReplies.length]);
+ }, [members, tasks]);
+
+ /** Whether the Status block has any visible items. */
+ const hasItems = useMemo(() => {
+ if (hasPendingReplies) return true;
+ return hasActiveTasks;
+ }, [hasActiveTasks, hasPendingReplies]);
// Only run the 1-second timer when the block actually has content to show.
useEffect(() => {
@@ -85,6 +89,7 @@ export const StatusBlock = ({
Status
);
+ const flowInlineToggle = layout === 'flow' && !collapsed ? toggleButton : null;
return (
<>
@@ -92,21 +97,25 @@ export const StatusBlock = ({
- ) : (
+ ) : collapsed ? (
{toggleButton}
- )}
+ ) : null}
{!collapsed && (
-
+ {hasPendingReplies ? (
+
+ ) : null}
diff --git a/src/renderer/components/ui/MentionableTextarea.tsx b/src/renderer/components/ui/MentionableTextarea.tsx
index a89b84e4..41861772 100644
--- a/src/renderer/components/ui/MentionableTextarea.tsx
+++ b/src/renderer/components/ui/MentionableTextarea.tsx
@@ -1102,10 +1102,10 @@ export const MentionableTextarea = React.forwardRef
0 &&
surfaceShellMetrics.height > 0 ? (