| @@ -163,17 +163,6 @@ const toolkitUrlFor = (orgSlug: string | undefined, slug: string): string => { · 162 unmodified lines |
| 163 | | 163 | |
| 164 | const identityPattern = (displayPattern: string): string => displayPattern; | 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 | const compareToolkitRows = (a: ToolkitResponse, b: ToolkitResponse): number => { | 166 | const compareToolkitRows = (a: ToolkitResponse, b: ToolkitResponse): number => { |
| 178 | if (a.updatedAt !== b.updatedAt) return b.updatedAt - a.updatedAt; | 167 | if (a.updatedAt !== b.updatedAt) return b.updatedAt - a.updatedAt; |
| 179 | return a.name.localeCompare(b.name); | 168 | return a.name.localeCompare(b.name); |
| @@ -192,10 +181,7 @@ const toolkitShelfStyle = { minHeight: "28.5rem" }; · 12 unmodified lines |
| 192 | const toolkitGridContainerStyle = { maxWidth: "80rem" }; | 181 | const toolkitGridContainerStyle = { maxWidth: "80rem" }; |
| 193 | const toolkitToolTreeStyle = { width: "24rem" }; | 182 | const toolkitToolTreeStyle = { width: "24rem" }; |
| 194 | | 183 | |
| 195 | const ownerAccentClass = (owner: Owner): string => | 184 | const neutralToolkitIconClass = "border-border/70 bg-muted/30 text-muted-foreground"; |
| 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"; | | |
| 199 | | 185 | |
| 200 | type ToolkitConnectionGroup = { | 186 | type ToolkitConnectionGroup = { |
| 201 | readonly id: string; | 187 | readonly id: string; |
| @@ -409,9 +395,77 @@ const configuredConnectionPatterns = ( · 207 unmodified lines |
| 409 | ...policies.filter((policy) => legacyPolicyIds.has(policy.id)).map((policy) => policy.pattern), | 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 | function ToolkitTile(props: { pluginId: string; toolkit: ToolkitResponse }) { | 437 | function ToolkitTile(props: { pluginId: string; toolkit: ToolkitResponse }) { |
| 413 | const toolkit = props.toolkit; | 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 | return ( | 469 | return ( |
| 416 | <Link | 470 | <Link |
| 417 | to="/{-$orgSlug}/plugins/$pluginId/$" | 471 | to="/{-$orgSlug}/plugins/$pluginId/$" |
| @@ -421,31 +475,18 @@ function ToolkitTile(props: { pluginId: string; toolkit: ToolkitResponse }) { · 3 unmodified lines |
| 421 | style={toolkitCardStyle} | 475 | style={toolkitCardStyle} |
| 422 | > | 476 | > |
| 423 | <div className="flex min-w-0 items-start gap-3"> | 477 | <div className="flex min-w-0 items-start gap-3"> |
| 424 | <div | 478 | {tileDataReady ? ( |
| 425 | className={cn( | 479 | <ToolkitConnectionIconStack connections={configuredConnections} /> |
| 426 | "flex size-9 shrink-0 items-center justify-center rounded-md border", | 480 | ) : ( |
| 427 | ownerAccentClass(toolkit.owner), | 481 | <Skeleton className="size-9 shrink-0 rounded-md" /> |
| 428 | )} | 482 | )} |
| 429 | > | | |
| 430 | <BoxIcon className="size-4" /> | | |
| 431 | </div> | | |
| 432 | <div className="min-w-0 flex-1"> | 483 | <div className="min-w-0 flex-1"> |
| 433 | <div className="truncate text-sm font-semibold text-foreground">{toolkit.name}</div> | 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"> | 485 | <div className="mt-1 truncate text-xs text-muted-foreground"> |
| 435 | {toolkit.slug} | 486 | {tileDataReady ? connectionCountLabel(configuredConnections.length) : "Loading"} |
| 436 | </div> | 487 | </div> |
| 437 | </div> | 488 | </div> |
| 438 | </div> | 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 | </Link> | 490 | </Link> |
| 450 | ); | 491 | ); |
| 451 | } | 492 | } |
| @@ -541,7 +582,7 @@ function AddToolkitCard(props: { owner: Owner; onClick: () => void }) { · 89 unmodified lines |
| 541 | <span | 582 | <span |
| 542 | className={cn( | 583 | className={cn( |
| 543 | "flex size-12 items-center justify-center rounded-md border transition-[border-color,background-color,color,transform]", | 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 | "group-hover:scale-105", | 586 | "group-hover:scale-105", |
| 546 | )} | 587 | )} |
| 547 | > | 588 | > |
| @@ -1117,23 +1158,14 @@ function ToolkitWorkspace(props: { · 569 unmodified lines |
| 1117 | function ToolkitTileSkeleton(props: { index: number }) { | 1158 | function ToolkitTileSkeleton(props: { index: number }) { |
| 1118 | return ( | 1159 | return ( |
| 1119 | <div | 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 | style={toolkitCardStyle} | 1162 | style={toolkitCardStyle} |
| 1122 | aria-hidden="true" | 1163 | aria-hidden="true" |
| 1123 | > | 1164 | > |
| 1124 | <div className="flex min-w-0 items-start gap-3"> | 1165 | <Skeleton className="size-9 shrink-0 rounded-md" /> |
| 1125 | <Skeleton className="size-9 shrink-0 rounded-md" /> | 1166 | <div className="min-w-0 flex-1 space-y-2 pt-0.5"> |
| 1126 | <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)}%` }} /> |
| 1127 | <Skeleton className="h-4" style={{ width: `${52 + ((props.index * 17) % 28)}%` }} /> | 1168 | <Skeleton className="h-3" style={{ width: `${34 + ((props.index * 11) % 24)}%` }} /> |
| 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> | | |
| 1137 | </div> | 1169 | </div> |
| 1138 | </div> | 1170 | </div> |
| 1139 | ); | 1171 | ); |
| @@ -1369,7 +1401,13 @@ export function ToolkitsPage(props: PluginPageProps) { · 229 unmodified lines |
| 1369 | </div> | 1401 | </div> |
| 1370 | ) : null} | 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 | toolkitsFailed ? ( | 1411 | toolkitsFailed ? ( |
| 1374 | <div className="p-6 text-sm text-destructive">Failed to load toolkits</div> | 1412 | <div className="p-6 text-sm text-destructive">Failed to load toolkits</div> |
| 1375 | ) : ( | 1413 | ) : ( |