import type { AgentLogEntry } from "@fusion/core"; import { ProviderIcon } from "./ProviderIcon"; import { useRef, useEffect, useState, useCallback, useLayoutEffect, useMemo, type ReactElement } from "react"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import type { Components } from "react-markdown"; import { Maximize2, Minimize2, Loader2, ChevronDown, ChevronRight } from "lucide-react"; import "./AgentLogViewer.css"; function formatTimestamp(iso: string): string { const date = new Date(iso); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMin = Math.floor(diffMs / 60000); const diffHr = Math.floor(diffMin / 60); const diffDay = Math.floor(diffHr / 24); if (diffMin < 1) return "just now"; if (diffMin < 60) return `${diffMin}m ago`; if (diffHr < 24) return `${diffHr}h ago`; if (diffDay < 7) return `${diffDay}d ago`; return date.toLocaleDateString(); } const markdownComponents: Components = { pre: ({ children, ...props }) => (
{children}
),
table: ({ children, ...props }) => (
{detail};
}
function shouldShowBadge(entry: AgentLogEntry, previousEntry?: AgentLogEntry): boolean {
if (!entry.agent) return false;
if (isToolLikeType(entry.type)) return true;
return !previousEntry || previousEntry.agent !== entry.agent || previousEntry.type !== entry.type;
}
type AgentLogRenderGroup =
| {
kind: "single";
entry: AgentLogEntry;
key: string;
showBadge: boolean;
}
| {
kind: "text" | "thinking";
entries: AgentLogEntry[];
key: string;
showBadge: boolean;
};
function buildRenderGroups(entries: AgentLogEntry[], entryKeys: string[]): AgentLogRenderGroup[] {
const groups: AgentLogRenderGroup[] = [];
for (let i = 0; i < entries.length; i += 1) {
const entry = entries[i];
const rowKey = entryKeys[i] ?? `${getEntrySignature(entry)}|fallback`;
const previousEntry = i > 0 ? entries[i - 1] : undefined;
const showBadge = shouldShowBadge(entry, previousEntry);
if (entry.type === "text" || entry.type === "thinking") {
const groupedEntries: AgentLogEntry[] = [entry];
let j = i + 1;
while (j < entries.length) {
const nextEntry = entries[j];
if (nextEntry.type !== entry.type || nextEntry.agent !== entry.agent) {
break;
}
groupedEntries.push(nextEntry);
j += 1;
}
const endKey = entryKeys[j - 1] ?? `${getEntrySignature(entries[j - 1])}|fallback`;
groups.push({
kind: entry.type,
entries: groupedEntries,
key: `${rowKey}->${endKey}`,
showBadge,
});
i = j - 1;
continue;
}
groups.push({
kind: "single",
entry,
key: rowKey,
showBadge,
});
}
return groups;
}
interface ModelInfo {
provider?: string;
modelId?: string;
}
interface AgentLogViewerProps {
entries: AgentLogEntry[];
loading: boolean;
executorModel?: ModelInfo | null;
validatorModel?: ModelInfo | null;
planningModel?: ModelInfo | null;
/** Whether more entries exist beyond what's currently loaded */
hasMore?: boolean;
/** Callback to load older entries */
onLoadMore?: () => void;
/** Whether a load more request is in progress */
loadingMore?: boolean;
/** Total number of entries (when known) for "Showing X of Y" summary */
totalCount?: number | null;
}
/**
* Renders agent log entries in a scrollable, monospace container.
*
* Features:
* - Displays entries in chronological order (oldest first, newest last)
* - Coalesces consecutive same-agent `text`/`thinking` chunks into continuous groups
* - Auto-scrolls to keep latest entries visible when streaming
* - Supports toggling between markdown-formatted and plain-text rendering
* - "Load More" button to fetch older entries when pagination is enabled
* - Shows "Showing X of Y entries" summary when totalCount is provided
*
* @param entries - Array of log entries (in chronological order, oldest first)
* @param loading - Whether initial load is in progress
* @param hasMore - Whether more older entries exist beyond the current page
* @param onLoadMore - Callback to load older entries
* @param loadingMore - Whether a load more request is in progress
* @param totalCount - Total number of entries (when known) for summary display
*/
export function AgentLogViewer({
entries,
loading,
executorModel,
validatorModel,
planningModel,
hasMore = false,
onLoadMore,
loadingMore = false,
totalCount = null,
}: AgentLogViewerProps) {
const containerRef = useRef{groupedText}
)}
{groupedText}
)}