feat: enhance message handling and UI components for improved user experience
- Implemented lead session ID propagation in TeamDataService to enrich inbox messages without existing leadSessionId. - Added source and leadSessionId fields to message payloads in TeamInboxReader for better tracking. - Updated CSS for compact CLI logs display, improving item density in the UI. - Refactored CliLogsRichView and LeadThoughtsGroupRow to support new UI features and enhance message visibility. - Adjusted ActivityTimeline to include zebra striping for better readability of messages.
This commit is contained in:
parent
a846c3949a
commit
7afe5d4d59
10 changed files with 103 additions and 27 deletions
|
|
@ -297,6 +297,31 @@ export class TeamDataService {
|
|||
});
|
||||
}
|
||||
|
||||
// Enrich inbox messages without leadSessionId by propagating from neighboring
|
||||
// messages that have it (lead_session, user_sent). Sort chronologically (asc),
|
||||
// sweep forward, then sweep backward so orphans at the start also get a session.
|
||||
if (config.leadSessionId || messages.some((m) => m.leadSessionId)) {
|
||||
messages.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
|
||||
// Forward pass: propagate leadSessionId from earlier messages to later ones
|
||||
let currentSessionId: string | undefined;
|
||||
for (const msg of messages) {
|
||||
if (msg.leadSessionId) {
|
||||
currentSessionId = msg.leadSessionId;
|
||||
} else if (currentSessionId) {
|
||||
msg.leadSessionId = currentSessionId;
|
||||
}
|
||||
}
|
||||
// Backward pass: fill messages before the first known session
|
||||
currentSessionId = undefined;
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
if (messages[i].leadSessionId) {
|
||||
currentSessionId = messages[i].leadSessionId;
|
||||
} else if (currentSessionId) {
|
||||
messages[i].leadSessionId = currentSessionId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
messages.sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp));
|
||||
|
||||
let metaMembers: TeamConfig['members'] = [];
|
||||
|
|
|
|||
|
|
@ -99,6 +99,8 @@ export class TeamInboxReader {
|
|||
summary: typeof row.summary === 'string' ? row.summary : undefined,
|
||||
color: typeof row.color === 'string' ? row.color : undefined,
|
||||
messageId: typeof row.messageId === 'string' ? row.messageId : undefined,
|
||||
source: typeof row.source === 'string' ? (row.source as InboxMessage['source']) : undefined,
|
||||
leadSessionId: typeof row.leadSessionId === 'string' ? row.leadSessionId : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2876,7 +2876,7 @@ export class TeamProvisioningService {
|
|||
const hasSendMessageToUser = (content ?? []).some((part) => {
|
||||
if (!part || typeof part !== 'object') return false;
|
||||
if (part.type !== 'tool_use' || part.name !== 'SendMessage') return false;
|
||||
const input = (part as Record<string, unknown>).input;
|
||||
const input = part.input;
|
||||
if (!input || typeof input !== 'object') return false;
|
||||
return (input as Record<string, unknown>).recipient === 'user';
|
||||
});
|
||||
|
|
@ -2885,7 +2885,7 @@ export class TeamProvisioningService {
|
|||
.filter((part) => part.type === 'text' && typeof part.text === 'string')
|
||||
.map((part) => part.text as string);
|
||||
if (textParts.length > 0) {
|
||||
const text = textParts.join('');
|
||||
const text = textParts.join('\n');
|
||||
// Auth failures sometimes show up as assistant text (e.g. "401", "Please run /login")
|
||||
// rather than stderr or a result.subtype=error. Detect early to avoid false "ready".
|
||||
this.handleAuthFailureInOutput(run, text, 'assistant');
|
||||
|
|
@ -2908,7 +2908,7 @@ export class TeamProvisioningService {
|
|||
clearTimeout(capture.idleHandle);
|
||||
}
|
||||
capture.idleHandle = setTimeout(() => {
|
||||
const combined = capture.textParts.join('').trim();
|
||||
const combined = capture.textParts.join('\n').trim();
|
||||
capture.resolveOnce(combined);
|
||||
}, capture.idleMs);
|
||||
}
|
||||
|
|
@ -2918,7 +2918,7 @@ export class TeamProvisioningService {
|
|||
// SendMessage and avoid duplicating it as a separate lead text entry.
|
||||
if (!run.silentUserDmForward && !hasSendMessageToUser) {
|
||||
run.directReplyParts.push(text);
|
||||
const raw = run.directReplyParts.join('');
|
||||
const raw = run.directReplyParts.join('\n');
|
||||
const cleanText = stripAgentBlocks(raw).trim();
|
||||
if (cleanText.length >= TeamProvisioningService.LEAD_TEXT_MIN_LENGTH) {
|
||||
const leadName =
|
||||
|
|
@ -2927,11 +2927,13 @@ export class TeamProvisioningService {
|
|||
if (!run.leadTurnMessageTimestamp) {
|
||||
run.leadTurnMessageTimestamp = nowIso();
|
||||
}
|
||||
// Update timestamp on each text block so the live indicator stays fresh
|
||||
const currentTimestamp = nowIso();
|
||||
const messageId = `lead-turn-${run.runId}-${run.leadTurnSeq}`;
|
||||
const leadMsg: InboxMessage = {
|
||||
from: leadName,
|
||||
text: cleanText,
|
||||
timestamp: run.leadTurnMessageTimestamp,
|
||||
timestamp: currentTimestamp,
|
||||
read: true,
|
||||
summary: cleanText.length > 60 ? cleanText.slice(0, 57) + '...' : cleanText,
|
||||
messageId,
|
||||
|
|
@ -3098,11 +3100,11 @@ export class TeamProvisioningService {
|
|||
}
|
||||
if (run.leadRelayCapture) {
|
||||
const capture = run.leadRelayCapture;
|
||||
const combined = capture.textParts.join('').trim();
|
||||
const combined = capture.textParts.join('\n').trim();
|
||||
capture.resolveOnce(combined);
|
||||
} else if (run.provisioningComplete && run.directReplyParts.length > 0) {
|
||||
// Finalize the current live lead turn message (single messageId per turn).
|
||||
const rawReply = run.directReplyParts.join('').trim();
|
||||
const rawReply = run.directReplyParts.join('\n').trim();
|
||||
run.directReplyParts = [];
|
||||
const leadName =
|
||||
run.request.members.find((m) => m.role?.toLowerCase().includes('lead'))?.name ||
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ const StreamGroup = ({
|
|||
<div className="rounded border border-[var(--color-border)] bg-[var(--color-surface)]">
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center gap-2 px-2.5 py-1.5 text-left transition-colors hover:bg-[var(--color-surface-raised)]"
|
||||
className="flex w-full items-center gap-1.5 px-2 py-1 text-left transition-colors hover:bg-[var(--color-surface-raised)]"
|
||||
onClick={onToggle}
|
||||
>
|
||||
<ChevronRight
|
||||
|
|
@ -121,14 +121,19 @@ const StreamGroup = ({
|
|||
<Bot size={13} className="shrink-0 text-[var(--color-text-muted)]" />
|
||||
<span className="min-w-0 truncate text-[11px] text-[var(--color-text-secondary)]">
|
||||
{searchQueryOverride && searchQueryOverride.trim().length > 0
|
||||
? highlightQueryInText(group.summary, searchQueryOverride, `${group.id}:group-summary`, {
|
||||
forceAllActive: true,
|
||||
})
|
||||
? highlightQueryInText(
|
||||
group.summary,
|
||||
searchQueryOverride,
|
||||
`${group.id}:group-summary`,
|
||||
{
|
||||
forceAllActive: true,
|
||||
}
|
||||
)
|
||||
: group.summary}
|
||||
</span>
|
||||
</button>
|
||||
{isExpanded && (
|
||||
<div className="border-t border-[var(--color-border)] p-2">
|
||||
<div className="border-t border-[var(--color-border)] p-1.5">
|
||||
<DisplayItemList
|
||||
items={group.items}
|
||||
onItemClick={handleItemClick}
|
||||
|
|
@ -218,7 +223,11 @@ export const CliLogsRichView = ({
|
|||
)}
|
||||
onScroll={(e) => {
|
||||
const el = e.currentTarget;
|
||||
onScroll?.({ scrollTop: el.scrollTop, scrollHeight: el.scrollHeight, clientHeight: el.clientHeight });
|
||||
onScroll?.({
|
||||
scrollTop: el.scrollTop,
|
||||
scrollHeight: el.scrollHeight,
|
||||
clientHeight: el.clientHeight,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{hasContent ? (
|
||||
|
|
@ -242,10 +251,14 @@ export const CliLogsRichView = ({
|
|||
scrollRef.current = el;
|
||||
containerRefCallback?.(el);
|
||||
}}
|
||||
className={cn('max-h-[400px] space-y-1.5 overflow-y-auto', className)}
|
||||
className={cn('cli-logs-compact max-h-[400px] space-y-1 overflow-y-auto', className)}
|
||||
onScroll={(e) => {
|
||||
const el = e.currentTarget;
|
||||
onScroll?.({ scrollTop: el.scrollTop, scrollHeight: el.scrollHeight, clientHeight: el.clientHeight });
|
||||
onScroll?.({
|
||||
scrollTop: el.scrollTop,
|
||||
scrollHeight: el.scrollHeight,
|
||||
clientHeight: el.clientHeight,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{visibleGroups.map((group) =>
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import { buildMemberColorMap } from '@renderer/utils/memberHelpers';
|
|||
import { ActivityItem, isNoiseMessage } from './ActivityItem';
|
||||
import { groupTimelineItems, isLeadThought, LeadThoughtsGroupRow } from './LeadThoughtsGroup';
|
||||
|
||||
import type { InboxMessage, ResolvedTeamMember } from '@shared/types';
|
||||
import type { TimelineItem } from './LeadThoughtsGroup';
|
||||
import type { InboxMessage, ResolvedTeamMember } from '@shared/types';
|
||||
|
||||
interface ActivityTimelineProps {
|
||||
messages: InboxMessage[];
|
||||
|
|
@ -324,6 +324,7 @@ export const ActivityTimeline = ({
|
|||
memberColor={info?.color}
|
||||
isNew={newItemKeys.has(itemKey)}
|
||||
onVisible={onMessageVisible}
|
||||
zebraShade={zebraShadeSet.has(index)}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|||
import { MemberBadge } from '@renderer/components/team/MemberBadge';
|
||||
import {
|
||||
CARD_BG,
|
||||
CARD_BG_ZEBRA,
|
||||
CARD_BORDER_STYLE,
|
||||
CARD_ICON_MUTED,
|
||||
CARD_TEXT_LIGHT,
|
||||
} from '@renderer/constants/cssVariables';
|
||||
import { getTeamColorSet } from '@renderer/constants/teamColors';
|
||||
import { useStore } from '@renderer/store';
|
||||
|
||||
import type { InboxMessage } from '@shared/types';
|
||||
|
||||
|
|
@ -65,7 +67,7 @@ export function groupTimelineItems(messages: InboxMessage[]): TimelineItem[] {
|
|||
}
|
||||
|
||||
const VIEWPORT_THRESHOLD = 0.15;
|
||||
const LIVE_WINDOW_MS = 10_000;
|
||||
const LIVE_WINDOW_MS = 5_000;
|
||||
const AUTO_SCROLL_THRESHOLD = 30;
|
||||
|
||||
interface LeadThoughtsGroupRowProps {
|
||||
|
|
@ -73,6 +75,8 @@ interface LeadThoughtsGroupRowProps {
|
|||
memberColor?: string;
|
||||
isNew?: boolean;
|
||||
onVisible?: (message: InboxMessage) => void;
|
||||
/** When true, apply a subtle lighter background for zebra-striped lists. */
|
||||
zebraShade?: boolean;
|
||||
}
|
||||
|
||||
function formatTime(timestamp: string): string {
|
||||
|
|
@ -98,10 +102,20 @@ export const LeadThoughtsGroupRow = ({
|
|||
memberColor,
|
||||
isNew,
|
||||
onVisible,
|
||||
zebraShade,
|
||||
}: LeadThoughtsGroupRowProps): React.JSX.Element => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const isUserScrolledUpRef = useRef(false);
|
||||
const isTeamAlive = useStore((s) => s.selectedTeamData?.isAlive ?? false);
|
||||
const leadActivity = useStore((s) => {
|
||||
const teamName = s.selectedTeamName;
|
||||
return teamName ? s.leadActivityByTeam[teamName] : undefined;
|
||||
});
|
||||
const leadContextUpdatedAt = useStore((s) => {
|
||||
const teamName = s.selectedTeamName;
|
||||
return teamName ? s.leadContextByTeam[teamName]?.updatedAt : undefined;
|
||||
});
|
||||
|
||||
const colors = getTeamColorSet(memberColor ?? '');
|
||||
const { thoughts } = group;
|
||||
|
|
@ -113,8 +127,15 @@ export const LeadThoughtsGroupRow = ({
|
|||
// Chronological order for rendering (oldest at top, newest at bottom)
|
||||
const chronologicalThoughts = useMemo(() => [...thoughts].reverse(), [thoughts]);
|
||||
|
||||
// Live indicator: newest thought is recent (actively streaming)
|
||||
const computeIsLive = useCallback(() => isRecentTimestamp(newest.timestamp), [newest.timestamp]);
|
||||
// Live = process alive AND (lead is in active turn OR context recently updated OR fresh thought)
|
||||
const computeIsLive = useCallback(
|
||||
() =>
|
||||
isTeamAlive &&
|
||||
(leadActivity === 'active' ||
|
||||
(leadContextUpdatedAt ? isRecentTimestamp(leadContextUpdatedAt) : false) ||
|
||||
isRecentTimestamp(newest.timestamp)),
|
||||
[isTeamAlive, leadActivity, leadContextUpdatedAt, newest.timestamp]
|
||||
);
|
||||
const [isLive, setIsLive] = useState(computeIsLive);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -170,7 +191,7 @@ export const LeadThoughtsGroupRow = ({
|
|||
<article
|
||||
className="group rounded-md [overflow:clip]"
|
||||
style={{
|
||||
backgroundColor: CARD_BG,
|
||||
backgroundColor: zebraShade ? CARD_BG_ZEBRA : CARD_BG,
|
||||
border: CARD_BORDER_STYLE,
|
||||
borderLeft: `3px solid ${colors.border}`,
|
||||
opacity: isLive ? undefined : 0.75,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ import { chipToken, serializeChipsWithText } from '@renderer/types/inlineChip';
|
|||
import { buildReplyBlock } from '@renderer/utils/agentMessageFormatting';
|
||||
import { removeChipTokenFromText } from '@renderer/utils/chipUtils';
|
||||
import { formatAgentRole } from '@renderer/utils/formatAgentRole';
|
||||
import { getModifierKeyName } from '@renderer/utils/keyboardUtils';
|
||||
import { buildMemberColorMap } from '@renderer/utils/memberHelpers';
|
||||
import { AlertCircle, ImagePlus, Send, X } from 'lucide-react';
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import { useStore } from '@renderer/store';
|
|||
import { buildReplyBlock, parseMessageReply } from '@renderer/utils/agentMessageFormatting';
|
||||
import { isImageMimeType } from '@renderer/utils/attachmentUtils';
|
||||
import { formatAgentRole } from '@renderer/utils/formatAgentRole';
|
||||
import { getModifierKeyName } from '@renderer/utils/keyboardUtils';
|
||||
import { buildMemberColorMap } from '@renderer/utils/memberHelpers';
|
||||
import { stripAgentBlocks } from '@shared/constants/agentBlocks';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|||
import { api } from '@renderer/api';
|
||||
import { MemberExecutionLog } from '@renderer/components/team/members/MemberExecutionLog';
|
||||
import {
|
||||
SubagentRecentMessagesPreview,
|
||||
type SubagentPreviewMessage,
|
||||
SubagentRecentMessagesPreview,
|
||||
} from '@renderer/components/team/members/SubagentRecentMessagesPreview';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
|
||||
import { enhanceAIGroup } from '@renderer/utils/aiGroupEnhancer';
|
||||
import { formatDuration } from '@renderer/utils/formatters';
|
||||
import { transformChunksToConversation } from '@renderer/utils/groupTransformer';
|
||||
import {
|
||||
AlertCircle,
|
||||
ChevronDown,
|
||||
|
|
@ -18,9 +20,6 @@ import {
|
|||
MessageSquare,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { enhanceAIGroup } from '@renderer/utils/aiGroupEnhancer';
|
||||
import { transformChunksToConversation } from '@renderer/utils/groupTransformer';
|
||||
|
||||
import type { EnhancedChunk } from '@renderer/types/data';
|
||||
import type { MemberLogSummary } from '@shared/types';
|
||||
|
||||
|
|
@ -225,7 +224,7 @@ export const MemberLogsTab = ({
|
|||
async (log: MemberLogSummary): Promise<EnhancedChunk[] | null> => {
|
||||
if (log.kind === 'subagent') {
|
||||
const d = await api.getSubagentDetail(log.projectId, log.sessionId, log.subagentId);
|
||||
return (d?.chunks ?? null) as EnhancedChunk[] | null;
|
||||
return d?.chunks ?? null;
|
||||
}
|
||||
const d = await api.getSessionDetail(log.projectId, log.sessionId);
|
||||
return (d?.chunks ?? null) as unknown as EnhancedChunk[] | null;
|
||||
|
|
|
|||
|
|
@ -734,6 +734,21 @@ body {
|
|||
background-color: #12131a;
|
||||
}
|
||||
|
||||
/* CLI Logs compact density — reduces item height inside CliLogsRichView */
|
||||
.cli-logs-compact [role='button'] {
|
||||
padding-top: 0.2rem;
|
||||
padding-bottom: 0.2rem;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
.cli-logs-compact [role='button'] .text-sm {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
}
|
||||
.cli-logs-compact [role='button'] .text-xs {
|
||||
font-size: 0.625rem;
|
||||
line-height: 0.875rem;
|
||||
}
|
||||
|
||||
:root.light .checkerboard-bg {
|
||||
background-image:
|
||||
linear-gradient(45deg, #e2e8f0 25%, transparent 25%),
|
||||
|
|
|
|||
Loading…
Reference in a new issue