import { router, useLocalSearchParams } from "expo-router";
import type { PluginIcon, PluginScreenParams, PluginScreenProps } from "@getpaseo/plugin/client";
import type { PluginTheme } from "@getpaseo/plugin";
import { X } from "lucide-react-native";
import { useCallback, useMemo, type ComponentType } from "react";
import { Text, View } from "react-native";
import { StyleSheet, withUnistyles } from "react-native-unistyles";
import { HeaderIconBadge } from "@/components/headers/header-icon-badge";
import { HeaderToggleButton } from "@/components/headers/header-toggle-button";
import { CompactHeader } from "@/components/headers/compact-header";
import { ScreenHeader } from "@/components/headers/screen-header";
import { ScreenTitle } from "@/components/headers/screen-title";
import { HostFilter } from "@/components/hosts/host-filter";
import type { HostPickerHost } from "@/components/hosts/host-picker";
import { LoadingSpinner } from "@/components/ui/loading-spinner";
import { useIsCompactFormFactor } from "@/constants/layout";
import {
useHostRuntimeClient,
useHostRuntimeConnectionStatus,
useHosts,
} from "@/runtime/host-runtime";
import type { Theme } from "@/styles/theme";
import type { ShortcutKey } from "@/utils/format-shortcut";
import { usePluginHostNavigation } from "./host-navigation";
import { PluginIconView } from "./icons";
import { toPluginTheme } from "./theme";
import { useInstalledPlugin, usePluginCatalogStatus, usePluginInstallations } from "./registry";
import { buildPluginSurfaceRoute, pluginScreenParamsFromRoute } from "./routes";
import {
legacySidebarItemHostKey,
pluginScreensHostKey,
rememberPluginContributionHost,
} from "./contribution-host";
import { SurfaceErrorBoundary } from "./surface-error-boundary";
import { PluginInstallationProvider } from "./installation-provider";
import {
getPluginSurfaceContributionServerIds,
resolvePluginScreenTitle,
resolvePluginSurfaceContribution,
resolvePluginSurfacePlaceholder,
type PluginSurfaceContributionIdentity,
type PluginSurfacePlaceholderStatus,
} from "./surface-contribution";
import { resolvePluginPlatform } from "./platform";
const EMPTY_SHORTCUT_KEYS: ShortcutKey[] = [];
const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
const pluginThemeMapping = (theme: Theme) => ({
theme: toPluginTheme(theme),
});
const ThemedX = withUnistyles(X);
const ThemedLoadingSpinner = withUnistyles(LoadingSpinner);
const SURFACE_ERRORS = {
offline: "Plugin host is offline.",
error: "Could not load plugins. Reconnect to try again.",
unavailable: "This plugin surface is unavailable.",
} as const;
function PluginSurfacePlaceholder({ status }: { status: PluginSurfacePlaceholderStatus }) {
if (status === "loading") {
return (
);
}
return {SURFACE_ERRORS[status]};
}
function routeParam(value: string | string[] | undefined): string {
return typeof value === "string" ? value : "";
}
function PluginHeaderIcon({ icon, color = "" }: { icon: PluginIcon; color?: string }) {
return ;
}
const ThemedPluginHeaderIcon = withUnistyles(PluginHeaderIcon);
function SurfaceRenderer({
Surface,
plugin,
layout,
host,
params,
theme,
}: {
Surface: ComponentType;
plugin: NonNullable>;
layout: PluginScreenProps["layout"];
host: PluginScreenProps["host"];
params: PluginScreenParams;
theme: PluginTheme;
}) {
const navigation = usePluginHostNavigation(host.id);
return (
);
}
const ThemedSurfaceRenderer = withUnistyles(SurfaceRenderer);
function PluginHostFilter({
serverId,
pluginId,
identity,
params,
hosts,
}: {
serverId: string;
pluginId: string;
identity: PluginSurfaceContributionIdentity;
params: PluginScreenParams;
hosts: HostPickerHost[];
}) {
const selectHost = useCallback(
(nextServerId: string) => {
// A legacy row remembers its own host; a screen's host carries to all the plugin's items.
rememberPluginContributionHost(
identity.kind === "sidebar"
? legacySidebarItemHostKey(pluginId, identity.id)
: pluginScreensHostKey(pluginId),
nextServerId,
);
router.replace(buildPluginSurfaceRoute(nextServerId, pluginId, identity, params));
},
[identity, params, pluginId],
);
return (
);
}
export function PluginSurfaceScreen() {
const routeParams = useLocalSearchParams<{
serverId?: string | string[];
pluginId?: string | string[];
contributionKind?: string | string[];
contributionId?: string | string[];
}>();
const serverId = routeParam(routeParams.serverId);
const pluginId = routeParam(routeParams.pluginId);
const contributionKind = routeParam(routeParams.contributionKind);
const contributionId = routeParam(routeParams.contributionId);
const paramsKey = JSON.stringify(pluginScreenParamsFromRoute(routeParams));
// Keyed by content: the route hands back a new object on every render.
const params = useMemo(() => JSON.parse(paramsKey), [paramsKey]);
const identity = useMemo(() => {
if (contributionKind !== "sidebar" && contributionKind !== "surface") return null;
return { kind: contributionKind, id: contributionId };
}, [contributionId, contributionKind]);
const plugin = useInstalledPlugin(serverId, pluginId);
const installations = usePluginInstallations(pluginId);
const hosts = useHosts();
const client = useHostRuntimeClient(serverId);
const connectionStatus = useHostRuntimeConnectionStatus(serverId);
const catalogStatus = usePluginCatalogStatus(serverId);
const placeholderStatus = resolvePluginSurfacePlaceholder({ connectionStatus, catalogStatus });
const compact = useIsCompactFormFactor();
const { sidebarItem, surface } = useMemo(
() => resolvePluginSurfaceContribution(plugin, identity),
[identity, plugin],
);
const hostLabel = hosts.find((host) => host.serverId === serverId)?.label ?? serverId;
// The hosts this contribution is installed on; the filter shows when there is a choice.
const filterHosts = useMemo(() => {
if (!identity) return [];
const serverIds = getPluginSurfaceContributionServerIds(installations, pluginId, identity);
return hosts.filter((candidate) => serverIds.includes(candidate.serverId));
}, [hosts, identity, installations, pluginId]);
const title = useMemo(() => {
if (surface && plugin)
return resolvePluginScreenTitle(surface, sidebarItem, params, (text) =>
plugin.i18n.resolve(text, plugin.locale),
);
if (sidebarItem && plugin) return plugin.i18n.resolve(sidebarItem.title, plugin.locale);
return pluginId || "Plugin";
}, [params, plugin, pluginId, sidebarItem, surface]);
const icon = sidebarItem?.icon ?? null;
const close = useCallback(() => {
if (router.canGoBack()) router.back();
else router.replace(`/h/${encodeURIComponent(serverId)}`);
}, [serverId]);
const layout = useMemo(() => ({ compact, platform: resolvePluginPlatform() }), [compact]);
const host = useMemo(() => ({ id: serverId, label: hostLabel }), [hostLabel, serverId]);
const headerLeft = useMemo(
() => (
<>
{icon ? (
) : null}
{title}
>
),
[icon, title],
);
const hostFilter = useMemo(
() =>
identity && filterHosts.length > 1 ? (
) : null,
[filterHosts, identity, params, pluginId, serverId],
);
const headerRight = useMemo(
() => (
<>
{hostFilter}
>
),
[close, hostFilter],
);
return (
{compact ? (
) : (
)}
{plugin && surface && client ? (
) : (
)}
);
}
const styles = StyleSheet.create((theme) => ({
screen: {
flex: 1,
backgroundColor: theme.colors.surface0,
},
body: {
flex: 1,
},
loading: {
flex: 1,
alignItems: "center",
justifyContent: "center",
},
errorText: {
color: theme.colors.statusDanger,
padding: theme.spacing[4],
},
}));