import React, { useEffect, useRef } from "react"; import { Box, Text, useInput, type Key } from "ink"; import { ScrollView, type ScrollViewRef } from "ink-scroll-view"; import type { FetchRequestEntry } from "@inspector/core/mcp/index.js"; import { useSelectableList } from "../hooks/useSelectableList.js"; interface RequestsTabProps { serverName: string | null; requests: FetchRequestEntry[]; width: number; height: number; onCountChange?: (count: number) => void; focusedPane?: "requests" | "details" | null; onViewDetails?: (request: FetchRequestEntry) => void; modalOpen?: boolean; } export function RequestsTab({ requests, width, height, onCountChange, focusedPane = null, onViewDetails, modalOpen = false, }: RequestsTabProps) { const visibleCount = Math.max(1, height - 7); const { selectedIndex, firstVisible, setSelection } = useSelectableList( requests.length, visibleCount, ); const scrollViewRef = useRef(null); const selectedRequest = requests[selectedIndex] || null; // Handle arrow key navigation and scrolling when focused useInput( (input: string, key: Key) => { if (focusedPane === "requests") { if (key.upArrow && selectedIndex > 0) { setSelection(selectedIndex - 1); } else if (key.downArrow && selectedIndex < requests.length - 1) { setSelection(selectedIndex + 1); } else if (key.pageUp) { setSelection(Math.max(0, selectedIndex - visibleCount)); } else if (key.pageDown) { setSelection( Math.min(requests.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 === "+" && selectedRequest && onViewDetails) { onViewDetails(selectedRequest); 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 requests change React.useEffect(() => { onCountChange?.(requests.length); // eslint-disable-next-line react-hooks/exhaustive-deps }, [requests.length]); // Reset details scroll when request selection changes useEffect(() => { scrollViewRef.current?.scrollTo(0); }, [selectedIndex]); const listWidth = Math.floor(width * 0.4); const detailWidth = width - listWidth; const getStatusColor = (status?: number): string => { if (!status) return "gray"; if (status >= 200 && status < 300) return "green"; if (status >= 300 && status < 400) return "yellow"; if (status >= 400) return "red"; return "gray"; }; return ( {/* Left column - Requests list */} Network ({requests.length}) {/* Requests list */} {requests.length === 0 ? ( No requests ) : ( {requests .slice(firstVisible, firstVisible + visibleCount) .map((req, i) => { const index = firstVisible + i; const isSelected = index === selectedIndex; const statusColor = getStatusColor(req.responseStatus); const statusText = req.responseStatus ? `${req.responseStatus}` : req.error ? "ERROR" : "..."; const categoryLabel = req.category === "auth" ? "AUTH" : "MCP "; const methodPadded = req.method === "GET" ? "GET " : req.method; return ( {isSelected ? "▶ " : " "} {categoryLabel}{" "} {methodPadded}{" "} {statusText} {req.duration !== undefined && ( {req.duration}ms )} ); })} )} {/* Right column - Request details */} {selectedRequest ? ( <> {/* Fixed header */} {selectedRequest.method} {selectedRequest.url} {/* Scrollable content area */} {/* Category */} Category:{" "} {selectedRequest.category === "auth" ? "auth" : "transport"} {/* Status */} {selectedRequest.responseStatus !== undefined ? ( Status:{" "} {selectedRequest.responseStatus}{" "} {selectedRequest.responseStatusText || ""} ) : selectedRequest.error ? ( Error: {selectedRequest.error} ) : ( Request in progress... )} {/* Duration */} {selectedRequest.duration !== undefined && ( {selectedRequest.timestamp.toLocaleTimeString()} ( {selectedRequest.duration}ms) )} {/* Request Headers */} Request Headers: {Object.entries(selectedRequest.requestHeaders).map( ([key, value]) => ( {key}: {value} ), )} {/* Request Body */} {selectedRequest.requestBody && ( <> Request Body: {(() => { try { const parsed = JSON.parse(selectedRequest.requestBody); return JSON.stringify(parsed, null, 2) .split("\n") .map((line: string, idx: number) => ( {line} )); } catch { return ( {selectedRequest.requestBody} ); } })()} )} {/* Response Headers */} {selectedRequest.responseHeaders && Object.keys(selectedRequest.responseHeaders).length > 0 && ( <> Response Headers: {Object.entries(selectedRequest.responseHeaders).map( ([key, value]) => ( {key}: {value} ), )} )} {/* Response Body */} {selectedRequest.responseBody && ( <> Response Body: {(() => { try { const parsed = JSON.parse(selectedRequest.responseBody); return JSON.stringify(parsed, null, 2) .split("\n") .map((line: string, idx: number) => ( {line} )); } catch { return ( {selectedRequest.responseBody} ); } })()} )} {/* Fixed footer - only show when details pane is focused */} {focusedPane === "details" && ( ↑/↓ to scroll, + to zoom )} ) : ( Select a request to view details )} ); }