import React, { useEffect, useRef } from "react"; import { Box, Text, useInput, type Key } from "ink"; import { ScrollView, type ScrollViewRef } from "ink-scroll-view"; import type { MessageEntry } from "@inspector/core/mcp/index.js"; import { useSelectableList } from "../hooks/useSelectableList.js"; interface HistoryTabProps { serverName: string | null; messages: MessageEntry[]; width: number; height: number; onCountChange?: (count: number) => void; focusedPane?: "messages" | "details" | null; onViewDetails?: (message: MessageEntry) => void; modalOpen?: boolean; } export function HistoryTab({ messages, width, height, onCountChange, focusedPane = null, onViewDetails, modalOpen = false, }: HistoryTabProps) { const visibleCount = Math.max(1, height - 7); const { selectedIndex, firstVisible, setSelection } = useSelectableList( messages.length, visibleCount, ); const scrollViewRef = useRef(null); const selectedMessage = messages[selectedIndex] || null; // Handle arrow key navigation and scrolling when focused useInput( (input: string, key: Key) => { if (focusedPane === "messages") { if (key.upArrow && selectedIndex > 0) { setSelection(selectedIndex - 1); } else if (key.downArrow && selectedIndex < messages.length - 1) { setSelection(selectedIndex + 1); } else if (key.pageUp) { setSelection(Math.max(0, selectedIndex - visibleCount)); } else if (key.pageDown) { setSelection( Math.min(messages.length - 1, selectedIndex + visibleCount), ); } return; } // details scrolling (only when details pane is focused) if (focusedPane === "details") { // Handle '+' key to view in full screen modal if (input === "+" && selectedMessage && onViewDetails) { onViewDetails(selectedMessage); return; } if (key.upArrow) { scrollViewRef.current?.scrollBy(-1); } else if (key.downArrow) { scrollViewRef.current?.scrollBy(1); } else if (key.pageUp) { const viewportHeight = scrollViewRef.current?.getViewportHeight() || 1; scrollViewRef.current?.scrollBy(-viewportHeight); } else if (key.pageDown) { const viewportHeight = scrollViewRef.current?.getViewportHeight() || 1; scrollViewRef.current?.scrollBy(viewportHeight); } } }, { isActive: !modalOpen && focusedPane !== undefined }, ); // Update count when messages change React.useEffect(() => { onCountChange?.(messages.length); // eslint-disable-next-line react-hooks/exhaustive-deps }, [messages.length]); // Reset details scroll when message selection changes useEffect(() => { scrollViewRef.current?.scrollTo(0); }, [selectedIndex]); const listWidth = Math.floor(width * 0.4); const detailWidth = width - listWidth; return ( {/* Left column - Messages list */} Protocol ({messages.length}) {/* Protocol message list */} {messages.length === 0 ? ( No messages ) : ( {messages .slice(firstVisible, firstVisible + visibleCount) .map((msg, i) => { const index = firstVisible + i; const isSelected = index === selectedIndex; let label: string; if (msg.direction === "request" && "method" in msg.message) { label = msg.message.method; } else if (msg.direction === "response") { if ("result" in msg.message) { label = "Response (result)"; } else if ("error" in msg.message) { label = `Response (error: ${msg.message.error.code})`; } else { label = "Response"; } } else if ( msg.direction === "notification" && "method" in msg.message ) { label = msg.message.method; } else { label = "Unknown"; } const direction = msg.direction === "request" ? "→" : msg.direction === "response" ? "←" : "•"; const hasResponse = msg.response !== undefined; return ( {isSelected ? "▶ " : " "} {direction} {label} {hasResponse ? " ✓" : msg.direction === "request" ? " ..." : ""} ); })} )} {/* Right column - Message details */} {selectedMessage ? ( <> {/* Fixed method caption only */} {selectedMessage.direction === "request" && "method" in selectedMessage.message ? selectedMessage.message.method : selectedMessage.direction === "response" ? "Response" : selectedMessage.direction === "notification" && "method" in selectedMessage.message ? selectedMessage.message.method : "Message"} {/* Scrollable content area */} {/* Metadata */} Direction: {selectedMessage.direction} {selectedMessage.timestamp.toLocaleTimeString()} {selectedMessage.duration !== undefined && ` (${selectedMessage.duration}ms)`} {selectedMessage.direction === "request" ? ( <> {/* Request label */} Request: {/* Request content */} {JSON.stringify(selectedMessage.message, null, 2) .split("\n") .map((line: string, idx: number) => ( {line} ))} {/* Response section */} {selectedMessage.response ? ( <> Response: {JSON.stringify(selectedMessage.response, null, 2) .split("\n") .map((line: string, idx: number) => ( {line} ))} ) : ( Waiting for response... )} ) : ( <> {/* Response or notification label */} {selectedMessage.direction === "response" ? "Response:" : "Notification:"} {/* Message content */} {JSON.stringify(selectedMessage.message, null, 2) .split("\n") .map((line: string, idx: number) => ( {line} ))} )} {/* Fixed footer - only show when details pane is focused */} {focusedPane === "details" && ( ↑/↓ to scroll, + to zoom )} ) : ( Select a message to view details )} ); }