diff --git a/frontend/src/components/agents/AgentsHub.tsx b/frontend/src/components/agents/AgentsHub.tsx index ccf1654e..fefaff29 100644 --- a/frontend/src/components/agents/AgentsHub.tsx +++ b/frontend/src/components/agents/AgentsHub.tsx @@ -173,6 +173,10 @@ const AgentsHub = ({ currentPodId: propPodId = null }) => { const params = new URLSearchParams(location.search); return params.get('tab') || ''; }, [location.search]); + const queryInstallable = useMemo(() => { + const params = new URLSearchParams(location.search); + return params.get('installable') || ''; + }, [location.search]); const [selectedPodId, setSelectedPodId] = useState(propPodId || queryPodId); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -258,6 +262,7 @@ const AgentsHub = ({ currentPodId: propPodId = null }) => { const runtimeLogsInputRef = useRef(null); const runtimeLogsScrollRef = useRef({ top: 0, atBottom: true }); const deepLinkHandledRef = useRef(''); + const installableDeepLinkHandledRef = useRef(''); const [userTokenValue, setUserTokenValue] = useState(''); const [userTokenScopes, setUserTokenScopes] = useState([]); const [userTokenMeta, setUserTokenMeta] = useState({ @@ -478,6 +483,25 @@ const AgentsHub = ({ currentPodId: propPodId = null }) => { } }, [queryAgentName, queryInstanceId, queryView, selectedPodId, installedAgents]); + // Deep-link from the marketplace detail page: /v2/agents/browse?installable= + // opens the install dialog pre-targeted to that listing. The marketplace keys + // on installableId; our catalog (/api/registry/agents) keys on name — match on + // either (plus id/displayName) and no-op if the catalog doesn't carry it, so a + // miss just leaves the user on the browse list (no regression). + useEffect(() => { + if (!queryInstallable || !agents.length) return; + if (installableDeepLinkHandledRef.current === queryInstallable) return; + const target = String(queryInstallable).trim().toLowerCase(); + const matched = agents.find((a) => ( + [a.installableId, a.name, a.id, a._id, a.displayName] + .some((v) => String(v || '').trim().toLowerCase() === target) + )); + if (!matched) return; + installableDeepLinkHandledRef.current = queryInstallable; + setActiveTab(TAB_DISCOVER); + openInstallDialog(matched); + }, [queryInstallable, agents]); + const fetchAgents = async () => { setLoading(true); setError(null); diff --git a/frontend/src/components/apps/AppCard.tsx b/frontend/src/components/apps/AppCard.tsx index fcda0f21..404f7a91 100644 --- a/frontend/src/components/apps/AppCard.tsx +++ b/frontend/src/components/apps/AppCard.tsx @@ -31,6 +31,7 @@ import { SvgIconComponent } from '@mui/icons-material'; export interface AppCardApp { id?: string; _id?: string; + installableId?: string; name?: string; displayName?: string; description?: string; @@ -50,6 +51,7 @@ interface AppCardProps { installed?: boolean; onInstall?: (app: AppCardApp) => void; onRemove?: (app: AppCardApp) => void; + onViewDetail?: (app: AppCardApp) => void; loading?: boolean; showScopes?: boolean; } @@ -73,6 +75,7 @@ const AppCard: React.FC = ({ installed = false, onInstall, onRemove, + onViewDetail, loading = false, showScopes = false, }) => { @@ -143,7 +146,13 @@ const AppCard: React.FC = ({ textTransform: 'capitalize', }} /> - + onViewDetail(app) : undefined} + role={onViewDetail ? 'button' : undefined} + tabIndex={onViewDetail ? 0 : undefined} + onKeyDown={onViewDetail ? (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onViewDetail(app); } } : undefined} + > { }); it('renders official marketplace listings', async () => { - render(); + render(); expect(await screen.findByText('Official Marketplace')).toBeInTheDocument(); expect(await screen.findByText('Discord')).toBeInTheDocument(); @@ -115,7 +116,7 @@ describe('AppsMarketplacePage', () => { }); it('renders installable browse results and installs via registry', async () => { - render(); + render(); expect((await screen.findAllByText('Community Agent')).length).toBeGreaterThan(0); expect(screen.getAllByText('@sam/community-agent').length).toBeGreaterThan(0); diff --git a/frontend/src/components/apps/AppsMarketplacePage.tsx b/frontend/src/components/apps/AppsMarketplacePage.tsx index 11232969..2abd20b3 100644 --- a/frontend/src/components/apps/AppsMarketplacePage.tsx +++ b/frontend/src/components/apps/AppsMarketplacePage.tsx @@ -5,6 +5,7 @@ */ import React, { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import { Box, Container, @@ -161,6 +162,14 @@ const toInstalledLegacyApp = (app: any): App => ({ const AppsMarketplacePage: React.FC = () => { const v2Embedded = useV2Embedded(); + const navigate = useNavigate(); + + // Open the v2-native manifest detail page. Only wired in the v2 shell — + // the detail route lives at /v2/marketplace/:id and has no v1 equivalent. + const handleViewDetail = (app: { installableId?: string; id?: string }) => { + const id = String(app?.installableId || app?.id || ''); + if (id) navigate(`/v2/marketplace/${encodeURIComponent(id)}`); + }; const theme = useTheme(); const [apps, setApps] = useState([]); const [featured, setFeatured] = useState([]); @@ -687,6 +696,7 @@ const AppsMarketplacePage: React.FC = () => { installed={isInstalled(app.id)} onInstall={handleInstall} onRemove={handleRemove} + onViewDetail={v2Embedded ? handleViewDetail : undefined} /> ))} @@ -806,6 +816,7 @@ const AppsMarketplacePage: React.FC = () => { installed={isInstalled(app.id)} onInstall={handleInstall} onRemove={handleRemove} + onViewDetail={v2Embedded ? handleViewDetail : undefined} /> ))}