feat: add active tab system
This commit is contained in:
parent
29678dd096
commit
6b204b5d29
2 changed files with 116 additions and 42 deletions
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue