import React, { useState } from "react"; import { Box, Text, useInput, type Key } from "ink"; import { Form } from "ink-form"; import { InspectorClient } from "@inspector/core/mcp/index.js"; import { AuthRecoveryRequiredError } from "@inspector/core/auth/challenge.js"; import type { Prompt, GetPromptResult, } from "@modelcontextprotocol/sdk/types.js"; import { promptArgsToForm } from "../utils/promptArgsToForm.js"; import { ScrollView, type ScrollViewRef } from "ink-scroll-view"; // Helper to extract error message from various error types function getErrorMessage(error: unknown): string { if (error instanceof Error) { return error.message; } if (typeof error === "string") { return error; } if (error && typeof error === "object" && "message" in error) { return String(error.message); } return "Unknown error"; } interface PromptTestModalProps { prompt: Prompt; inspectorClient: InspectorClient | null; width: number; height: number; onClose: () => void; onAuthRecoveryRequired?: (error: AuthRecoveryRequiredError) => void; } type ModalState = "form" | "loading" | "results"; interface PromptResult { input: Record; output: GetPromptResult | null; error?: string; errorDetails?: unknown; duration: number; } export function PromptTestModal({ prompt, inspectorClient, width, height, onClose, onAuthRecoveryRequired, }: PromptTestModalProps) { const [state, setState] = useState("form"); const [result, setResult] = useState(null); const scrollViewRef = React.useRef(null); // Use full terminal dimensions instead of passed dimensions const [terminalDimensions, setTerminalDimensions] = React.useState({ width: process.stdout.columns || width, height: process.stdout.rows || height, }); React.useEffect(() => { const updateDimensions = () => { setTerminalDimensions({ width: process.stdout.columns || width, height: process.stdout.rows || height, }); }; process.stdout.on("resize", updateDimensions); updateDimensions(); return () => { process.stdout.off("resize", updateDimensions); }; }, [width, height]); const formStructure = promptArgsToForm( prompt.arguments || [], prompt.name || "Unknown Prompt", ); // Reset state when modal closes React.useEffect(() => { return () => { // Cleanup: reset state when component unmounts setState("form"); setResult(null); }; }, []); // Handle all input when modal is open - prevents input from reaching underlying components // When in form mode, only handle escape (form handles its own input) // When in results mode, handle scrolling keys useInput( (input: string, key: Key) => { // Always handle escape to close modal if (key.escape) { setState("form"); setResult(null); onClose(); return; } if (state === "form") { // In form mode, let the form handle all other input // Don't process anything else - this prevents input from reaching underlying components return; } if (state === "results") { // Allow scrolling in results view if (key.downArrow) { scrollViewRef.current?.scrollBy(1); } else if (key.upArrow) { scrollViewRef.current?.scrollBy(-1); } else if (key.pageDown) { const viewportHeight = scrollViewRef.current?.getViewportHeight() || 1; scrollViewRef.current?.scrollBy(viewportHeight); } else if (key.pageUp) { const viewportHeight = scrollViewRef.current?.getViewportHeight() || 1; scrollViewRef.current?.scrollBy(-viewportHeight); } } }, { isActive: true }, ); const handleFormSubmit = async (values: Record) => { if (!inspectorClient || !prompt) return; setState("loading"); const startTime = Date.now(); try { // Get the prompt using the provided arguments const invocation = await inspectorClient.getPrompt(prompt.name, values); const duration = Date.now() - startTime; setResult({ input: values, output: invocation.result, duration, }); setState("results"); } catch (error) { if (error instanceof AuthRecoveryRequiredError) { onAuthRecoveryRequired?.(error); onClose(); return; } const duration = Date.now() - startTime; const errorMessage = getErrorMessage(error); // Extract detailed error information const errorObj: Record = { message: errorMessage, }; if (error instanceof Error) { errorObj.name = error.name; errorObj.stack = error.stack; } else if (error && typeof error === "object") { // Try to extract more details from error object Object.assign(errorObj, error); } else { errorObj.error = String(error); } setResult({ input: values, output: null, error: errorMessage, errorDetails: errorObj, duration, }); setState("results"); } }; // Calculate modal dimensions - use almost full screen const modalWidth = terminalDimensions.width - 2; const modalHeight = terminalDimensions.height - 2; return ( {/* Modal Content */} {/* Header */} {formStructure.title} (Press ESC to close) {/* Content Area */} {state === "form" && ( {prompt.description && ( {prompt.description} )}
handleFormSubmit(values as Record) } /> )} {state === "loading" && ( Getting prompt... )} {state === "results" && result && ( {/* Timing */} Duration: {result.duration}ms {/* Input */} {Object.keys(result.input).length > 0 && ( Arguments: {JSON.stringify(result.input, null, 2)} )} {/* Output or Error */} {result.error ? ( Error: {String(result.error)} {result.errorDetails != null ? ( <> Error Details: {JSON.stringify(result.errorDetails, null, 2)} ) : null} ) : ( Prompt Messages: {JSON.stringify(result.output, null, 2)} )} )} ); }