| @@ -163,17 +163,6 @@ const toolkitUrlFor = (orgSlug: string | undefined, slug: string): string => { · 162 unmodified lines |
| 163 | 163 | |
| 164 | 164 | const identityPattern = (displayPattern: string): string => displayPattern; |
| 165 | 165 | |
| 166 | | −const formatUpdatedAt = (value: number): string => |
| 167 | | − new Intl.DateTimeFormat(undefined, { |
| 168 | | − month: "short", |
| 169 | | − day: "numeric", |
| 170 | | − hour: "numeric", |
| 171 | | − minute: "2-digit", |
| 172 | | − }).format(new Date(value)); |
| 173 | | − |
| 174 | | −const toolkitScopeLabel = (toolkit: ToolkitResponse): string => |
| 175 | | − toolkit.owner === "org" ? "Workspace tools" : "Workspace + personal tools"; |
| 176 | | − |
| 177 | 166 | const compareToolkitRows = (a: ToolkitResponse, b: ToolkitResponse): number => { |
| 178 | 167 | if (a.updatedAt !== b.updatedAt) return b.updatedAt - a.updatedAt; |
| 179 | 168 | return a.name.localeCompare(b.name); |
| @@ -192,10 +181,7 @@ const toolkitShelfStyle = { minHeight: "28.5rem" }; · 12 unmodified lines |
| 192 | 181 | const toolkitGridContainerStyle = { maxWidth: "80rem" }; |
| 193 | 182 | const toolkitToolTreeStyle = { width: "24rem" }; |
| 194 | 183 | |
| 195 | | −const ownerAccentClass = (owner: Owner): string => |
| 196 | | − owner === "org" |
| 197 | | − ? "border-sky-500/25 bg-sky-500/10 text-sky-700 dark:text-sky-300" |
| 198 | | − : "border-emerald-500/25 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300"; |
| 184 | +const neutralToolkitIconClass = "border-border/70 bg-muted/30 text-muted-foreground"; |
| 199 | 185 | |
| 200 | 186 | type ToolkitConnectionGroup = { |
| 201 | 187 | readonly id: string; |
| @@ -409,9 +395,77 @@ const configuredConnectionPatterns = ( · 207 unmodified lines |
| 409 | 395 | ...policies.filter((policy) => legacyPolicyIds.has(policy.id)).map((policy) => policy.pattern), |
| 410 | 396 | ]); |
| 411 | 397 | |
| 398 | +function ToolkitConnectionIconStack(props: { connections: readonly ConfiguredConnectionView[] }) { |
| 399 | + const visibleConnections = props.connections.slice(0, 3); |
| 400 | + if (visibleConnections.length === 0) { |
| 401 | + return ( |
| 402 | + <div |
| 403 | + className={cn( |
| 404 | + "flex size-9 shrink-0 items-center justify-center rounded-md border", |
| 405 | + neutralToolkitIconClass, |
| 406 | + )} |
| 407 | + > |
| 408 | + <BoxIcon className="size-4" /> |
| 409 | + </div> |
| 410 | + ); |
| 411 | + } |
| 412 | + |
| 413 | + return ( |
| 414 | + <div className="flex size-9 shrink-0 items-center justify-center rounded-md border border-border/70 bg-muted/20"> |
| 415 | + <div className="flex -space-x-2"> |
| 416 | + {visibleConnections.map((connection) => ( |
| 417 | + <span |
| 418 | + key={connection.id} |
| 419 | + className="flex size-5 items-center justify-center rounded-sm border border-background bg-card shadow-xs" |
| 420 | + > |
| 421 | + <IntegrationFavicon |
| 422 | + icon={connection.icon} |
| 423 | + sourceId={connection.sourceId} |
| 424 | + url={connection.url} |
| 425 | + size={14} |
| 426 | + /> |
| 427 | + </span> |
| 428 | + ))} |
| 429 | + </div> |
| 430 | + </div> |
| 431 | + ); |
| 432 | +} |
| 433 | + |
| 434 | +const connectionCountLabel = (count: number): string => |
| 435 | + count === 0 ? "No connections" : `${count} ${count === 1 ? "connection" : "connections"}`; |
| 436 | + |
| 412 | 437 | function ToolkitTile(props: { pluginId: string; toolkit: ToolkitResponse }) { |
| 413 | 438 | const toolkit = props.toolkit; |
| 414 | | − const scope = toolkitScopeLabel(toolkit); |
| 439 | + const tools = useAtomValue(toolsAllAtom); |
| 440 | + const integrations = useAtomValue(integrationsOptimisticAtom); |
| 441 | + const connections = useAtomValue(toolkitConnectionsAtom(toolkit.id)); |
| 442 | + const integrationPlugins = useIntegrationPlugins(); |
| 443 | + const visibleTools = useMemo( |
| 444 | + () => |
| 445 | + AsyncResult.isSuccess(tools) |
| 446 | + ? (tools.value as readonly ToolRow[]).filter((tool) => |
| 447 | + toolCanAppearInToolkit(toolkit, tool), |
| 448 | + ) |
| 449 | + : [], |
| 450 | + [toolkit, tools], |
| 451 | + ); |
| 452 | + const connectionGroups = useMemo(() => buildConnectionGroups(visibleTools), [visibleTools]); |
| 453 | + const connectionRows = AsyncResult.isSuccess(connections) ? connections.value.connections : []; |
| 454 | + const integrationRows = AsyncResult.isSuccess(integrations) |
| 455 | + ? (integrations.value as readonly Integration[]) |
| 456 | + : []; |
| 457 | + const tileDataReady = |
| 458 | + AsyncResult.isSuccess(connections) && |
| 459 | + AsyncResult.isSuccess(tools) && |
| 460 | + AsyncResult.isSuccess(integrations); |
| 461 | + const configuredConnections = tileDataReady |
| 462 | + ? configuredConnectionViews( |
| 463 | + connectionRows, |
| 464 | + connectionGroups, |
| 465 | + integrationRows, |
| 466 | + integrationPlugins, |
| 467 | + ) |
| 468 | + : []; |
| 415 | 469 | return ( |
| 416 | 470 | <Link |
| 417 | 471 | to="/{-$orgSlug}/plugins/$pluginId/$" |
| @@ -421,31 +475,18 @@ function ToolkitTile(props: { pluginId: string; toolkit: ToolkitResponse }) { · 3 unmodified lines |
| 421 | 475 | style={toolkitCardStyle} |
| 422 | 476 | > |
| 423 | 477 | <div className="flex min-w-0 items-start gap-3"> |
| 424 | | − <div |
| 425 | | − className={cn( |
| 426 | | − "flex size-9 shrink-0 items-center justify-center rounded-md border", |
| 427 | | − ownerAccentClass(toolkit.owner), |
| 428 | | − )} |
| 429 | | − > |
| 430 | | − <BoxIcon className="size-4" /> |
| 431 | | − </div> |
| 478 | + {tileDataReady ? ( |
| 479 | + <ToolkitConnectionIconStack connections={configuredConnections} /> |
| 480 | + ) : ( |
| 481 | + <Skeleton className="size-9 shrink-0 rounded-md" /> |
| 482 | + )} |
| 432 | 483 | <div className="min-w-0 flex-1"> |
| 433 | 484 | <div className="truncate text-sm font-semibold text-foreground">{toolkit.name}</div> |
| 434 | | − <div className="mt-1 truncate font-mono text-[11px] text-muted-foreground"> |
| 435 | | − {toolkit.slug} |
| 485 | + <div className="mt-1 truncate text-xs text-muted-foreground"> |
| 486 | + {tileDataReady ? connectionCountLabel(configuredConnections.length) : "Loading"} |
| 436 | 487 | </div> |
| 437 | 488 | </div> |
| 438 | 489 | </div> |
| 439 | | − |
| 440 | | − <div className="mt-5 min-w-0 space-y-3"> |
| 441 | | − <code className="block truncate rounded-md border border-border/60 bg-muted/30 px-2 py-1.5 font-mono text-[11px] text-muted-foreground transition-colors group-hover:border-border"> |
| 442 | | − /mcp/toolkits/{toolkit.slug} |
| 443 | | − </code> |
| 444 | | − <div className="flex min-w-0 items-center justify-between gap-3 text-xs text-muted-foreground"> |
| 445 | | − <span className="truncate">{scope}</span> |
| 446 | | − <span className="shrink-0 tabular-nums">{formatUpdatedAt(toolkit.updatedAt)}</span> |
| 447 | | − </div> |
| 448 | | − </div> |
| 449 | 490 | </Link> |
| 450 | 491 | ); |
| 451 | 492 | } |
| @@ -541,7 +582,7 @@ function AddToolkitCard(props: { owner: Owner; onClick: () => void }) { · 89 unmodified lines |
| 541 | 582 | <span |
| 542 | 583 | className={cn( |
| 543 | 584 | "flex size-12 items-center justify-center rounded-md border transition-[border-color,background-color,color,transform]", |
| 544 | | − ownerAccentClass(props.owner), |
| 585 | + neutralToolkitIconClass, |
| 545 | 586 | "group-hover:scale-105", |
| 546 | 587 | )} |
| 547 | 588 | > |
| @@ -1117,23 +1158,14 @@ function ToolkitWorkspace(props: { · 569 unmodified lines |
| 1117 | 1158 | function ToolkitTileSkeleton(props: { index: number }) { |
| 1118 | 1159 | return ( |
| 1119 | 1160 | <div |
| 1120 | | − className="flex min-h-36 min-w-0 flex-col justify-between rounded-md border border-border/60 bg-card/35 p-3" |
| 1161 | + className="flex min-h-36 min-w-0 items-start gap-3 rounded-md border border-border/60 bg-card/35 p-3.5" |
| 1121 | 1162 | style={toolkitCardStyle} |
| 1122 | 1163 | aria-hidden="true" |
| 1123 | 1164 | > |
| 1124 | | − <div className="flex min-w-0 items-start gap-3"> |
| 1125 | | − <Skeleton className="size-9 shrink-0 rounded-md" /> |
| 1126 | | − <div className="min-w-0 flex-1 space-y-2 pt-0.5"> |
| 1127 | | − <Skeleton className="h-4" style={{ width: `${52 + ((props.index * 17) % 28)}%` }} /> |
| 1128 | | − <Skeleton className="h-3" style={{ width: `${34 + ((props.index * 11) % 24)}%` }} /> |
| 1129 | | − </div> |
| 1130 | | − </div> |
| 1131 | | − <div className="space-y-2"> |
| 1132 | | − <Skeleton className="h-3 w-24" /> |
| 1133 | | − <div className="flex min-w-0 items-center gap-1.5"> |
| 1134 | | − <Skeleton className="h-5 w-14 rounded-full" /> |
| 1135 | | − <Skeleton className="h-5 w-20 rounded-full" /> |
| 1136 | | − </div> |
| 1165 | + <Skeleton className="size-9 shrink-0 rounded-md" /> |
| 1166 | + <div className="min-w-0 flex-1 space-y-2 pt-0.5"> |
| 1167 | + <Skeleton className="h-4" style={{ width: `${52 + ((props.index * 17) % 28)}%` }} /> |
| 1168 | + <Skeleton className="h-3" style={{ width: `${34 + ((props.index * 11) % 24)}%` }} /> |
| 1137 | 1169 | </div> |
| 1138 | 1170 | </div> |
| 1139 | 1171 | ); |
| @@ -1369,7 +1401,13 @@ export function ToolkitsPage(props: PluginPageProps) { · 229 unmodified lines |
| 1369 | 1401 | </div> |
| 1370 | 1402 | ) : null} |
| 1371 | 1403 | |
| 1372 | | − {!toolkitsReady ? ( |
| 1404 | + {selectedToolkitSlug !== null && !toolkitsReady ? ( |
| 1405 | + toolkitsFailed ? ( |
| 1406 | + <div className="p-6 text-sm text-destructive">Failed to load toolkit</div> |
| 1407 | + ) : ( |
| 1408 | + <ToolkitDetailSkeleton /> |
| 1409 | + ) |
| 1410 | + ) : !toolkitsReady ? ( |
| 1373 | 1411 | toolkitsFailed ? ( |
| 1374 | 1412 | <div className="p-6 text-sm text-destructive">Failed to load toolkits</div> |
| 1375 | 1413 | ) : ( |