({ projectId, onSessionStatusChange, onProjectStatusUpdate, onSseFallbackActive, startRequest, completeRequest, onAddUserMessage }: ChatLogProps)
| 998 | } |
| 999 | |
| 1000 | export default function ChatLog({ projectId, onSessionStatusChange, onProjectStatusUpdate, onSseFallbackActive, startRequest, completeRequest, onAddUserMessage }: ChatLogProps) { |
| 1001 | const [messages, setMessages] = useState<ChatMessage[]>([]); |
| 1002 | const [logs, setLogs] = useState<LogEntry[]>([]); |
| 1003 | const [selectedLog, setSelectedLog] = useState<LogEntry | null>(null); |
| 1004 | const [isLoading, setIsLoading] = useState(true); |
| 1005 | const [hasLoadedOnce, setHasLoadedOnce] = useState(false); |
| 1006 | const [hasError, setHasError] = useState(false); |
| 1007 | const [errorMessage, setErrorMessage] = useState<string | null>(null); |
| 1008 | const [activeSession, setActiveSession] = useState<ActiveSession | null>(null); |
| 1009 | const [isWaitingForResponse, setIsWaitingForResponse] = useState(false); |
| 1010 | const [needsHistoryRefresh, setNeedsHistoryRefresh] = useState(false); |
| 1011 | const logsEndRef = useRef<HTMLDivElement>(null); |
| 1012 | const pollIntervalRef = useRef<NodeJS.Timeout | null>(null); |
| 1013 | const hasLoadedInitialDataRef = useRef(false); |
| 1014 | const sseFallbackTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); |
| 1015 | const hasLoggedSseFallbackRef = useRef(false); |
| 1016 | const [enableSseFallback, setEnableSseFallback] = useState(false); |
| 1017 | const [isSseConnected, setIsSseConnected] = useState(false); |
| 1018 | const [failedImageUrls, setFailedImageUrls] = useState<Set<string>>(new Set()); |
| 1019 | const [expandedToolMessages, setExpandedToolMessages] = useState<Record<string, ToolExpansionState>>({}); |
| 1020 | const fallbackMessageIdRef = useRef<Map<string, string>>(new Map()); |
| 1021 | const visibleToolMessageIdsRef = useRef<Set<string>>(new Set()); |
| 1022 | |
| 1023 | const ensureStableMessageId = useCallback((message: ChatMessage): string => { |
| 1024 | if (message.id) { |
| 1025 | return message.id; |
| 1026 | } |
| 1027 | |
| 1028 | const parts: string[] = []; |
| 1029 | const addPart = (label: string, value: unknown) => { |
| 1030 | const candidate = pickFirstString(value); |
| 1031 | if (candidate) { |
| 1032 | parts.push(`${label}:${candidate}`); |
| 1033 | } |
| 1034 | }; |
| 1035 | |
| 1036 | addPart('request', message.requestId); |
| 1037 | addPart('parent', message.parentMessageId); |
| 1038 | addPart('session', message.sessionId); |
| 1039 | addPart('type', message.messageType); |
| 1040 | addPart('role', message.role); |
| 1041 | addPart('cli', message.cliSource); |
| 1042 | addPart('created', message.createdAt); |
| 1043 | |
| 1044 | const metadata = |
| 1045 | message.metadata && typeof message.metadata === 'object' |
| 1046 | ? (message.metadata as Record<string, unknown>) |
| 1047 | : null; |
| 1048 | if (metadata) { |
| 1049 | const toolCallId = extractToolCallId(metadata); |
| 1050 | if (toolCallId) { |
| 1051 | addPart('tool_call', toolCallId); |
| 1052 | } |
| 1053 | } |
| 1054 | |
| 1055 | const fingerprint = |
| 1056 | parts.length > 0 |
| 1057 | ? parts.join('|') |
nothing calls this directly
no test coverage detected