Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
59 changes: 52 additions & 7 deletions apps/web/src/components/mcp-server-status.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useAuth } from "@clerk/tanstack-react-start";
import { convexQuery } from "@convex-dev/react-query";
import { api } from "@harness/convex-backend/convex/_generated/api";
import { useQuery } from "@tanstack/react-query";
import { AlertTriangle, Server, Shield } from "lucide-react";
import { AlertTriangle, GraduationCap, Server, Shield } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { useCallback, useEffect, useRef, useState } from "react";
import toast from "react-hot-toast";
Expand Down Expand Up @@ -98,7 +98,12 @@ export type HealthStatus =
| "unreachable"
| "auth_required";

type ServerStatus = "connected" | "expired" | "disconnected" | "checking";
type ServerStatus =
| "connected"
| "expired"
| "disconnected"
| "checking"
| "needs_verification";

function getServerStatus(
server: McpServer,
Expand Down Expand Up @@ -126,7 +131,15 @@ function getServerStatus(
return "connected";
}

// For non-OAuth servers: use health check result
// For Princeton servers: auth_required means no verified netid on the account
if (server.authType === "tiger_junction") {
if (healthStatus === "auth_required") return "needs_verification";
if (healthStatus === "unreachable") return "disconnected";
if (healthStatus === "reachable") return "connected";
return "checking";
}

// For bearer / none servers: use health check result
if (healthStatus === "unreachable") return "disconnected";
if (healthStatus === "auth_required") return "disconnected";
if (healthStatus === "reachable") return "connected";
Expand All @@ -139,13 +152,15 @@ const STATUS_DOT: Record<ServerStatus, string> = {
expired: "bg-amber-400",
disconnected: "bg-red-400",
checking: "bg-muted-foreground/40",
needs_verification: "bg-amber-400",
};

const STATUS_LABEL: Record<ServerStatus, string> = {
connected: "Connected",
expired: "Token expired",
disconnected: "Unreachable",
checking: "Checking…",
needs_verification: "Verify Princeton account",
};

export function McpServerStatus({
Expand Down Expand Up @@ -186,7 +201,10 @@ export function McpServerStatus({

const allConnected = statuses.every((s) => s.status === "connected");
const hasIssue = statuses.some(
(s) => s.status === "expired" || s.status === "disconnected",
(s) =>
s.status === "expired" ||
s.status === "disconnected" ||
s.status === "needs_verification",
);

const anyChecking = statuses.some((s) => s.status === "checking");
Expand Down Expand Up @@ -315,17 +333,44 @@ function McpServerRow({
Reconnect
</Button>
)}
{server.authType !== "oauth" && status === "connected" && (
{status === "connected" && server.authType === "bearer" && (
<Badge variant="secondary" className="shrink-0 text-[9px]">
{server.authType === "bearer" ? "Key" : "Open"}
Key
</Badge>
)}
{server.authType === "oauth" && status === "connected" && (
{status === "connected" && server.authType === "none" && (
<Badge variant="secondary" className="shrink-0 text-[9px]">
Public
</Badge>
)}
{status === "connected" && server.authType === "tiger_junction" && (
<Badge variant="secondary" className="shrink-0 gap-1 text-[9px]">
<GraduationCap size={8} />
Princeton
</Badge>
)}
{status === "connected" && server.authType === "oauth" && (
<Badge variant="secondary" className="shrink-0 gap-1 text-[9px]">
<div className="h-1 w-1 rounded-full bg-emerald-500" />
OAuth
</Badge>
)}
{status === "needs_verification" && (
<Tooltip>
<TooltipTrigger asChild>
<Badge
variant="secondary"
className="shrink-0 gap-1 border border-amber-400/40 bg-amber-500/10 text-[9px] text-amber-700 dark:text-amber-400"
>
<GraduationCap size={8} />
Verify
</Badge>
</TooltipTrigger>
<TooltipContent>
Open this harness's settings to verify your Princeton account.
</TooltipContent>
</Tooltip>
)}
</div>
);
}
Expand Down
73 changes: 73 additions & 0 deletions apps/web/src/components/pending-response-indicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { AnimatePresence, motion } from "motion/react";
import { useEffect, useState } from "react";
import { cn } from "@/lib/utils";
import { RoseCurveSpinner } from "./rose-curve-spinner";

const FLAVOR_MESSAGES = [
"Thinking…",
"Warming up…",
"Consulting the tools…",
"Wiring up neurons…",
"Querying the oracle…",
"Reticulating splines…",
"Gathering thoughts…",
"Brewing a response…",
"Summoning tokens…",
"Sharpening the quill…",
"Reading the room…",
"Chasing down sources…",
"Following the thread…",
"Picking the right words…",
"Double-checking the math…",
"Aligning the stars…",
];

const ROTATE_MS = 2600;

function pickInitial(): number {
return Math.floor(Math.random() * FLAVOR_MESSAGES.length);
}

export function PendingResponseIndicator({
className,
}: {
className?: string;
}) {
const [idx, setIdx] = useState(pickInitial);

useEffect(() => {
const prefersReduced =
window?.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;
if (prefersReduced) return;
const id = setInterval(() => {
setIdx((prev) => (prev + 1) % FLAVOR_MESSAGES.length);
}, ROTATE_MS);
return () => clearInterval(id);
}, []);

const message = FLAVOR_MESSAGES[idx];

return (
<div
className={cn(
"flex items-center gap-2 text-muted-foreground",
className,
)}
aria-live="polite"
>
<RoseCurveSpinner size={14} />
<AnimatePresence mode="wait">
<motion.span
key={idx}
initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -4 }}
transition={{ duration: 0.22 }}
className="text-xs italic"
>
{message}
</motion.span>
</AnimatePresence>
</div>
);
}
21 changes: 21 additions & 0 deletions apps/web/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.

import { Route as rootRouteImport } from './routes/__root'
import { Route as SignUpRouteImport } from './routes/sign-up'
import { Route as SignInRouteImport } from './routes/sign-in'
import { Route as OnboardingRouteImport } from './routes/onboarding'
import { Route as AppRouteImport } from './routes/app'
Expand All @@ -21,6 +22,11 @@ import { Route as SandboxesCreate_sandboxRouteImport } from './routes/sandboxes/
import { Route as SandboxesSandboxIdRouteImport } from './routes/sandboxes/$sandboxId'
import { Route as HarnessesHarnessIdRouteImport } from './routes/harnesses/$harnessId'

const SignUpRoute = SignUpRouteImport.update({
id: '/sign-up',
path: '/sign-up',
getParentRoute: () => rootRouteImport,
} as any)
const SignInRoute = SignInRouteImport.update({
id: '/sign-in',
path: '/sign-in',
Expand Down Expand Up @@ -82,6 +88,7 @@ export interface FileRoutesByFullPath {
'/app': typeof AppRoute
'/onboarding': typeof OnboardingRoute
'/sign-in': typeof SignInRoute
'/sign-up': typeof SignUpRoute
'/harnesses/$harnessId': typeof HarnessesHarnessIdRoute
'/sandboxes/$sandboxId': typeof SandboxesSandboxIdRoute
'/sandboxes/create_sandbox': typeof SandboxesCreate_sandboxRoute
Expand All @@ -95,6 +102,7 @@ export interface FileRoutesByTo {
'/app': typeof AppRoute
'/onboarding': typeof OnboardingRoute
'/sign-in': typeof SignInRoute
'/sign-up': typeof SignUpRoute
'/harnesses/$harnessId': typeof HarnessesHarnessIdRoute
'/sandboxes/$sandboxId': typeof SandboxesSandboxIdRoute
'/sandboxes/create_sandbox': typeof SandboxesCreate_sandboxRoute
Expand All @@ -109,6 +117,7 @@ export interface FileRoutesById {
'/app': typeof AppRoute
'/onboarding': typeof OnboardingRoute
'/sign-in': typeof SignInRoute
'/sign-up': typeof SignUpRoute
'/harnesses/$harnessId': typeof HarnessesHarnessIdRoute
'/sandboxes/$sandboxId': typeof SandboxesSandboxIdRoute
'/sandboxes/create_sandbox': typeof SandboxesCreate_sandboxRoute
Expand All @@ -124,6 +133,7 @@ export interface FileRouteTypes {
| '/app'
| '/onboarding'
| '/sign-in'
| '/sign-up'
| '/harnesses/$harnessId'
| '/sandboxes/$sandboxId'
| '/sandboxes/create_sandbox'
Expand All @@ -137,6 +147,7 @@ export interface FileRouteTypes {
| '/app'
| '/onboarding'
| '/sign-in'
| '/sign-up'
| '/harnesses/$harnessId'
| '/sandboxes/$sandboxId'
| '/sandboxes/create_sandbox'
Expand All @@ -150,6 +161,7 @@ export interface FileRouteTypes {
| '/app'
| '/onboarding'
| '/sign-in'
| '/sign-up'
| '/harnesses/$harnessId'
| '/sandboxes/$sandboxId'
| '/sandboxes/create_sandbox'
Expand All @@ -164,6 +176,7 @@ export interface RootRouteChildren {
AppRoute: typeof AppRoute
OnboardingRoute: typeof OnboardingRoute
SignInRoute: typeof SignInRoute
SignUpRoute: typeof SignUpRoute
HarnessesHarnessIdRoute: typeof HarnessesHarnessIdRoute
SandboxesSandboxIdRoute: typeof SandboxesSandboxIdRoute
SandboxesCreate_sandboxRoute: typeof SandboxesCreate_sandboxRoute
Expand All @@ -175,6 +188,13 @@ export interface RootRouteChildren {

declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/sign-up': {
id: '/sign-up'
path: '/sign-up'
fullPath: '/sign-up'
preLoaderRoute: typeof SignUpRouteImport
parentRoute: typeof rootRouteImport
}
'/sign-in': {
id: '/sign-in'
path: '/sign-in'
Expand Down Expand Up @@ -260,6 +280,7 @@ const rootRouteChildren: RootRouteChildren = {
AppRoute: AppRoute,
OnboardingRoute: OnboardingRoute,
SignInRoute: SignInRoute,
SignUpRoute: SignUpRoute,
HarnessesHarnessIdRoute: HarnessesHarnessIdRoute,
SandboxesSandboxIdRoute: SandboxesSandboxIdRoute,
SandboxesCreate_sandboxRoute: SandboxesCreate_sandboxRoute,
Expand Down
Loading
Loading