feat(ui): polish messages timeline and graph toolbar

This commit is contained in:
777genius 2026-04-13 10:41:23 +03:00
parent e5d6911025
commit 674c1d8b13
11 changed files with 535 additions and 339 deletions

View file

@ -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 (
<div className="mb-3 space-y-1.5">
<button
type="button"
className="flex w-full items-center gap-1.5 text-[10px] font-medium uppercase tracking-wide text-[var(--color-text-muted)] transition-colors hover:text-[var(--color-text-secondary)]"
onClick={() => setCollapsed((v) => !v)}
aria-label={collapsed ? 'Expand in progress' : 'Collapse in progress'}
>
<ChevronRight
size={10}
className={`shrink-0 transition-transform duration-150 ${collapsed ? '' : 'rotate-90'}`}
/>
<span>In progress</span>
{collapsed && (
<span className="rounded-full bg-[var(--color-surface-raised)] px-1.5 py-0.5 text-[10px] font-medium tabular-nums leading-none text-[var(--color-text-muted)]">
{entries.length}
</span>
)}
</button>
<div className="flex items-center justify-between gap-3">
<button
type="button"
className="flex min-w-0 items-center gap-1.5 text-[10px] font-medium uppercase tracking-wide text-[var(--color-text-muted)] transition-colors hover:text-[var(--color-text-secondary)]"
onClick={() => setCollapsed((v) => !v)}
aria-label={collapsed ? 'Expand in progress' : 'Collapse in progress'}
>
<ChevronRight
size={10}
className={`shrink-0 transition-transform duration-150 ${collapsed ? '' : 'rotate-90'}`}
/>
<span>In progress</span>
{collapsed && (
<span className="rounded-full bg-[var(--color-surface-raised)] px-1.5 py-0.5 text-[10px] font-medium tabular-nums leading-none text-[var(--color-text-muted)]">
{entries.length}
</span>
)}
</button>
{headerRight ? <div className="shrink-0">{headerRight}</div> : null}
</div>
{!collapsed &&
entries.map(({ member, task, taskId, kind }) => {
const colors = getTeamColorSet(colorMap.get(member.name) ?? '');

View file

@ -881,6 +881,132 @@ export const ActivityItem = memo(
onToggleCollapse?.(collapseToggleKey);
}
}, [collapseToggleKey, isHeaderClickable, onToggleCollapse]);
const useCompactCollapsedHeader = compactHeader && !isExpanded;
const senderBadge = isSlashCommandResult ? (
<span className="inline-flex items-center rounded-full bg-amber-500/15 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-amber-300">
result
</span>
) : (
<MemberBadge
name={senderName}
color={senderColor}
teamName={teamName}
hideAvatar={senderHideAvatar || compactHeader}
onClick={onMemberNameClick}
disableHoverCard={crossTeamOrigin != null}
/>
);
const messageTypeBadge = systemLabel ? (
<span className="text-[10px] uppercase tracking-wide" style={{ color: CARD_ICON_MUTED }}>
{systemLabel}
</span>
) : isSlashCommandResult && message.commandOutput ? (
<span
className={[
'inline-flex items-center rounded-full px-1.5 py-0.5 text-[10px] uppercase tracking-wide',
isCommandOutputError ? 'bg-rose-500/15 text-rose-300' : 'bg-amber-500/15 text-amber-300',
].join(' ')}
>
{message.commandOutput.stream}
</span>
) : isSlashCommandMessage ? (
<span className="text-[10px] uppercase tracking-wide text-amber-400">command</span>
) : messageType ? (
<span className="text-[10px] uppercase tracking-wide" style={{ color: CARD_ICON_MUTED }}>
{messageType}
</span>
) : null;
const leadSourceBadge =
message.source === 'lead_session' && !isSlashCommandResult ? (
<span className="text-[10px] uppercase tracking-wide" style={{ color: CARD_ICON_MUTED }}>
session
</span>
) : message.source === 'lead_process' && !isSlashCommandResult ? (
<span className="text-[10px] uppercase tracking-wide" style={{ color: CARD_ICON_MUTED }}>
live
</span>
) : null;
const statusBadge = rateLimited ? (
<span className="inline-flex items-center gap-1 rounded-full bg-red-500/20 px-1.5 py-0.5 text-[10px] font-medium text-red-400">
<AlertTriangle size={10} />
Rate Limited
</span>
) : isApiError ? (
<span className="inline-flex items-center gap-1 rounded-full bg-red-500/20 px-1.5 py-0.5 text-[10px] font-medium text-red-400">
<AlertTriangle size={10} />
API Error
</span>
) : null;
const recipientBadge =
message.to && message.to !== message.from ? (
<>
<MoveRight size={10} style={{ color: CARD_ICON_MUTED }} className="shrink-0" />
{crossTeamTarget ? (
<CrossTeamTeamBadge teamName={crossTeamTarget} onClick={onTeamClick} />
) : null}
{crossTeamSentMemberName || !crossTeamTarget ? (
<MemberBadge
name={crossTeamSentMemberName ?? qualifiedRecipient?.memberName ?? message.to}
color={crossTeamTarget ? undefined : recipientColor}
teamName={crossTeamTarget ? undefined : teamName}
hideAvatar={
compactHeader ||
(crossTeamSentMemberName ?? qualifiedRecipient?.memberName ?? message.to) === 'user'
}
onClick={onMemberNameClick}
disableHoverCard={crossTeamTarget != null}
/>
) : null}
</>
) : null;
const summaryContent =
isSlashCommandResult && message.commandOutput ? (
<span className="inline-flex min-w-0 items-center gap-1.5">
<Command
size={12}
className={['shrink-0', isCommandOutputError ? 'text-rose-400' : 'text-amber-400'].join(
' '
)}
/>
<span
className={[
'shrink-0 font-mono text-[11px]',
isCommandOutputError ? 'text-rose-300' : 'text-amber-300',
].join(' ')}
>
{message.commandOutput.commandLabel}
</span>
<span className="min-w-0 truncate text-[11px] text-[var(--color-text-secondary)]">
{message.summary || getCommandOutputSummary(message.text) || rawSummary}
</span>
</span>
) : isSlashCommandMessage && slashCommandMeta ? (
<span className="inline-flex min-w-0 items-center gap-1.5">
<Command size={12} className="shrink-0 text-amber-400" />
<span className="shrink-0 font-mono text-[11px] text-amber-300">
{slashCommandMeta.command}
</span>
{slashCommandMeta.args ? (
<span className="min-w-0 truncate text-[11px] text-[var(--color-text-secondary)]">
{slashCommandMeta.args.replace(/\n+/g, ' ')}
</span>
) : (slashCommandMeta.knownDescription ?? knownSlashCommand?.description) ? (
<span className="min-w-0 truncate text-[11px] text-[var(--color-text-secondary)]">
{slashCommandMeta.knownDescription ?? knownSlashCommand?.description}
</span>
) : null}
</span>
) : onTaskIdClick ? (
renderInlineBoldSummary(rawSummary, onTaskIdClick)
) : (
renderInlineBoldSummary(rawSummary)
);
return (
<article
@ -933,7 +1059,9 @@ export const ActivityItem = memo(
role={isHeaderClickable ? 'button' : undefined}
tabIndex={isHeaderClickable ? 0 : undefined}
className={[
'flex min-w-0 items-center gap-2 px-3 py-2',
useCompactCollapsedHeader
? 'min-w-0 px-3 py-2'
: 'flex min-w-0 items-center gap-2 px-3 py-2',
isHeaderClickable ? 'cursor-pointer select-none' : '',
].join(' ')}
onClick={handleHeaderToggle}
@ -948,207 +1076,125 @@ export const ActivityItem = memo(
: undefined
}
>
{isUnread ? (
<span className="size-2 shrink-0 rounded-full bg-blue-500" title="Unread" aria-hidden />
) : null}
{/* Chevron for collapsible messages */}
{showChevron ? (
<ChevronRight
className="size-3 shrink-0 transition-transform duration-150"
style={{
color: CARD_ICON_MUTED,
transform: isExpanded ? 'rotate(90deg)' : undefined,
}}
/>
) : null}
{/* Sender avatar + name badge */}
{crossTeamOrigin ? (
<CrossTeamTeamBadge teamName={crossTeamOrigin.teamName} onClick={onTeamClick} />
) : null}
{isSlashCommandResult ? (
<span className="inline-flex items-center rounded-full bg-amber-500/15 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-amber-300">
result
</span>
) : (
<MemberBadge
name={senderName}
color={senderColor}
teamName={teamName}
hideAvatar={senderHideAvatar || compactHeader}
onClick={onMemberNameClick}
disableHoverCard={crossTeamOrigin != null}
/>
)}
{/* Role */}
{!compactHeader && formattedRole && !isSlashCommandResult ? (
<span className="text-[10px]" style={{ color: CARD_ICON_MUTED }}>
{formattedRole}
</span>
) : null}
{/* Message type label or system label */}
{systemLabel ? (
<span
className="text-[10px] uppercase tracking-wide"
style={{ color: CARD_ICON_MUTED }}
>
{systemLabel}
</span>
) : isSlashCommandResult && message.commandOutput ? (
<span
className={[
'inline-flex items-center rounded-full px-1.5 py-0.5 text-[10px] uppercase tracking-wide',
isCommandOutputError
? 'bg-rose-500/15 text-rose-300'
: 'bg-amber-500/15 text-amber-300',
].join(' ')}
>
{message.commandOutput.stream}
</span>
) : isSlashCommandMessage ? (
<span className="text-[10px] uppercase tracking-wide text-amber-400">command</span>
) : messageType ? (
<span
className="text-[10px] uppercase tracking-wide"
style={{ color: CARD_ICON_MUTED }}
>
{messageType}
</span>
) : null}
{/* Lead session marker */}
{message.source === 'lead_session' && !isSlashCommandResult ? (
<span
className="text-[10px] uppercase tracking-wide"
style={{ color: CARD_ICON_MUTED }}
>
session
</span>
) : message.source === 'lead_process' && !isSlashCommandResult ? (
<span
className="text-[10px] uppercase tracking-wide"
style={{ color: CARD_ICON_MUTED }}
>
live
</span>
) : null}
{/* Rate limit warning badge */}
{rateLimited ? (
<span className="inline-flex items-center gap-1 rounded-full bg-red-500/20 px-1.5 py-0.5 text-[10px] font-medium text-red-400">
<AlertTriangle size={10} />
Rate Limited
</span>
) : null}
{/* API Error warning badge */}
{isApiError && !rateLimited ? (
<span className="inline-flex items-center gap-1 rounded-full bg-red-500/20 px-1.5 py-0.5 text-[10px] font-medium text-red-400">
<AlertTriangle size={10} />
API Error
</span>
) : null}
{/* Recipient — arrow + avatar + badge */}
{message.to && message.to !== message.from ? (
<>
<MoveRight size={10} style={{ color: CARD_ICON_MUTED }} className="shrink-0" />
{crossTeamTarget ? (
<CrossTeamTeamBadge teamName={crossTeamTarget} onClick={onTeamClick} />
) : null}
{crossTeamSentMemberName || !crossTeamTarget ? (
<MemberBadge
name={crossTeamSentMemberName ?? qualifiedRecipient?.memberName ?? message.to}
color={crossTeamTarget ? undefined : recipientColor}
teamName={crossTeamTarget ? undefined : teamName}
hideAvatar={
compactHeader ||
(crossTeamSentMemberName ?? qualifiedRecipient?.memberName ?? message.to) ===
'user'
}
onClick={onMemberNameClick}
disableHoverCard={crossTeamTarget != null}
/>
) : null}
</>
) : null}
{/* Summary */}
<span className="min-w-0 flex-1 truncate text-xs" style={{ color: CARD_TEXT_LIGHT }}>
{isSlashCommandResult && message.commandOutput ? (
<span className="inline-flex items-center gap-1.5">
<Command
size={12}
className={[
'shrink-0',
isCommandOutputError ? 'text-rose-400' : 'text-amber-400',
].join(' ')}
/>
<span
className={[
'font-mono text-[11px]',
isCommandOutputError ? 'text-rose-300' : 'text-amber-300',
].join(' ')}
>
{message.commandOutput.commandLabel}
</span>
<span className="truncate text-[11px] text-[var(--color-text-secondary)]">
{message.summary || getCommandOutputSummary(message.text) || rawSummary}
</span>
</span>
) : isSlashCommandMessage && slashCommandMeta ? (
<span className="inline-flex items-center gap-1.5">
<Command size={12} className="shrink-0 text-amber-400" />
<span className="font-mono text-[11px] text-amber-300">
{slashCommandMeta.command}
</span>
{slashCommandMeta.args ? (
<span className="truncate text-[11px] text-[var(--color-text-secondary)]">
{slashCommandMeta.args.replace(/\n+/g, ' ')}
{useCompactCollapsedHeader ? (
<div className="min-w-0">
<div className="flex min-w-0 items-start gap-3">
<div className="flex min-w-0 flex-1 items-center gap-2 overflow-hidden">
{isUnread ? (
<span
className="size-2 shrink-0 rounded-full bg-blue-500"
title="Unread"
aria-hidden
/>
) : null}
{crossTeamOrigin ? (
<CrossTeamTeamBadge teamName={crossTeamOrigin.teamName} onClick={onTeamClick} />
) : null}
{senderBadge}
{messageTypeBadge}
{leadSourceBadge}
{statusBadge}
{recipientBadge}
</div>
<div className="relative flex shrink-0 items-center">
<span
className={
onExpand && expandItemKey
? 'text-[10px] transition-opacity group-hover:opacity-0'
: 'text-[10px]'
}
style={{ color: CARD_ICON_MUTED }}
>
{timestamp}
</span>
) : (slashCommandMeta.knownDescription ?? knownSlashCommand?.description) ? (
<span className="truncate text-[11px] text-[var(--color-text-secondary)]">
{slashCommandMeta.knownDescription ?? knownSlashCommand?.description}
</span>
) : null}
</span>
) : onTaskIdClick ? (
renderInlineBoldSummary(rawSummary, onTaskIdClick)
) : (
renderInlineBoldSummary(rawSummary)
)}
</span>
{/* Timestamp / expand */}
<div className="relative flex shrink-0 items-center">
<span
className={
onExpand && expandItemKey
? 'text-[10px] transition-opacity group-hover:opacity-0'
: 'text-[10px]'
}
style={{ color: CARD_ICON_MUTED }}
>
{timestamp}
</span>
{onExpand && expandItemKey && (
<button
type="button"
aria-label="Expand message"
className="absolute right-0 top-1/2 -translate-y-1/2 rounded p-0.5 opacity-0 transition-opacity focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-blue-500/50 group-hover:opacity-100"
style={{ color: CARD_ICON_MUTED }}
onClick={(e) => {
e.stopPropagation();
onExpand(expandItemKey);
}}
onKeyDown={(e) => e.stopPropagation()}
{onExpand && expandItemKey && (
<button
type="button"
aria-label="Expand message"
className="absolute right-0 top-1/2 -translate-y-1/2 rounded p-0.5 opacity-0 transition-opacity focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-blue-500/50 group-hover:opacity-100"
style={{ color: CARD_ICON_MUTED }}
onClick={(e) => {
e.stopPropagation();
onExpand(expandItemKey);
}}
onKeyDown={(e) => e.stopPropagation()}
>
<Maximize2 size={12} />
</button>
)}
</div>
</div>
<div
className="mt-1 min-w-0 truncate text-[11px]"
style={{ color: CARD_TEXT_LIGHT }}
title={summaryText || rawSummary}
>
<Maximize2 size={12} />
</button>
)}
</div>
{summaryContent}
</div>
</div>
) : (
<>
{isUnread ? (
<span
className="size-2 shrink-0 rounded-full bg-blue-500"
title="Unread"
aria-hidden
/>
) : null}
{showChevron ? (
<ChevronRight
className="size-3 shrink-0 transition-transform duration-150"
style={{
color: CARD_ICON_MUTED,
transform: isExpanded ? 'rotate(90deg)' : undefined,
}}
/>
) : null}
{crossTeamOrigin ? (
<CrossTeamTeamBadge teamName={crossTeamOrigin.teamName} onClick={onTeamClick} />
) : null}
{senderBadge}
{!compactHeader && formattedRole && !isSlashCommandResult ? (
<span className="text-[10px]" style={{ color: CARD_ICON_MUTED }}>
{formattedRole}
</span>
) : null}
{messageTypeBadge}
{leadSourceBadge}
{statusBadge}
{recipientBadge}
<span className="min-w-0 flex-1 truncate text-xs" style={{ color: CARD_TEXT_LIGHT }}>
{summaryContent}
</span>
<div className="relative flex shrink-0 items-center">
<span
className={
onExpand && expandItemKey
? 'text-[10px] transition-opacity group-hover:opacity-0'
: 'text-[10px]'
}
style={{ color: CARD_ICON_MUTED }}
>
{timestamp}
</span>
{onExpand && expandItemKey && (
<button
type="button"
aria-label="Expand message"
className="absolute right-0 top-1/2 -translate-y-1/2 rounded p-0.5 opacity-0 transition-opacity focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-blue-500/50 group-hover:opacity-100"
style={{ color: CARD_ICON_MUTED }}
onClick={(e) => {
e.stopPropagation();
onExpand(expandItemKey);
}}
onKeyDown={(e) => e.stopPropagation()}
>
<Maximize2 size={12} />
</button>
)}
</div>
</>
)}
</div>
{/* Content — collapsed for system messages, expanded for others */}

View file

@ -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 (
<AnimatedHeightReveal animate={isNew} containerRef={ref} style={{ overflowAnchor: 'none' }}>
<article
@ -767,7 +774,9 @@ const LeadThoughtsGroupRowComponent = ({
role={canToggleBodyVisibility ? 'button' : undefined}
tabIndex={canToggleBodyVisibility ? 0 : undefined}
className={[
'flex select-none items-center gap-2 px-3 py-1.5',
useCompactCollapsedHeader
? 'select-none px-3 py-2'
: 'flex select-none items-center gap-2 px-3 py-1.5',
canToggleBodyVisibility ? 'cursor-pointer' : '',
].join(' ')}
style={hasApiError ? { backgroundColor: 'rgba(248, 113, 113, 0.08)' } : undefined}
@ -783,101 +792,148 @@ const LeadThoughtsGroupRowComponent = ({
: undefined
}
>
{/* Chevron for collapse mode */}
{canToggleBodyVisibility && !compactHeader ? (
<ChevronRight
className="size-3 shrink-0 transition-transform duration-150"
style={{
color: CARD_ICON_MUTED,
transform: isBodyVisible ? 'rotate(90deg)' : undefined,
}}
/>
) : null}
{/* Lead avatar with optional live indicator */}
{!compactHeader ? (
<div className="relative shrink-0">
<img
src={agentAvatarUrl(leadName, 24)}
alt=""
className="size-5 rounded-full bg-[var(--color-surface-raised)]"
loading="lazy"
/>
<LiveThoughtStatusBadge
canBeLive={canBeLive}
isTeamAlive={isTeamAlive}
leadActivity={leadActivity}
leadContextUpdatedAt={leadContextUpdatedAt}
newestTimestamp={newest.timestamp}
/>
</div>
) : null}
<MemberBadge name={leadName} color={memberColor} hideAvatar />
<span className="text-[10px]" style={{ color: CARD_ICON_MUTED }}>
{thoughts.length} thoughts
</span>
{!isBodyVisible && headerTextPreview ? (
<Tooltip>
<TooltipTrigger asChild>
<span
className="min-w-0 flex-1 cursor-default truncate text-[10px]"
style={{ color: CARD_TEXT_LIGHT }}
{useCompactCollapsedHeader ? (
<div className="min-w-0">
<div className="flex min-w-0 items-start gap-3">
<div className="flex min-w-0 flex-1 items-center gap-2 overflow-hidden">
<MemberBadge name={leadName} color={memberColor} hideAvatar />
<span className="shrink-0 text-[10px]" style={{ color: CARD_ICON_MUTED }}>
{thoughts.length} thoughts
</span>
</div>
<div className="relative flex shrink-0 items-center">
<span
className={
onExpand && expandItemKey
? 'text-[10px] transition-opacity group-hover:opacity-0'
: 'text-[10px]'
}
style={{ color: CARD_ICON_MUTED }}
>
{timestampLabel}
</span>
{onExpand && expandItemKey && (
<button
type="button"
aria-label="Expand thoughts"
className="absolute right-0 top-1/2 -translate-y-1/2 rounded p-0.5 opacity-0 transition-opacity focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-blue-500/50 group-hover:opacity-100"
style={{ color: CARD_ICON_MUTED }}
onClick={(e) => {
e.stopPropagation();
onExpand(expandItemKey);
}}
onKeyDown={(e) => e.stopPropagation()}
>
<Maximize2 size={12} />
</button>
)}
</div>
</div>
{compactPreviewText ? (
<div
className="mt-1 min-w-0 truncate text-[11px]"
style={{ color: headerTextPreview ? CARD_TEXT_LIGHT : CARD_ICON_MUTED }}
title={compactPreviewText}
>
{headerTextPreview}
</span>
</TooltipTrigger>
{totalToolSummary ? (
<TooltipContent side="bottom" className="max-w-[420px] font-mono text-[11px]">
<ToolSummaryTooltipContent
toolCalls={allToolCalls}
toolSummary={totalToolSummary}
/>
</TooltipContent>
{compactPreviewText}
</div>
) : null}
</Tooltip>
) : totalToolSummary ? (
<Tooltip>
<TooltipTrigger asChild>
<span className="cursor-default text-[10px]" style={{ color: CARD_ICON_MUTED }}>
{totalToolSummary}
</span>
</TooltipTrigger>
<TooltipContent side="bottom" className="max-w-[420px] font-mono text-[11px]">
<ToolSummaryTooltipContent
toolCalls={allToolCalls}
toolSummary={totalToolSummary}
</div>
) : (
<>
{canToggleBodyVisibility && !compactHeader ? (
<ChevronRight
className="size-3 shrink-0 transition-transform duration-150"
style={{
color: CARD_ICON_MUTED,
transform: isBodyVisible ? 'rotate(90deg)' : undefined,
}}
/>
</TooltipContent>
</Tooltip>
) : null}
<div className="relative ml-auto flex shrink-0 items-center">
<span
className={
onExpand && expandItemKey
? 'text-[10px] transition-opacity group-hover:opacity-0'
: 'text-[10px]'
}
style={{ color: CARD_ICON_MUTED }}
>
{formatTime(oldest.timestamp) === formatTime(newest.timestamp)
? formatTime(oldest.timestamp)
: `${formatTime(oldest.timestamp)}${formatTime(newest.timestamp)}`}
</span>
{onExpand && expandItemKey && (
<button
type="button"
aria-label="Expand thoughts"
className="absolute right-0 top-1/2 -translate-y-1/2 rounded p-0.5 opacity-0 transition-opacity focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-blue-500/50 group-hover:opacity-100"
style={{ color: CARD_ICON_MUTED }}
onClick={(e) => {
e.stopPropagation();
onExpand(expandItemKey);
}}
onKeyDown={(e) => e.stopPropagation()}
>
<Maximize2 size={12} />
</button>
)}
</div>
) : null}
{!compactHeader ? (
<div className="relative shrink-0">
<img
src={agentAvatarUrl(leadName, 24)}
alt=""
className="size-5 rounded-full bg-[var(--color-surface-raised)]"
loading="lazy"
/>
<LiveThoughtStatusBadge
canBeLive={canBeLive}
isTeamAlive={isTeamAlive}
leadActivity={leadActivity}
leadContextUpdatedAt={leadContextUpdatedAt}
newestTimestamp={newest.timestamp}
/>
</div>
) : null}
<MemberBadge name={leadName} color={memberColor} hideAvatar />
<span className="text-[10px]" style={{ color: CARD_ICON_MUTED }}>
{thoughts.length} thoughts
</span>
{!isBodyVisible && headerTextPreview ? (
<Tooltip>
<TooltipTrigger asChild>
<span
className="min-w-0 flex-1 cursor-default truncate text-[10px]"
style={{ color: CARD_TEXT_LIGHT }}
>
{headerTextPreview}
</span>
</TooltipTrigger>
{totalToolSummary ? (
<TooltipContent side="bottom" className="max-w-[420px] font-mono text-[11px]">
<ToolSummaryTooltipContent
toolCalls={allToolCalls}
toolSummary={totalToolSummary}
/>
</TooltipContent>
) : null}
</Tooltip>
) : totalToolSummary ? (
<Tooltip>
<TooltipTrigger asChild>
<span className="cursor-default text-[10px]" style={{ color: CARD_ICON_MUTED }}>
{totalToolSummary}
</span>
</TooltipTrigger>
<TooltipContent side="bottom" className="max-w-[420px] font-mono text-[11px]">
<ToolSummaryTooltipContent
toolCalls={allToolCalls}
toolSummary={totalToolSummary}
/>
</TooltipContent>
</Tooltip>
) : null}
<div className="relative ml-auto flex shrink-0 items-center">
<span
className={
onExpand && expandItemKey
? 'text-[10px] transition-opacity group-hover:opacity-0'
: 'text-[10px]'
}
style={{ color: CARD_ICON_MUTED }}
>
{timestampLabel}
</span>
{onExpand && expandItemKey && (
<button
type="button"
aria-label="Expand thoughts"
className="absolute right-0 top-1/2 -translate-y-1/2 rounded p-0.5 opacity-0 transition-opacity focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-blue-500/50 group-hover:opacity-100"
style={{ color: CARD_ICON_MUTED }}
onClick={(e) => {
e.stopPropagation();
onExpand(expandItemKey);
}}
onKeyDown={(e) => e.stopPropagation()}
>
<Maximize2 size={12} />
</button>
)}
</div>
</>
)}
</div>
{/* Scrollable body — live thoughts follow bottom unless user scrolls up */}

View file

@ -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<string, number>;
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 (
<div className="mb-3 space-y-1.5">
<p className="text-[10px] font-medium uppercase tracking-wide text-[var(--color-text-muted)]">
Awaiting replies
</p>
<div className="flex items-center justify-between gap-3">
<p className="text-[10px] font-medium uppercase tracking-wide text-[var(--color-text-muted)]">
Awaiting replies
</p>
{headerRight ? <div className="shrink-0">{headerRight}</div> : null}
</div>
{pending.map((entry) => {
const since = formatDistanceToNowStrict(entry.sentAtMs, { addSuffix: true });

View file

@ -465,7 +465,7 @@ export const MessageComposer = ({
return (
<div
className={cn('relative', isCompactLayout ? 'pb-1' : 'mb-3 pb-3')}
className={cn('relative', isCompactLayout ? 'pb-1' : 'mb-1.5 pb-1.5')}
role="group"
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}

View file

@ -648,6 +648,7 @@ export const MessagesPanel = memo(function MessagesPanel({
tasks={tasks}
messages={effectiveMessages}
pendingRepliesByMember={pendingRepliesByMember}
layout="flow"
position="inline"
onMemberClick={onMemberClick}
onTaskClick={onTaskClick}
@ -831,6 +832,7 @@ export const MessagesPanel = memo(function MessagesPanel({
tasks={tasks}
messages={effectiveMessages}
pendingRepliesByMember={pendingRepliesByMember}
layout="flow"
position="sidebar"
onMemberClick={onMemberClick}
onTaskClick={onTaskClick}

View file

@ -44,15 +44,13 @@ export const StatusBlock = ({
() => 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
</button>
);
const flowInlineToggle = layout === 'flow' && !collapsed ? toggleButton : null;
return (
<>
@ -92,21 +97,25 @@ export const StatusBlock = ({
<div className="relative h-0">
<div className="absolute -top-[19px] right-0 z-10">{toggleButton}</div>
</div>
) : (
) : collapsed ? (
<div className="mb-2 flex justify-end">{toggleButton}</div>
)}
) : null}
{!collapsed && (
<div className={layout === 'overlay' ? 'mt-5' : ''}>
<PendingRepliesBlock
members={members}
pendingRepliesByMember={pendingRepliesByMember}
pendingCrossTeamReplies={pendingCrossTeamReplies}
onMemberClick={onMemberClick}
/>
{hasPendingReplies ? (
<PendingRepliesBlock
members={members}
pendingRepliesByMember={pendingRepliesByMember}
pendingCrossTeamReplies={pendingCrossTeamReplies}
headerRight={flowInlineToggle}
onMemberClick={onMemberClick}
/>
) : null}
<ActiveTasksBlock
members={members}
tasks={tasks}
defaultCollapsed={position === 'sidebar'}
headerRight={!hasPendingReplies ? flowInlineToggle : undefined}
onMemberClick={onMemberClick}
onTaskClick={onTaskClick}
/>

View file

@ -1102,10 +1102,10 @@ export const MentionableTextarea = React.forwardRef<HTMLTextAreaElement, Mention
const orbitTrackWidth = 1;
const orbitStrokeWidth = 1.35;
const orbitGlowWidth = 3;
const orbitInset = orbitTrackWidth / 2;
const orbitWidth = Math.max(surfaceShellMetrics.width - orbitTrackWidth, 0);
const orbitHeight = Math.max(surfaceShellMetrics.height - orbitTrackWidth, 0);
const orbitRadius = Math.max(surfaceShellMetrics.borderRadius - orbitInset, 0);
const orbitInset = 0;
const orbitWidth = Math.max(surfaceShellMetrics.width, 0);
const orbitHeight = Math.max(surfaceShellMetrics.height, 0);
const orbitRadius = Math.max(surfaceShellMetrics.borderRadius, 0);
const orbitRight = orbitInset + orbitWidth;
const orbitBottom = orbitInset + orbitHeight;
const orbitMidX = orbitInset + orbitWidth / 2;
@ -1134,7 +1134,7 @@ export const MentionableTextarea = React.forwardRef<HTMLTextAreaElement, Mention
surfaceShellMetrics.width > 0 &&
surfaceShellMetrics.height > 0 ? (
<svg
className="message-composer-orbit-svg pointer-events-none absolute inset-0 z-[1] h-full w-full"
className="message-composer-orbit-svg pointer-events-none absolute inset-0 z-[16] h-full w-full"
viewBox={`0 0 ${surfaceShellMetrics.width} ${surfaceShellMetrics.height}`}
aria-hidden="true"
>

View file

@ -84,7 +84,7 @@ export const TeamGraphOverlay = ({
events={events}
onRequestClose={onClose}
onRequestPinAsTab={onPinAsTab}
className="min-w-0 flex-1"
className="team-graph-view min-w-0 flex-1"
renderEdgeOverlay={({ edge, sourceNode, targetNode, onClose: closeEdge, onSelectNode }) => (
<GraphBlockingEdgePopover
teamName={teamName}

View file

@ -114,7 +114,7 @@ export const TeamGraphTab = ({
<GraphView
data={graphData}
events={events}
className="size-full"
className="team-graph-view size-full"
suspendAnimation={!isActive}
onRequestFullscreen={() => setFullscreen(true)}
renderEdgeOverlay={({ edge, sourceNode, targetNode, onClose, onSelectNode }) => (

View file

@ -1367,7 +1367,7 @@ body.theme-transitioning {
.message-composer-orbit-glow {
stroke-dasharray: 9 91;
animation: composer-orbit-border 19s linear infinite;
stroke-linecap: round;
stroke-linecap: butt;
stroke-linejoin: round;
will-change: stroke-dashoffset;
}
@ -1418,6 +1418,78 @@ body.theme-transitioning {
stroke: rgba(138, 130, 118, 0.2);
}
:root.light .team-graph-view > .absolute.left-20.top-3 {
display: none !important;
}
:root.light .team-graph-view > .absolute.right-3.top-3 > div:not(.relative),
:root.light .team-graph-view > .absolute.right-3.top-3 > .relative > div:first-child,
:root.light .team-graph-view > .absolute.bottom-3.right-3 > div,
:root.light .team-graph-view > .absolute.bottom-3.left-3 > div {
background: rgba(248, 244, 237, 0.9) !important;
border-color: rgba(120, 113, 108, 0.18) !important;
box-shadow: 0 10px 24px rgba(120, 113, 108, 0.1);
}
:root.light .team-graph-view > .absolute.right-3.top-3 > .relative > .absolute {
background: rgba(248, 244, 237, 0.96) !important;
border-color: rgba(120, 113, 108, 0.18) !important;
box-shadow: 0 16px 32px rgba(120, 113, 108, 0.12);
}
.team-graph-view > .absolute.right-3.top-3 > div:not(.relative),
.team-graph-view > .absolute.right-3.top-3 > .relative > div:first-child {
width: 32px !important;
min-width: 32px !important;
height: 32px !important;
min-height: 32px !important;
padding: 0 !important;
justify-content: center !important;
border-radius: 10px !important;
}
.team-graph-view > .absolute.right-3.top-3 > div:not(.relative) > button,
.team-graph-view > .absolute.right-3.top-3 > .relative > div:first-child > button {
width: 100% !important;
height: 100% !important;
min-width: 100% !important;
min-height: 100% !important;
padding: 6px !important;
justify-content: center !important;
gap: 0 !important;
}
.team-graph-view > .absolute.right-3.top-3 > div:not(.relative) > button svg,
.team-graph-view > .absolute.right-3.top-3 > .relative > div:first-child > button svg {
width: 100% !important;
height: 100% !important;
}
.team-graph-view > .absolute.right-3.top-3 > div:not(.relative) > button span,
.team-graph-view > .absolute.right-3.top-3 > .relative > div:first-child > button span {
display: none !important;
}
:root.light .team-graph-view button[class*='font-mono'] {
color: rgba(70, 85, 125, 0.9) !important;
}
:root.light .team-graph-view button[class*='font-mono']:hover {
color: #334155 !important;
background: rgba(79, 70, 229, 0.08) !important;
}
:root.light
.team-graph-view
button[class*='font-mono'][class*='bg-\\[rgba\\(100\\,200\\,255\\,0\\.14\\)\\]'] {
color: #4338ca !important;
background: rgba(79, 70, 229, 0.12) !important;
}
:root.light .team-graph-view button[class*='font-mono'] svg {
opacity: 0.96;
}
@media (prefers-reduced-motion: reduce) {
.message-composer-orbit-path,
.message-composer-orbit-glow {