diff --git a/src/app/chat/page.tsx b/src/app/chat/page.tsx index 2e04dcb..4905001 100644 --- a/src/app/chat/page.tsx +++ b/src/app/chat/page.tsx @@ -148,6 +148,17 @@ type ChatExecutionSnapshot = { events?: ExecutionProgressEvent[]; } | null; +type ExecutionAuditGroup = { + id: string; + createdAt: string; + progress: ExecutionProgressEvent | null; + events: ExecutionProgressEvent[]; +}; + +type TranscriptTimelineItem = + | { type: "message"; message: Message; index: number } + | { type: "audit"; audit: ExecutionAuditGroup }; + type ChatHistoryLoadResult = { sessionId: string | null; execution: ChatExecutionSnapshot; @@ -911,6 +922,55 @@ function uniqueMessagesById(messages: Message[]) { }); } +function eventTimeMs(event: ExecutionProgressEvent) { + const parsed = event.at ? Date.parse(event.at) : NaN; + return Number.isFinite(parsed) ? parsed : 0; +} + +function groupKeyForExecutionEvent(event: ExecutionProgressEvent, index: number) { + return event.runId || event.activeTool?.id || event.checkpoint?.id || `audit-${index}`; +} + +function buildExecutionAuditGroups(events: ExecutionProgressEvent[]): ExecutionAuditGroup[] { + const groups = new Map(); + events.forEach((event, index) => { + if (!event.activeTool && !event.checkpoint) return; + const key = groupKeyForExecutionEvent(event, index); + groups.set(key, [...(groups.get(key) ?? []), event]); + }); + + return Array.from(groups.entries()) + .map(([id, groupEvents]) => { + const sortedEvents = [...groupEvents].sort((a, b) => eventTimeMs(a) - eventTimeMs(b)); + const firstEvent = sortedEvents[0]; + const lastEvent = sortedEvents[sortedEvents.length - 1]; + return { + id, + createdAt: firstEvent?.at ?? lastEvent?.at ?? new Date().toISOString(), + progress: lastEvent ?? null, + events: sortedEvents, + }; + }) + .sort((a, b) => Date.parse(a.createdAt) - Date.parse(b.createdAt)); +} + +function buildTranscriptTimeline(messages: Message[], audits: ExecutionAuditGroup[]): TranscriptTimelineItem[] { + const items: TranscriptTimelineItem[] = [ + ...messages.map((message, index) => ({ type: "message" as const, message, index })), + ...audits.map((audit) => ({ type: "audit" as const, audit })), + ]; + + return items.sort((a, b) => { + const aTime = Date.parse(a.type === "message" ? a.message.createdAt ?? "" : a.audit.createdAt); + const bTime = Date.parse(b.type === "message" ? b.message.createdAt ?? "" : b.audit.createdAt); + const safeATime = Number.isFinite(aTime) ? aTime : 0; + const safeBTime = Number.isFinite(bTime) ? bTime : 0; + if (safeATime !== safeBTime) return safeATime - safeBTime; + if (a.type === b.type) return 0; + return a.type === "message" ? -1 : 1; + }); +} + function messagePreview(content: string, max = 140) { const text = content.replace(/\s+/g, " ").trim(); return text.length > max ? `${text.slice(0, max - 3)}...` : text; @@ -2507,6 +2567,14 @@ export default function ChatPage() { () => executionEvents.some((event) => event.activeTool || event.checkpoint), [executionEvents] ); + const executionAuditGroups = useMemo( + () => buildExecutionAuditGroups(executionEvents), + [executionEvents] + ); + const transcriptTimelineItems = useMemo( + () => buildTranscriptTimeline(transcriptItems, executionAuditGroups), + [executionAuditGroups, transcriptItems] + ); const scrollToMessage = useCallback((messageId: string) => { window.requestAnimationFrame(() => { @@ -5737,10 +5805,31 @@ export default function ChatPage() { )} - {transcriptItems.map((msg, i) => { - const prevDate = i > 0 ? getDateKey(transcriptItems[i - 1].createdAt) : null; - const currDate = getDateKey(msg.createdAt); + {transcriptTimelineItems.map((item, i) => { + const itemDate = item.type === "message" ? item.message.createdAt : item.audit.createdAt; + const prevItem = i > 0 ? transcriptTimelineItems[i - 1] : null; + const prevDate = prevItem + ? getDateKey(prevItem.type === "message" ? prevItem.message.createdAt : prevItem.audit.createdAt) + : null; + const currDate = getDateKey(itemDate); const showSeparator = currDate && currDate !== prevDate; + if (item.type === "audit") { + return ( +
+ {showSeparator && } + +
+ ); + } + + const msg = item.message; + const messageIndex = item.index; const threadSummary = threadReplySummaries[`id:${threadParentIdForMessage(msg)}`]; const threadReplies = threadSummary?.replies ?? []; const canPersistMessageAction = isUuid(msg.id); @@ -5767,7 +5856,7 @@ export default function ChatPage() { authorEmoji={msg.role === "assistant" ? agentEmoji : null} identityDetails={msg.role === "user" ? userIdentityDetails : assistantIdentityDetails} onOpenIdentity={setActiveIdentityProfile} - onReplyInThread={() => openThreadForMessage(msg, i, threadSummary?.sessionKey)} + onReplyInThread={() => openThreadForMessage(msg, messageIndex, threadSummary?.sessionKey)} onTogglePin={canPersistMessageAction ? () => void togglePin(msg) : undefined} onToggleSaved={canPersistMessageAction ? () => void toggleSaved(msg) : undefined} isPinned={pinnedMessageIds.has(msg.id)} @@ -5785,7 +5874,7 @@ export default function ChatPage() { })} {/* Execution progress */} - {(isLoading || executionProgress || hasPersistedExecutionActivity) && ( + {(isLoading || executionProgress || (hasPersistedExecutionActivity && executionAuditGroups.length === 0)) && (