import React, { useState, useEffect, useRef } from "react"; import { Box, Text, useInput, type Key } from "ink"; import { ScrollView, type ScrollViewRef } from "ink-scroll-view"; import type { InspectorClient } from "@inspector/core/mcp/index.js"; import { AuthRecoveryRequiredError } from "@inspector/core/auth/challenge.js"; import type { Prompt, PromptArgument, GetPromptResult, } from "@modelcontextprotocol/sdk/types.js"; import { useSelectableList } from "../hooks/useSelectableList.js"; interface PromptsTabProps { prompts: Prompt[]; inspectorClient: InspectorClient | null; width: number; height: number; onCountChange?: (count: number) => void; focusedPane?: "list" | "details" | null; onViewDetails?: (prompt: Prompt & { result?: GetPromptResult }) => void; onFetchPrompt?: (prompt: Prompt) => void; onAuthRecoveryRequired?: (error: AuthRecoveryRequiredError) => void; modalOpen?: boolean; } export function PromptsTab({ prompts, inspectorClient, width, height, focusedPane = null, onViewDetails, onFetchPrompt, onAuthRecoveryRequired, modalOpen = false, }: PromptsTabProps) { const visibleCount = Math.max(1, height - 7); const { selectedIndex, firstVisible, setSelection } = useSelectableList( prompts.length, visibleCount, { resetWhen: prompts }, ); const [error, setError] = useState(null); const scrollViewRef = useRef(null); // Handle arrow key navigation when focused useInput( (input: string, key: Key) => { // Handle Enter key to fetch prompt (works from both list and details) if (key.return && selectedPrompt && inspectorClient && onFetchPrompt) { // If prompt has arguments, open modal to collect them // Otherwise, fetch directly if (selectedPrompt.arguments && selectedPrompt.arguments.length > 0) { onFetchPrompt(selectedPrompt); } else { // No arguments, fetch directly (async () => { try { const invocation = await inspectorClient.getPrompt( selectedPrompt.name, ); // Show result in details modal if (onViewDetails) { onViewDetails({ ...selectedPrompt, result: invocation.result, }); } } catch (error) { if (error instanceof AuthRecoveryRequiredError) { onAuthRecoveryRequired?.(error); return; } setError( error instanceof Error ? error.message : "Failed to get prompt", ); } })(); } return; } if (focusedPane === "list") { if (key.upArrow && selectedIndex > 0) { setSelection(selectedIndex - 1); } else if (key.downArrow && selectedIndex < prompts.length - 1) { setSelection(selectedIndex + 1); } return; } if (focusedPane === "details") { // Handle '+' key to view in full screen modal if (input === "+" && selectedPrompt && onViewDetails) { onViewDetails(selectedPrompt); return; } // Scroll the details pane using ink-scroll-view 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 === "list" || focusedPane === "details"), }, ); // Reset scroll when selection changes useEffect(() => { scrollViewRef.current?.scrollTo(0); }, [selectedIndex]); const selectedPrompt = prompts[selectedIndex] || null; const listWidth = Math.floor(width * 0.4); const detailWidth = width - listWidth; return ( {/* Prompts List */} Prompts ({prompts.length}) {error ? ( {error} ) : prompts.length === 0 ? ( No prompts available ) : ( {prompts .slice(firstVisible, firstVisible + visibleCount) .map((prompt, i) => { const index = firstVisible + i; const isSelected = index === selectedIndex; return ( {isSelected ? "▶ " : " "} {prompt.name || `Prompt ${index + 1}`} ); })} )} {/* Prompt Details */} {selectedPrompt ? ( <> {/* Fixed header */} {selectedPrompt.name} {/* Scrollable content area - direct ScrollView with height prop like NotificationsTab */} {/* Description */} {selectedPrompt.description && ( <> {selectedPrompt.description .split("\n") .map((line: string, idx: number) => ( {line} ))} )} {/* Arguments */} {selectedPrompt.arguments && selectedPrompt.arguments.length > 0 && ( <> Arguments: {selectedPrompt.arguments.map( (arg: PromptArgument, idx: number) => ( - {arg.name}:{" "} {arg.description ?? (arg as { type?: string }).type ?? "string"} ), )} )} {/* Enter to Get Prompt message */} [Enter to Get Prompt] {/* Fixed footer - only show when details pane is focused */} {focusedPane === "details" && ( ↑/↓ to scroll, + to zoom )} ) : ( Select a prompt to view details )} ); }