Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 136 additions & 27 deletions mcpjam-inspector/client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { ChatTabV2 } from "./components/ChatTabV2";
import { EvalsTab } from "./components/EvalsTab";
import { CiEvalsTab } from "./components/CiEvalsTab";
import { ViewsTab } from "./components/ViewsTab";
import { SandboxesTab } from "./components/SandboxesTab";
import { SettingsTab } from "./components/SettingsTab";
import { TracingTab } from "./components/TracingTab";
import { AuthTab } from "./components/AuthTab";
Expand Down Expand Up @@ -54,13 +55,23 @@ import {
SharedServerChatPage,
getSharedPathTokenFromLocation,
} from "./components/hosted/SharedServerChatPage";
import {
SandboxChatPage,
getSandboxPathTokenFromLocation,
} from "./components/hosted/SandboxChatPage";
import { useHostedApiContext } from "./hooks/hosted/use-hosted-api-context";
import { HOSTED_MODE } from "./lib/config";
import { resolveHostedNavigation } from "./lib/hosted-navigation";
import { buildOAuthTokensByServerId } from "./lib/oauth/oauth-tokens";
import {
clearSandboxSignInReturnPath,
readSandboxSession,
readSandboxSignInReturnPath,
SANDBOX_OAUTH_PENDING_KEY,
writeSandboxSignInReturnPath,
} from "./lib/sandbox-session";
import {
clearSharedSignInReturnPath,
hasActiveSharedSession,
readSharedServerSession,
readSharedSignInReturnPath,
slugify,
Expand Down Expand Up @@ -88,38 +99,106 @@ export default function App() {
isLoading: isWorkOsLoading,
} = useAuth();
const { isAuthenticated, isLoading: isAuthLoading } = useConvexAuth();
const [sharedOAuthHandling, setSharedOAuthHandling] = useState(false);
const [hostedOAuthHandling, setHostedOAuthHandling] = useState(false);
const [exitedSharedChat, setExitedSharedChat] = useState(false);
const [exitedSandboxChat, setExitedSandboxChat] = useState(false);
const sharedPathToken = HOSTED_MODE ? getSharedPathTokenFromLocation() : null;
const sandboxPathToken = HOSTED_MODE
? getSandboxPathTokenFromLocation()
: null;
const sharedSession = HOSTED_MODE ? readSharedServerSession() : null;
const sandboxSession = HOSTED_MODE ? readSandboxSession() : null;
const currentHashSlug = window.location.hash
.replace(/^#/, "")
.replace(/^\/+/, "")
.split("/")[0];
const hostedRouteKind = useMemo(() => {
if (!HOSTED_MODE) {
return null;
}

if (sharedPathToken) {
return "shared" as const;
}
if (sandboxPathToken) {
return "sandbox" as const;
}

if (sharedSession && sandboxSession) {
if (currentHashSlug === slugify(sharedSession.payload.serverName)) {
return "shared" as const;
}
if (currentHashSlug === slugify(sandboxSession.payload.name)) {
return "sandbox" as const;
}
return null;
}

if (sharedSession) {
return "shared" as const;
}
if (sandboxSession) {
return "sandbox" as const;
}

return null;
}, [
currentHashSlug,
sandboxPathToken,
sandboxSession,
sharedPathToken,
sharedSession,
]);
const isSharedChatRoute =
HOSTED_MODE &&
!exitedSharedChat &&
(!!sharedPathToken || hasActiveSharedSession());
HOSTED_MODE && !exitedSharedChat && hostedRouteKind === "shared";
const isSandboxChatRoute =
HOSTED_MODE && !exitedSandboxChat && hostedRouteKind === "sandbox";
const isHostedChatRoute = isSharedChatRoute || isSandboxChatRoute;

// Handle shared OAuth callback: detect code + pending flag before normal rendering
// Handle hosted OAuth callback: detect code + hosted pending flag before normal rendering.
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get("code");
if (!code || !localStorage.getItem(SHARED_OAUTH_PENDING_KEY)) return;
const hasSharedOAuthPending = !!localStorage.getItem(
SHARED_OAUTH_PENDING_KEY,
);
const hasSandboxOAuthPending = !!localStorage.getItem(
SANDBOX_OAUTH_PENDING_KEY,
);
if (!code || (!hasSharedOAuthPending && !hasSandboxOAuthPending)) return;

let cancelled = false;
setSharedOAuthHandling(true);
setHostedOAuthHandling(true);

const cleanupOAuth = () => {
if (cancelled) return;
localStorage.removeItem(SHARED_OAUTH_PENDING_KEY);
const storedSession = readSharedServerSession();
const sharedHash = storedSession
? slugify(storedSession.payload.serverName)
: "shared";
window.history.replaceState({}, "", `/#${sharedHash}`);
localStorage.removeItem(SANDBOX_OAUTH_PENDING_KEY);

const sandboxSession = readSandboxSession();
if (hasSandboxOAuthPending && sandboxSession) {
window.history.replaceState(
{},
"",
`/#${slugify(sandboxSession.payload.name)}`,
);
return;
}

const sharedSession = readSharedServerSession();
const hostedHash = sharedSession
? slugify(sharedSession.payload.serverName)
: hasSandboxOAuthPending
? "sandbox"
: "shared";
window.history.replaceState({}, "", `/#${hostedHash}`);
};

handleOAuthCallback(code)
.then(cleanupOAuth)
.catch(cleanupOAuth)
.finally(() => {
if (!cancelled) setSharedOAuthHandling(false);
if (!cancelled) setHostedOAuthHandling(false);
});

return () => {
Expand Down Expand Up @@ -167,9 +246,15 @@ export default function App() {

// Let AuthKit + Convex auth settle before leaving /callback.
if (!isAuthLoading && isAuthenticated) {
const sandboxReturnPath = readSandboxSignInReturnPath();
const sharedReturnPath = readSharedSignInReturnPath();
clearSandboxSignInReturnPath();
clearSharedSignInReturnPath();
window.history.replaceState({}, "", sharedReturnPath ?? "/");
window.history.replaceState(
{},
"",
sandboxReturnPath ?? sharedReturnPath ?? "/",
);
setCallbackCompleted(true);
setCallbackRecoveryExpired(false);
return;
Expand Down Expand Up @@ -212,7 +297,7 @@ export default function App() {

// Auto-add a shared server when returning from SharedServerChatPage via "Open MCPJam"
useEffect(() => {
if (isSharedChatRoute) return;
if (isHostedChatRoute) return;
if (isLoadingRemoteWorkspaces) return;
if (isAuthLoading) return;

Expand All @@ -233,7 +318,7 @@ export default function App() {
oauthScopes: pending.oauthScopes ?? undefined,
});
}, [
isSharedChatRoute,
isHostedChatRoute,
isLoadingRemoteWorkspaces,
isAuthLoading,
workspaceServers,
Expand Down Expand Up @@ -306,7 +391,7 @@ export default function App() {
guestOauthTokensByServerName,
isAuthenticated,
serverConfigs: guestServerConfigs,
enabled: !isSharedChatRoute,
enabled: !isHostedChatRoute,
});

// Compute the set of server names that have saved views
Expand Down Expand Up @@ -337,6 +422,17 @@ export default function App() {
return;
}

if (isSandboxChatRoute) {
const storedSession = readSandboxSession();
if (storedSession) {
const expectedHash = slugify(storedSession.payload.name);
if (window.location.hash !== `#${expectedHash}`) {
window.location.hash = expectedHash;
}
}
return;
}

const resolved = resolveHostedNavigation(target, HOSTED_MODE);

if (
Expand Down Expand Up @@ -374,12 +470,16 @@ export default function App() {
}
setActiveTab(resolved.normalizedTab);
},
[isSharedChatRoute, setSelectedMultipleServersToAllServers],
[
isSandboxChatRoute,
isSharedChatRoute,
setSelectedMultipleServersToAllServers,
],
);

// Sync tab with hash on mount and when hash changes
useEffect(() => {
if (isSharedChatRoute) {
if (isHostedChatRoute) {
return;
}

Expand All @@ -390,7 +490,7 @@ export default function App() {
applyHash();
window.addEventListener("hashchange", applyHash);
return () => window.removeEventListener("hashchange", applyHash);
}, [applyNavigation, isSharedChatRoute]);
}, [applyNavigation, isHostedChatRoute]);

// Redirect away from tabs hidden by the ci-evals feature flag.
// Use strict equality to avoid redirecting while the flag is still loading (undefined).
Expand All @@ -410,7 +510,7 @@ export default function App() {
return <OAuthDebugCallback />;
}

if (sharedOAuthHandling) {
if (hostedOAuthHandling) {
return <LoadingScreen />;
}

Expand Down Expand Up @@ -447,7 +547,7 @@ export default function App() {
return <CompletingSignInLoading />;
}

if (isLoading && !isSharedChatRoute) {
if (isLoading && !isHostedChatRoute) {
return <LoadingScreen />;
}

Expand All @@ -459,7 +559,7 @@ export default function App() {
hasWorkOsUser: !!workOsUser,
isLoadingRemoteWorkspaces,
});
const sharedHostedShellGateState = resolveHostedShellGateState({
const hostedChatShellGateState = resolveHostedShellGateState({
hostedMode: HOSTED_MODE,
isConvexAuthLoading: isAuthLoading,
isConvexAuthenticated: isAuthenticated,
Expand Down Expand Up @@ -554,6 +654,9 @@ export default function App() {
onLeaveWorkspace={() => handleLeaveWorkspace(activeWorkspaceId)}
/>
)}
{activeTab === "sandboxes" && (
<SandboxesTab workspaceId={convexWorkspaceId} />
)}
{activeTab === "resources" && (
<div className="h-full overflow-hidden">
<ResourcesTab
Expand Down Expand Up @@ -647,14 +750,15 @@ export default function App() {
<Toaster />
<HostedShellGate
state={
isSharedChatRoute
? sharedHostedShellGateState
: hostedShellGateState
isHostedChatRoute ? hostedChatShellGateState : hostedShellGateState
}
onSignIn={() => {
if (sharedPathToken) {
writeSharedSignInReturnPath(window.location.pathname);
}
if (sandboxPathToken) {
writeSandboxSignInReturnPath(window.location.pathname);
}
signIn();
}}
>
Expand All @@ -663,6 +767,11 @@ export default function App() {
pathToken={sharedPathToken}
onExitSharedChat={() => setExitedSharedChat(true)}
/>
) : isSandboxChatRoute ? (
<SandboxChatPage
pathToken={sandboxPathToken}
onExitSandboxChat={() => setExitedSandboxChat(true)}
/>
) : (
appContent
)}
Expand Down
15 changes: 15 additions & 0 deletions mcpjam-inspector/client/src/components/ChatTabV2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ interface ChatTabProps {
hostedSelectedServerIdsOverride?: string[];
hostedOAuthTokensOverride?: Record<string, string>;
hostedShareToken?: string;
hostedSandboxToken?: string;
initialModelId?: string;
initialSystemPrompt?: string;
initialTemperature?: number;
initialRequireToolApproval?: boolean;
onOAuthRequired?: (serverUrl?: string) => void;
}

Expand Down Expand Up @@ -82,6 +87,11 @@ export function ChatTabV2({
hostedSelectedServerIdsOverride,
hostedOAuthTokensOverride,
hostedShareToken,
hostedSandboxToken,
initialModelId,
initialSystemPrompt,
initialTemperature,
initialRequireToolApproval,
onOAuthRequired,
}: ChatTabProps) {
const { signUp } = useAuth();
Expand Down Expand Up @@ -195,6 +205,11 @@ export function ChatTabV2({
hostedSelectedServerIds: effectiveHostedSelectedServerIds,
hostedOAuthTokens: effectiveHostedOAuthTokens,
hostedShareToken,
hostedSandboxToken,
initialModelId,
initialSystemPrompt,
initialTemperature,
initialRequireToolApproval,
minimalMode,
onReset: () => {
setInput("");
Expand Down
Loading