diff --git a/src/main/services/team/TeamDataService.ts b/src/main/services/team/TeamDataService.ts index 83e3f8ca..964e620c 100644 --- a/src/main/services/team/TeamDataService.ts +++ b/src/main/services/team/TeamDataService.ts @@ -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'] = []; diff --git a/src/main/services/team/TeamInboxReader.ts b/src/main/services/team/TeamInboxReader.ts index b4e9cc5e..0e9a8849 100644 --- a/src/main/services/team/TeamInboxReader.ts +++ b/src/main/services/team/TeamInboxReader.ts @@ -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, }); } diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index 2376468d..5b7e882c 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -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).input; + const input = part.input; if (!input || typeof input !== 'object') return false; return (input as Record).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 || diff --git a/src/renderer/components/team/CliLogsRichView.tsx b/src/renderer/components/team/CliLogsRichView.tsx index 971faecd..8371ee99 100644 --- a/src/renderer/components/team/CliLogsRichView.tsx +++ b/src/renderer/components/team/CliLogsRichView.tsx @@ -108,7 +108,7 @@ const StreamGroup = ({
{isExpanded && ( -
+
{ 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) => diff --git a/src/renderer/components/team/activity/ActivityTimeline.tsx b/src/renderer/components/team/activity/ActivityTimeline.tsx index 63e0864f..0f29d8ef 100644 --- a/src/renderer/components/team/activity/ActivityTimeline.tsx +++ b/src/renderer/components/team/activity/ActivityTimeline.tsx @@ -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)} /> ); diff --git a/src/renderer/components/team/activity/LeadThoughtsGroup.tsx b/src/renderer/components/team/activity/LeadThoughtsGroup.tsx index 5228cb88..1ffaad78 100644 --- a/src/renderer/components/team/activity/LeadThoughtsGroup.tsx +++ b/src/renderer/components/team/activity/LeadThoughtsGroup.tsx @@ -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(null); const scrollRef = useRef(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 = ({
=> { 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; diff --git a/src/renderer/index.css b/src/renderer/index.css index 563130fa..7506603d 100644 --- a/src/renderer/index.css +++ b/src/renderer/index.css @@ -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%),