feat: add active tab system

This commit is contained in:
Alex Andru 2025-07-18 18:21:40 +02:00
commit 6b204b5d29
No known key found for this signature in database
GPG key ID: DC2D0023BD08E486
2 changed files with 116 additions and 42 deletions

View file

@ -138,11 +138,9 @@ const App = () => {
>([]); >([]);
const [isAuthDebuggerVisible, setIsAuthDebuggerVisible] = useState(false); const [isAuthDebuggerVisible, setIsAuthDebuggerVisible] = useState(false);
// Auth debugger state
const [authState, setAuthState] = const [authState, setAuthState] =
useState<AuthDebuggerState>(EMPTY_DEBUGGER_STATE); useState<AuthDebuggerState>(EMPTY_DEBUGGER_STATE);
// Helper function to update specific auth state properties
const updateAuthState = (updates: Partial<AuthDebuggerState>) => { const updateAuthState = (updates: Partial<AuthDebuggerState>) => {
setAuthState((prev) => ({ ...prev, ...updates })); setAuthState((prev) => ({ ...prev, ...updates }));
}; };
@ -170,6 +168,19 @@ const App = () => {
const [nextToolCursor, setNextToolCursor] = useState<string | undefined>(); const [nextToolCursor, setNextToolCursor] = useState<string | undefined>();
const progressTokenRef = useRef(0); const progressTokenRef = useRef(0);
const [activeTab, setActiveTab] = useState<string>(() => {
const hash = window.location.hash.slice(1);
const initialTab = hash || "resources";
return initialTab;
});
const currentTabRef = useRef<string>(activeTab);
const lastToolCallOriginTabRef = useRef<string>(activeTab);
useEffect(() => {
currentTabRef.current = activeTab;
}, [activeTab]);
const { height: historyPaneHeight, handleDragStart } = useDraggablePane(300); const { height: historyPaneHeight, handleDragStart } = useDraggablePane(300);
const { const {
width: sidebarWidth, width: sidebarWidth,
@ -213,6 +224,8 @@ const App = () => {
]); ]);
}, },
onElicitationRequest: (request, resolve) => { onElicitationRequest: (request, resolve) => {
const currentTab = lastToolCallOriginTabRef.current;
setPendingElicitationRequests((prev) => [ setPendingElicitationRequests((prev) => [
...prev, ...prev,
{ {
@ -222,16 +235,52 @@ const App = () => {
message: request.params.message, message: request.params.message,
requestedSchema: request.params.requestedSchema, requestedSchema: request.params.requestedSchema,
}, },
originatingTab: currentTab,
resolve, resolve,
decline: (error: Error) => { decline: (error: Error) => {
console.error("Elicitation request rejected:", error); console.error("Elicitation request rejected:", error);
}, },
}, },
]); ]);
setActiveTab("elicitations");
window.location.hash = "elicitations";
}, },
getRoots: () => rootsRef.current, getRoots: () => rootsRef.current,
}); });
useEffect(() => {
if (serverCapabilities) {
const hash = window.location.hash.slice(1);
const validTabs = [
...(serverCapabilities?.resources ? ["resources"] : []),
...(serverCapabilities?.prompts ? ["prompts"] : []),
...(serverCapabilities?.tools ? ["tools"] : []),
"ping",
"sampling",
"elicitations",
"roots",
"auth",
];
const isValidTab = validTabs.includes(hash);
if (!isValidTab) {
const defaultTab = serverCapabilities?.resources
? "resources"
: serverCapabilities?.prompts
? "prompts"
: serverCapabilities?.tools
? "tools"
: "ping";
setActiveTab(defaultTab);
window.location.hash = defaultTab;
}
}
}, [serverCapabilities]);
useEffect(() => { useEffect(() => {
localStorage.setItem("lastCommand", command); localStorage.setItem("lastCommand", command);
}, [command]); }, [command]);
@ -260,7 +309,6 @@ const App = () => {
saveInspectorConfig(CONFIG_LOCAL_STORAGE_KEY, config); saveInspectorConfig(CONFIG_LOCAL_STORAGE_KEY, config);
}, [config]); }, [config]);
// Auto-connect to previously saved serverURL after OAuth callback
const onOAuthConnect = useCallback( const onOAuthConnect = useCallback(
(serverUrl: string) => { (serverUrl: string) => {
setSseUrl(serverUrl); setSseUrl(serverUrl);
@ -270,7 +318,6 @@ const App = () => {
[connectMcpServer], [connectMcpServer],
); );
// Update OAuth debug state during debug callback
const onOAuthDebugConnect = useCallback( const onOAuthDebugConnect = useCallback(
async ({ async ({
authorizationCode, authorizationCode,
@ -291,7 +338,6 @@ const App = () => {
} }
if (restoredState && authorizationCode) { if (restoredState && authorizationCode) {
// Restore the previous auth state and continue the OAuth flow
let currentState: AuthDebuggerState = { let currentState: AuthDebuggerState = {
...restoredState, ...restoredState,
authorizationCode, authorizationCode,
@ -302,12 +348,10 @@ const App = () => {
}; };
try { try {
// Create a new state machine instance to continue the flow
const stateMachine = new OAuthStateMachine(sseUrl, (updates) => { const stateMachine = new OAuthStateMachine(sseUrl, (updates) => {
currentState = { ...currentState, ...updates }; currentState = { ...currentState, ...updates };
}); });
// Continue stepping through the OAuth flow from where we left off
while ( while (
currentState.oauthStep !== "complete" && currentState.oauthStep !== "complete" &&
currentState.oauthStep !== "authorization_code" currentState.oauthStep !== "authorization_code"
@ -316,7 +360,6 @@ const App = () => {
} }
if (currentState.oauthStep === "complete") { if (currentState.oauthStep === "complete") {
// After the flow completes or reaches a user-input step, update the app state
updateAuthState({ updateAuthState({
...currentState, ...currentState,
statusMessage: { statusMessage: {
@ -339,7 +382,6 @@ const App = () => {
}); });
} }
} else if (authorizationCode) { } else if (authorizationCode) {
// Fallback to the original behavior if no state was restored
updateAuthState({ updateAuthState({
authorizationCode, authorizationCode,
oauthStep: "token_request", oauthStep: "token_request",
@ -349,7 +391,6 @@ const App = () => {
[sseUrl], [sseUrl],
); );
// Load OAuth tokens when sseUrl changes
useEffect(() => { useEffect(() => {
const loadOAuthTokens = async () => { const loadOAuthTokens = async () => {
try { try {
@ -408,6 +449,18 @@ const App = () => {
} }
}, []); }, []);
useEffect(() => {
const handleHashChange = () => {
const hash = window.location.hash.slice(1);
if (hash && hash !== activeTab) {
setActiveTab(hash);
}
};
window.addEventListener("hashchange", handleHashChange);
return () => window.removeEventListener("hashchange", handleHashChange);
}, [activeTab]);
const handleApproveSampling = (id: number, result: CreateMessageResult) => { const handleApproveSampling = (id: number, result: CreateMessageResult) => {
setPendingSampleRequests((prev) => { setPendingSampleRequests((prev) => {
const request = prev.find((r) => r.id === id); const request = prev.find((r) => r.id === id);
@ -430,7 +483,34 @@ const App = () => {
) => { ) => {
setPendingElicitationRequests((prev) => { setPendingElicitationRequests((prev) => {
const request = prev.find((r) => r.id === id); const request = prev.find((r) => r.id === id);
request?.resolve(response); if (request) {
request.resolve(response);
if (request.originatingTab) {
const originatingTab = request.originatingTab;
const validTabs = [
...(serverCapabilities?.resources ? ["resources"] : []),
...(serverCapabilities?.prompts ? ["prompts"] : []),
...(serverCapabilities?.tools ? ["tools"] : []),
"ping",
"sampling",
"elicitations",
"roots",
"auth",
];
if (validTabs.includes(originatingTab)) {
setActiveTab(originatingTab);
window.location.hash = originatingTab;
setTimeout(() => {
setActiveTab(originatingTab);
window.location.hash = originatingTab;
}, 100);
}
}
}
return prev.filter((r) => r.id !== id); return prev.filter((r) => r.id !== id);
}); });
}; };
@ -492,7 +572,23 @@ const App = () => {
setNextResourceTemplateCursor(response.nextCursor); setNextResourceTemplateCursor(response.nextCursor);
}; };
const getPrompt = async (name: string, args: Record<string, string> = {}) => {
lastToolCallOriginTabRef.current = currentTabRef.current;
const response = await sendMCPRequest(
{
method: "prompts/get" as const,
params: { name, arguments: args },
},
GetPromptResultSchema,
"prompts",
);
setPromptContent(JSON.stringify(response, null, 2));
};
const readResource = async (uri: string) => { const readResource = async (uri: string) => {
lastToolCallOriginTabRef.current = currentTabRef.current;
const response = await sendMCPRequest( const response = await sendMCPRequest(
{ {
method: "resources/read" as const, method: "resources/read" as const,
@ -549,18 +645,6 @@ const App = () => {
setNextPromptCursor(response.nextCursor); setNextPromptCursor(response.nextCursor);
}; };
const getPrompt = async (name: string, args: Record<string, string> = {}) => {
const response = await sendMCPRequest(
{
method: "prompts/get" as const,
params: { name, arguments: args },
},
GetPromptResultSchema,
"prompts",
);
setPromptContent(JSON.stringify(response, null, 2));
};
const listTools = async () => { const listTools = async () => {
const response = await sendMCPRequest( const response = await sendMCPRequest(
{ {
@ -572,11 +656,12 @@ const App = () => {
); );
setTools(response.tools); setTools(response.tools);
setNextToolCursor(response.nextCursor); setNextToolCursor(response.nextCursor);
// Cache output schemas for validation
cacheToolOutputSchemas(response.tools); cacheToolOutputSchemas(response.tools);
}; };
const callTool = async (name: string, params: Record<string, unknown>) => { const callTool = async (name: string, params: Record<string, unknown>) => {
lastToolCallOriginTabRef.current = currentTabRef.current;
try { try {
const response = await sendMCPRequest( const response = await sendMCPRequest(
{ {
@ -592,6 +677,7 @@ const App = () => {
CompatibilityCallToolResultSchema, CompatibilityCallToolResultSchema,
"tools", "tools",
); );
setToolResult(response); setToolResult(response);
} catch (e) { } catch (e) {
const toolResult: CompatibilityCallToolResult = { const toolResult: CompatibilityCallToolResult = {
@ -626,7 +712,6 @@ const App = () => {
setStdErrNotifications([]); setStdErrNotifications([]);
}; };
// Helper component for rendering the AuthDebugger
const AuthDebuggerWrapper = () => ( const AuthDebuggerWrapper = () => (
<TabsContent value="auth"> <TabsContent value="auth">
<AuthDebugger <AuthDebugger
@ -638,7 +723,6 @@ const App = () => {
</TabsContent> </TabsContent>
); );
// Helper function to render OAuth callback components
if (window.location.pathname === "/oauth/callback") { if (window.location.pathname === "/oauth/callback") {
const OAuthCallback = React.lazy( const OAuthCallback = React.lazy(
() => import("./components/OAuthCallback"), () => import("./components/OAuthCallback"),
@ -698,7 +782,6 @@ const App = () => {
loggingSupported={!!serverCapabilities?.logging || false} loggingSupported={!!serverCapabilities?.logging || false}
clearStdErrNotifications={clearStdErrNotifications} clearStdErrNotifications={clearStdErrNotifications}
/> />
{/* Drag handle for resizing sidebar */}
<div <div
onMouseDown={handleSidebarDragStart} onMouseDown={handleSidebarDragStart}
style={{ style={{
@ -719,21 +802,12 @@ const App = () => {
<div className="flex-1 overflow-auto"> <div className="flex-1 overflow-auto">
{mcpClient ? ( {mcpClient ? (
<Tabs <Tabs
defaultValue={ value={activeTab}
Object.keys(serverCapabilities ?? {}).includes(
window.location.hash.slice(1),
)
? window.location.hash.slice(1)
: serverCapabilities?.resources
? "resources"
: serverCapabilities?.prompts
? "prompts"
: serverCapabilities?.tools
? "tools"
: "ping"
}
className="w-full p-4" className="w-full p-4"
onValueChange={(value) => (window.location.hash = value)} onValueChange={(value) => {
setActiveTab(value);
window.location.hash = value;
}}
> >
<TabsList className="mb-4 py-0"> <TabsList className="mb-4 py-0">
<TabsTrigger <TabsTrigger
@ -895,7 +969,6 @@ const App = () => {
clearTools={() => { clearTools={() => {
setTools([]); setTools([]);
setNextToolCursor(undefined); setNextToolCursor(undefined);
// Clear cached output schemas
cacheToolOutputSchemas([]); cacheToolOutputSchemas([]);
}} }}
callTool={async (name, params) => { callTool={async (name, params) => {

View file

@ -17,6 +17,7 @@ export interface ElicitationResponse {
export type PendingElicitationRequest = { export type PendingElicitationRequest = {
id: number; id: number;
request: ElicitationRequestData; request: ElicitationRequestData;
originatingTab?: string;
}; };
export type Props = { export type Props = {