diff --git a/apps/mobile/src/features/threads/ThreadFeed.tsx b/apps/mobile/src/features/threads/ThreadFeed.tsx index 58191639331a..221782f4cd26 100644 --- a/apps/mobile/src/features/threads/ThreadFeed.tsx +++ b/apps/mobile/src/features/threads/ThreadFeed.tsx @@ -1619,6 +1619,7 @@ function renderFeedEntry( environmentId={props.environmentId} projectedItem={entry.activities[0]!.projectedItem} iconColor={iconSubtleColor} + surfaceColor={props.screenColor} /> ); } diff --git a/apps/mobile/src/features/threads/thread-handoff-row.tsx b/apps/mobile/src/features/threads/thread-handoff-row.tsx index 3b1b2894a8fc..1af6c1ec0099 100644 --- a/apps/mobile/src/features/threads/thread-handoff-row.tsx +++ b/apps/mobile/src/features/threads/thread-handoff-row.tsx @@ -1,9 +1,14 @@ import { useAtomValue } from "@effect/atom-react"; import { scopeThreadRef } from "@t3tools/client-runtime/environment"; import { resolveHandoffEndpoints } from "@t3tools/client-runtime/handoff"; -import { resolveProviderInstanceDisplayName } from "@t3tools/client-runtime/state/provider-instance-display"; +import { + normalizeProviderAccentColor, + resolveProviderInstanceDisplayName, + shouldShowInstanceBadge, +} from "@t3tools/client-runtime/state/provider-instance-display"; import type { EnvironmentId, + ProviderInstanceConfig, OrchestrationV2ProjectedTurnItem, ProviderInstanceId, ServerProvider, @@ -13,7 +18,7 @@ import { Fragment, useMemo } from "react"; import { Alert, Pressable, View, type ColorValue } from "react-native"; import { AppText as Text } from "../../components/AppText"; import { SymbolView } from "../../components/AppSymbol"; -import { ProviderIcon } from "../../components/ProviderIcon"; +import { ProviderIcon, ProviderInstanceIcon } from "../../components/ProviderIcon"; import { environmentThreadDetails } from "../../state/threads"; import { serverEnvironment } from "../../state/server"; import { ThreadContextDivider } from "./thread-context-divider"; @@ -38,6 +43,7 @@ export function ThreadHandoffRow(props: { environmentId: EnvironmentId; projectedItem: OrchestrationV2ProjectedTurnItem; iconColor: ColorValue; + surfaceColor: string; }) { const { item } = props.projectedItem; const endpointsAtom = useMemo( @@ -48,6 +54,11 @@ export function ThreadHandoffRow(props: { const config = useAtomValue(serverEnvironment.configValueAtom(props.environmentId)); if (item.type !== "handoff" || endpoints === null) return null; const color = item.status === "failed" ? "#e11d48" : props.iconColor; + const endpointProps = { + providers: config?.providers ?? [], + providerInstances: config?.settings.providerInstances, + surfaceColor: props.surfaceColor, + }; return ( ) : null} - + ))} {endpoints.from.length > 0 ? ( ) : null} - + ); @@ -79,6 +90,8 @@ function HandoffEndpoint(props: { instanceId: ProviderInstanceId; model?: string | undefined; providers: ReadonlyArray; + providerInstances: Readonly> | undefined; + surfaceColor: string; }) { const provider = props.providers.find((candidate) => candidate.instanceId === props.instanceId); const model = provider?.models.find((candidate) => candidate.slug === props.model); @@ -87,12 +100,36 @@ function HandoffEndpoint(props: { model?.name ?? props.model ?? (provider ? resolveProviderInstanceDisplayName(provider) : props.instanceId); + const accentColor = normalizeProviderAccentColor(provider?.accentColor); + // Same account badge as the thread rows: a handoff between two accounts of + // one provider would otherwise show the same glyph on both sides. Settings + // carry each ACP instance's agent, which tells its glyph apart. + const badgeEntry = (candidate: ServerProvider) => { + const config = props.providerInstances?.[candidate.instanceId]?.config; + // A local ACP command has no registry agent, as on web. + const agentId = + typeof config === "object" && config && Reflect.get(config, "source") !== "local" + ? Reflect.get(config, "agentId") + : null; + return { + driverKind: candidate.driver, + ...(typeof agentId === "string" && agentId.trim() + ? { acpRegistryAgentId: agentId.trim() } + : {}), + }; + }; + const showBadge = + provider !== undefined && + shouldShowInstanceBadge( + { ...badgeEntry(provider), accentColor }, + props.providers.map(badgeEntry), + ); return ( Alert.alert( @@ -101,11 +138,23 @@ function HandoffEndpoint(props: { ) } > - + {showBadge && provider !== undefined ? ( + + ) : ( + + )} {label} diff --git a/apps/web/src/components/chat/V2LifecycleRow.tsx b/apps/web/src/components/chat/V2LifecycleRow.tsx index cdd469f7acb4..ee3d2add3b83 100644 --- a/apps/web/src/components/chat/V2LifecycleRow.tsx +++ b/apps/web/src/components/chat/V2LifecycleRow.tsx @@ -37,7 +37,12 @@ import { } from "lucide-react"; import { Tooltip, TooltipTrigger, TooltipPopup } from "../ui/tooltip"; -import { getProviderInstanceEntry } from "../../providerInstances"; +import { + applyProviderInstanceSettings, + deriveProviderInstanceEntries, + shouldShowInstanceBadge, +} from "../../providerInstances"; +import { useEnvironmentSettings } from "../../hooks/useSettings"; import { formatShortTimestamp } from "../../timestampFormat"; import { getTriggerDisplayModelName } from "./providerIconUtils"; import { ProviderInstanceIcon, providerTextColorClassName } from "./ProviderInstanceIcon"; @@ -137,6 +142,7 @@ export function V2LifecycleRow(props: { ) : null}