diff --git a/apps/mobile/src/components/ProviderIcon.tsx b/apps/mobile/src/components/ProviderIcon.tsx index 1fe46085d9e8..cd4574b6f84e 100644 --- a/apps/mobile/src/components/ProviderIcon.tsx +++ b/apps/mobile/src/components/ProviderIcon.tsx @@ -10,6 +10,7 @@ import { useId, useState } from "react"; import { resolveOfficialAcpRegistryIconUrl } from "@t3tools/contracts"; import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider"; import { AppText as Text } from "./ui/AppText"; +import { cn } from "../lib/cn"; import { getProviderClient } from "@t3tools/client-runtime/provider-clients"; type ProviderIconProps = { @@ -158,7 +159,7 @@ export function ProviderInstanceIcon(props: { readonly displayName: string; readonly accentColor?: string; readonly showBadge?: boolean; - readonly surfaceColor: string; + readonly surfaceColor?: string; }) { const accentColor = normalizeProviderAccentColor(props.accentColor); return ( @@ -173,14 +174,14 @@ export function ProviderInstanceIcon(props: { style={{ position: "absolute", right: -3, bottom: -3, width: 32, alignItems: "flex-end" }} > candidate.instanceId === props.item.providerInstanceId, + ); // A remote task's own driver is only a "remote" label. const driver = - props.provider?.driver ?? - (props.item.remoteChild === undefined ? props.item.driver : undefined); + provider?.driver ?? (props.item.remoteChild === undefined ? props.item.driver : undefined); + const badgeEntries = providers.map((candidate) => { + const rawConfig = props.config?.settings.providerInstances[candidate.instanceId]?.config; + const instanceConfig = + rawConfig !== null && typeof rawConfig === "object" + ? (rawConfig as Readonly>) + : null; + const agentId = instanceConfig?.source === "local" ? undefined : instanceConfig?.agentId; + return { + ...candidate, + driverKind: candidate.driver, + acpRegistryAgentId: typeof agentId === "string" ? agentId.trim() || undefined : undefined, + }; + }); + const badgeEntry = badgeEntries.find( + (candidate) => candidate.instanceId === props.item.providerInstanceId, + ); + const showBadge = + provider !== undefined && + badgeEntry !== undefined && + shouldShowInstanceBadge(badgeEntry, badgeEntries); return ( - {driver === undefined ? ( + {showBadge ? ( + + ) : driver === undefined ? ( ) : ( - + )} ); @@ -100,11 +136,8 @@ export function ThreadSubagentGroup(props: { const expanded = props.expandedRows[props.anchorKey] ?? false; const serverConfigs = useServerConfigs(); // A task in a linked environment runs a provider instance of that environment. - const providerOf = (item: SubagentItem) => - (item.remoteChild === undefined - ? config - : serverConfigs.get(item.remoteChild.environmentId) - )?.providers.find((provider) => provider.instanceId === item.providerInstanceId); + const configOf = (item: SubagentItem) => + item.remoteChild === undefined ? config : serverConfigs.get(item.remoteChild.environmentId); return ( {grouped ? ( @@ -118,7 +151,7 @@ export function ThreadSubagentGroup(props: { {agents.slice(0, 3).map((agent, index) => ( - + ))} {agents.length > 3 ? ( diff --git a/apps/web/src/components/chat/V2LifecycleRow.tsx b/apps/web/src/components/chat/V2LifecycleRow.tsx index 42ab22fa5f3d..688736a3a876 100644 --- a/apps/web/src/components/chat/V2LifecycleRow.tsx +++ b/apps/web/src/components/chat/V2LifecycleRow.tsx @@ -47,6 +47,7 @@ import { import { Tooltip, TooltipTrigger, TooltipPopup } from "../ui/tooltip"; import { applyProviderInstanceSettings, + brandProviderInstanceEntries, deriveProviderInstanceEntries, shouldShowInstanceBadge, type ProviderInstanceEntry, @@ -206,12 +207,16 @@ function SubagentAvatar({ driver, displayName, iconUrl, + accentColor, + showBadge, status, className, }: { driver?: ProviderDriverKind | undefined; displayName?: string | undefined; iconUrl?: string | undefined; + accentColor?: string | undefined; + showBadge: boolean; status?: OrchestrationV2TurnItem["status"] | undefined; className?: string; }) { @@ -227,7 +232,10 @@ function SubagentAvatar({ @@ -237,7 +245,8 @@ function SubagentAvatar({ {status ? ( @@ -261,9 +270,13 @@ export function EnvironmentSubagentAvatar({ remote?: OrchestrationV2RemoteTaskChild | undefined; status?: OrchestrationV2TurnItem["status"] | undefined; }) { - const providersAtom = serverEnvironment.providersValueAtom( - remote?.environmentId ?? environmentId, - ); + const providerEnvironmentId = remote?.environmentId ?? environmentId; + const providersAtom = serverEnvironment.providersValueAtom(providerEnvironmentId); + const settings = useAtomValue(serverEnvironment.settingsValueAtom(providerEnvironmentId)); + const entries = (providers: ReadonlyArray | null | undefined) => { + const derived = deriveProviderInstanceEntries(providers ?? []); + return settings ? brandProviderInstanceEntries(derived, settings) : derived; + }; const providerDriver = useAtomValue( providersAtom, (providers) => @@ -272,8 +285,18 @@ export function EnvironmentSubagentAvatar({ const displayName = useAtomValue( providersAtom, (providers) => - providers?.find((provider) => provider.instanceId === providerInstanceId)?.displayName, + entries(providers).find((entry) => entry.instanceId === providerInstanceId)?.displayName, + ); + const accentColor = useAtomValue( + providersAtom, + (providers) => + entries(providers).find((entry) => entry.instanceId === providerInstanceId)?.accentColor, ); + const showBadge = useAtomValue(providersAtom, (providers) => { + const all = entries(providers); + const entry = all.find((candidate) => candidate.instanceId === providerInstanceId); + return entry !== undefined && shouldShowInstanceBadge(entry, all); + }); const iconUrl = useAtomValue( providersAtom, (providers) => @@ -285,6 +308,8 @@ export function EnvironmentSubagentAvatar({ driver={providerDriver ?? (remote === undefined ? driver : undefined)} displayName={displayName} iconUrl={iconUrl} + accentColor={accentColor} + showBadge={showBadge} /> ); } diff --git a/apps/web/src/providerInstances.test.ts b/apps/web/src/providerInstances.test.ts index a07ff9ae9b17..891f3312ae83 100644 --- a/apps/web/src/providerInstances.test.ts +++ b/apps/web/src/providerInstances.test.ts @@ -8,6 +8,7 @@ import { import { describe, expect, it } from "vite-plus/test"; import { applyProviderInstanceSettings, + brandProviderInstanceEntries, deriveProviderEntriesByEnvironment, deriveProviderInstanceEntries, getDefaultProviderInstanceModel, @@ -254,6 +255,32 @@ describe("deriveProviderEntriesByEnvironment", () => { }); }); +describe("brandProviderInstanceEntries", () => { + it("keeps an instance a project turns on as a sibling for the instance badge", () => { + const codex = ProviderDriverKind.make("codex"); + const settings = { + providerInstances: { + [ProviderInstanceId.make("codex_work")]: { driver: codex, enabled: false }, + }, + projectSettingsOverrides: { + [ProjectId.make("project-work")]: { + providerInstances: { [ProviderInstanceId.make("codex_work")]: { enabled: true } }, + }, + }, + }; + const entries = brandProviderInstanceEntries( + deriveProviderInstanceEntries([ + provider({ provider: codex, instanceId: "codex" }), + provider({ provider: codex, instanceId: "codex_work" }), + ]), + settings, + ); + const personal = entries.find((entry) => entry.instanceId === "codex"); + + expect(personal && shouldShowInstanceBadge(personal, entries)).toBe(true); + }); +}); + describe("resolveSelectableProviderInstance", () => { it("falls back to the first enabled and available instance", () => { const disabled = ProviderInstanceId.make("codex"); diff --git a/apps/web/src/providerInstances.ts b/apps/web/src/providerInstances.ts index f9e711c40e7f..c2120f60e31c 100644 --- a/apps/web/src/providerInstances.ts +++ b/apps/web/src/providerInstances.ts @@ -93,10 +93,7 @@ export function deriveProviderEntriesByEnvironment( const byEnvironment = new Map>(); for (const [environmentId, providers, settings] of providersByEnvironment) { const derived = deriveProviderInstanceEntries(providers); - // The snapshot includes instances enabled by any project; machine settings alone do not. - const entries = settings - ? derived.map((entry) => applyProviderInstanceBranding(entry, settings)) - : derived; + const entries = settings ? brandProviderInstanceEntries(derived, settings) : derived; byEnvironment.set( environmentId, new Map(entries.map((entry) => [entry.instanceId as string, entry] as const)), @@ -105,6 +102,14 @@ export function deriveProviderEntriesByEnvironment( return byEnvironment; } +// The snapshot includes instances enabled by any project; machine settings alone do not. +export function brandProviderInstanceEntries( + entries: ReadonlyArray, + settings: Pick, +): ReadonlyArray { + return entries.map((entry) => applyProviderInstanceBranding(entry, settings)); +} + /** projectId applies project overrides; explicit null uses machine settings. */ export function applyProviderInstanceSettings( entries: ReadonlyArray, diff --git a/packages/client-runtime/src/state/providerInstanceDisplay.test.ts b/packages/client-runtime/src/state/providerInstanceDisplay.test.ts index 27129f11128b..4ac36acedf76 100644 --- a/packages/client-runtime/src/state/providerInstanceDisplay.test.ts +++ b/packages/client-runtime/src/state/providerInstanceDisplay.test.ts @@ -62,6 +62,11 @@ describe("shouldShowInstanceBadge", () => { expect(shouldShowInstanceBadge(entry, [entry])).toBe(true); }); + it("ignores a non-hex accent color", () => { + const entry = { driverKind: codex, accentColor: "blue", enabled: true }; + expect(shouldShowInstanceBadge(entry, [entry])).toBe(false); + }); + it("shows the badge when two entries share a driver, even without an accent", () => { const first = { driverKind: codex, accentColor: undefined, enabled: true }; const second = { driverKind: codex, accentColor: undefined, enabled: true }; diff --git a/packages/client-runtime/src/state/providerInstanceDisplay.ts b/packages/client-runtime/src/state/providerInstanceDisplay.ts index 62f65445e821..ad69b7275342 100644 --- a/packages/client-runtime/src/state/providerInstanceDisplay.ts +++ b/packages/client-runtime/src/state/providerInstanceDisplay.ts @@ -74,7 +74,7 @@ export function shouldShowInstanceBadge( readonly enabled: boolean; }>, ): boolean { - if (entry.accentColor) return true; + if (normalizeProviderAccentColor(entry.accentColor)) return true; let sharedProviderCount = 0; for (const candidate of entries) { if (candidate.driverKind !== entry.driverKind) continue;