import React from 'react'; import Markdown from 'marked-react'; import type { ChatMessage } from '../../types/electron'; import type { ChatToolEvent } from '../../navigation/useChatSurfaceState'; import type { A2UIResolvedComponent, A2UIClientAction } from '../../../main/a2ui/types'; import { InlineSurface } from '../../a2ui/InlineSurface'; import type { SurfaceEntry } from '../../a2ui/useA2UISurface'; import { computeTurnIndex } from '../../a2ui/surfaceAssociation'; interface ChatTranscriptProps { messages: ChatMessage[]; isStreaming: boolean; streamingContent: string; toolEvents: ChatToolEvent[]; assistantRoleLabel: string; userRoleLabel: string; showToolMarkers?: boolean; endRef?: React.RefObject; /** Surfaces grouped by the turn index that created them */ surfacesByTurn?: Map; /** The surfaceId of the most recently created surface */ latestSurfaceId?: string | null; /** Set of surface IDs the user has dismissed */ dismissedSurfaceIds?: Set; /** Callback to dismiss a surface */ onSurfaceDismiss?: (surfaceId: string) => void; /** Callback for surface actions */ onSurfaceAction?: (action: A2UIClientAction) => void; /** Callback for surface data changes */ onSurfaceDataChange?: (surfaceId: string, path: string, value: unknown) => void; /** The current streaming turn index */ currentTurnIndex?: number; } export const ChatTranscript: React.FC = ({ messages, isStreaming, streamingContent, toolEvents, assistantRoleLabel, userRoleLabel, showToolMarkers = true, endRef, surfacesByTurn, latestSurfaceId, dismissedSurfaceIds, onSurfaceDismiss, onSurfaceAction, onSurfaceDataChange, currentTurnIndex, }) => { const renderToolMarkers = (events: ChatToolEvent[]) => { if (events.length === 0) { return null; } const markers: Array<{ name: string; args?: unknown; completed: boolean }> = []; for (const event of events) { if (event.type === 'call') { markers.push({ name: event.name, args: event.args, completed: false }); } else if (event.type === 'result') { for (let markerIndex = markers.length - 1; markerIndex >= 0; markerIndex -= 1) { if (markers[markerIndex].name === event.name && !markers[markerIndex].completed) { markers[markerIndex].completed = true; break; } } } } return (
{markers.map((marker, index) => { const argsPreview = marker.args ? Object.entries(marker.args as Record) .map(([key, value]) => `${key}: ${typeof value === 'string' ? `"${value.length > 30 ? value.slice(0, 30) + '...' : value}"` : JSON.stringify(value)}`) .join(', ') : ''; return (
{marker.completed ? '\u2713' : '\u25CF'} {marker.name} {argsPreview && ({argsPreview})}
); })}
); }; const renderInlineSurfaces = (turnIndex: number) => { if (!surfacesByTurn?.has(turnIndex)) { return null; } const turnSurfaces = surfacesByTurn.get(turnIndex)!; const visibleSurfaces = turnSurfaces.filter( (s) => !dismissedSurfaceIds?.has(s.surfaceId), ); if (visibleSurfaces.length === 0) { return null; } return visibleSurfaces.map((surface) => ( )); }; const renderMessage = (message: ChatMessage, messageIndex: number) => { if (message.role === 'system' || message.role === 'tool') { return null; } const storedToolCalls: Array<{ name: string; args?: unknown; completed: boolean }> = []; if (message.role === 'assistant' && message.toolCalls) { try { const parsedToolCalls = JSON.parse(message.toolCalls) as Array<{ name: string; args?: unknown }>; parsedToolCalls.forEach((toolCall) => storedToolCalls.push({ name: toolCall.name, args: toolCall.args, completed: true })); } catch { // no-op } } const messageEl = (
{message.role === 'user' ? '\u{1F464}' : '\u{1F916}'}
{message.role === 'user' ? userRoleLabel : assistantRoleLabel}
{showToolMarkers && storedToolCalls.length > 0 && (
{storedToolCalls.map((marker, markerIndex) => { const argsPreview = marker.args ? Object.entries(marker.args as Record) .map(([key, value]) => `${key}: ${typeof value === 'string' ? `"${value.length > 30 ? value.slice(0, 30) + '...' : value}"` : JSON.stringify(value)}`) .join(', ') : ''; return (
{'\u2713'} {marker.name} {argsPreview && ({argsPreview})}
); })}
)}
{message.role === 'assistant' ? {message.content} : message.content}
); // After an assistant message, render any inline surfaces for this turn if (message.role === 'assistant' && surfacesByTurn) { const turnIndex = computeTurnIndex(messages, messageIndex); const inlineSurfaces = renderInlineSurfaces(turnIndex); if (inlineSurfaces) { return ( {messageEl} {inlineSurfaces} ); } } return messageEl; }; return ( <> {messages.map((message, index) => renderMessage(message, index))} {isStreaming && (streamingContent || (showToolMarkers && toolEvents.length > 0)) && ( <>
{'\u{1F916}'}
{assistantRoleLabel} {'\u25CF'}
{showToolMarkers ? renderToolMarkers(toolEvents) : null} {streamingContent && (
{streamingContent}
)}
{currentTurnIndex !== undefined && renderInlineSurfaces(currentTurnIndex)} )} {isStreaming && !streamingContent && (!showToolMarkers || toolEvents.length === 0) && (
{'\u{1F916}'}
)}
); };