import { useT } from "@agent-native/core/client/i18n"; import { filterOtherApps, type ConnectedAppSummary } from "../lib/other-apps"; import type { WorkspaceAppId } from "../lib/other-apps"; import { cn } from "../lib/utils"; import { isDefaultWorkspaceAppHiddenId } from "../lib/workspace-apps"; import { ActionQueryError } from "./action-query-error"; import { APP_LIST_GRID_CLASS, APP_LIST_GRID_ROW_CLASS, AppList, } from "./app-list-row"; import { ConnectedAppCard } from "./connected-app-card"; import { Skeleton } from "./ui/skeleton"; import { WorkspaceTemplateCard, type CuratedWorkspaceTemplate, type CuratedWorkspaceTemplatesResult, type WorkspaceTemplateLabels, } from "./workspace-template-card"; export type OtherAppEntry = | { kind: "template"; template: CuratedWorkspaceTemplate } | { kind: "connected"; app: ConnectedAppSummary }; function templateKey(template: CuratedWorkspaceTemplate): string { return (template.templateId || template.id || template.appId || template.name) .trim() .toLowerCase(); } function getTemplateItems( result: CuratedWorkspaceTemplatesResult | undefined, ): CuratedWorkspaceTemplate[] { if (!result) return []; return Array.isArray(result) ? result : result.templates; } export function otherAppEntryMatchesQuery( entry: OtherAppEntry, query: string, ): boolean { const normalizedQuery = query.trim().toLowerCase(); if (!normalizedQuery) return true; const searchable = entry.kind === "template" ? `${entry.template.name} ${entry.template.description ?? ""}` : `${entry.app.name} ${entry.app.description ?? ""}`; return searchable.toLowerCase().includes(normalizedQuery); } export function filterOtherAppEntries( entries: OtherAppEntry[], query: string, ): OtherAppEntry[] { return entries.filter((entry) => otherAppEntryMatchesQuery(entry, query)); } export function mergeOtherAppEntries({ templates, connectedApps, workspaceApps, }: { templates?: CuratedWorkspaceTemplatesResult; connectedApps: ConnectedAppSummary[]; workspaceApps: WorkspaceAppId[]; }): OtherAppEntry[] { const workspaceAppIds = new Set( workspaceApps.map((app) => app.id.trim().toLowerCase()), ); const seen = new Set(); const entries: OtherAppEntry[] = []; for (const template of getTemplateItems(templates)) { const id = templateKey(template); if ( !id || isDefaultWorkspaceAppHiddenId(id) || template.installed || workspaceAppIds.has(id) ) continue; seen.add(id); entries.push({ kind: "template", template }); } for (const app of filterOtherApps(connectedApps, workspaceApps)) { const id = app.id.trim().toLowerCase(); if (seen.has(id)) continue; seen.add(id); entries.push({ kind: "connected", app }); } return entries; } export function OtherAppsSection({ templates, connectedApps = [], workspaceApps, templatesLoading = false, connectedAppsLoading = false, templatesError, connectedAppsError, onRetryTemplates, onRetryConnectedApps, templateLabels, onRemixSuccess, query = "", heading = "Other apps", embeddedInList = false, className, }: { templates?: CuratedWorkspaceTemplatesResult; connectedApps?: ConnectedAppSummary[]; workspaceApps: WorkspaceAppId[]; templatesLoading?: boolean; connectedAppsLoading?: boolean; templatesError?: Error | null; connectedAppsError?: Error | null; onRetryTemplates?: () => void; onRetryConnectedApps?: () => void; templateLabels?: Partial; onRemixSuccess?: ( result: unknown, template: CuratedWorkspaceTemplate, ) => void; query?: string; heading?: string | null; embeddedInList?: boolean; className?: string; }) { const t = useT(); const entries = filterOtherAppEntries( mergeOtherAppEntries({ templates, connectedApps, workspaceApps, }), query, ); const isLoading = templatesLoading || connectedAppsLoading; const hasError = Boolean(templatesError || connectedAppsError); if (!isLoading && !hasError && entries.length === 0) return null; const entryCards = entries.map((entry) => entry.kind === "template" ? ( ) : ( ), ); const content = ( <> {heading ? (

{t("dispatch.pages.otherApps", { defaultValue: heading })}

) : null} {templatesError ? ( onRetryTemplates?.()} /> ) : null} {connectedAppsError ? ( onRetryConnectedApps?.()} /> ) : null} {isLoading && entries.length === 0 ? ( embeddedInList ? ( ) : ( ) ) : entries.length > 0 ? ( embeddedInList ? ( entryCards ) : ( {entryCards} ) ) : null} ); if (embeddedInList) return content; return
{content}
; } function OtherAppsSkeletonList() { return ( {Array.from({ length: 4 }).map((_, index) => (
))}
); } function OtherAppsSkeletonRows() { return ( <> {Array.from({ length: 4 }).map((_, index) => (
))} ); }