From bbd0238648161629fd42d22fbd9292604dd3dd6c Mon Sep 17 00:00:00 2001 From: Vitalii Yehorov Date: Sun, 25 Oct 2026 15:38:28 +0100 Subject: [PATCH 1/4] fix(web,mobile): subagent cards show which account each subagent runs on Closes #24896 Co-Authored-By: Claude Opus 5.5 --- apps/mobile/src/components/ProviderIcon.tsx | 7 ++-- .../threads/thread-subagent-group.tsx | 35 +++++++++++++++---- .../src/components/chat/V2LifecycleRow.tsx | 34 ++++++++++++++++-- 3 files changed, 64 insertions(+), 12 deletions(-) diff --git a/apps/mobile/src/components/ProviderIcon.tsx b/apps/mobile/src/components/ProviderIcon.tsx index 248594ab8655a..d646d1ad4cd30 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 "../lib/providerClients"; 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" }} > ; }) { + const provider = props.providers.find( + (candidate) => candidate.instanceId === props.item.providerInstanceId, + ); + const showBadge = + provider !== undefined && + shouldShowInstanceBadge( + { ...provider, driverKind: provider.driver }, + props.providers.map((candidate) => ({ ...candidate, driverKind: candidate.driver })), + ); return ( - + {showBadge ? ( + + ) : ( + + )} ); } @@ -98,8 +123,6 @@ export function ThreadSubagentGroup(props: { const label = `${agents.length} subagents`; const summary = summarizeSubagentStatuses(agents.map((agent) => agent.status)); const expanded = props.expandedRows[props.anchorKey] ?? false; - const iconUrl = (item: SubagentItem) => - config?.providers.find((provider) => provider.instanceId === item.providerInstanceId)?.iconUrl; return ( {grouped ? ( @@ -113,7 +136,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 193ac1bf297a3..626355903ecb4 100644 --- a/apps/web/src/components/chat/V2LifecycleRow.tsx +++ b/apps/web/src/components/chat/V2LifecycleRow.tsx @@ -259,12 +259,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; }) { @@ -280,7 +284,10 @@ function SubagentAvatar({ @@ -290,7 +297,8 @@ function SubagentAvatar({ {status ? ( @@ -311,17 +319,37 @@ export function EnvironmentSubagentAvatar({ status?: OrchestrationV2TurnItem["status"] | undefined; }) { const providersAtom = serverEnvironment.providersValueAtom(environmentId); + const entries = (providers: ReadonlyArray | null | undefined) => + deriveProviderInstanceEntries(providers ?? []); 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) => providers?.find((provider) => provider.instanceId === providerInstanceId)?.iconUrl, ); - return ; + return ( + + ); } const NOTIFICATION_OUTCOME_STATUS: Record< From 8b1f3131abf083e3e2f610238283aa54cb7a50bd Mon Sep 17 00:00:00 2001 From: Vitalii Yehorov Date: Sun, 25 Oct 2026 17:00:14 +0100 Subject: [PATCH 2/4] fix(client-runtime): a non-hex accent no longer forces the account badge Co-Authored-By: Claude Opus 5.5 --- .../client-runtime/src/state/providerInstanceDisplay.test.ts | 5 +++++ packages/client-runtime/src/state/providerInstanceDisplay.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/client-runtime/src/state/providerInstanceDisplay.test.ts b/packages/client-runtime/src/state/providerInstanceDisplay.test.ts index 27129f11128b3..4ac36acedf761 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 62f65445e8217..ad69b7275342b 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; From cadbe4c113aba62b22e7a64c096f4c066f810cc5 Mon Sep 17 00:00:00 2001 From: Vitalii Yehorov Date: Mon, 26 Oct 2026 07:13:12 +0100 Subject: [PATCH 3/4] fix(web,mobile): distinct ACP agents no longer get account badges on subagent cards Co-Authored-By: Claude Opus 5.5 --- .../threads/thread-subagent-group.tsx | 31 ++++++++++++++----- .../src/components/chat/V2LifecycleRow.tsx | 7 +++-- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/apps/mobile/src/features/threads/thread-subagent-group.tsx b/apps/mobile/src/features/threads/thread-subagent-group.tsx index 42b16eb984483..a700efb2671fa 100644 --- a/apps/mobile/src/features/threads/thread-subagent-group.tsx +++ b/apps/mobile/src/features/threads/thread-subagent-group.tsx @@ -11,7 +11,7 @@ import type { EnvironmentId, OrchestrationV2Subagent, OrchestrationV2TurnItem, - ServerProvider, + ServerConfig, } from "@t3tools/contracts"; import { useEffect, useState } from "react"; import { AppState, Pressable, View, type ColorValue } from "react-native"; @@ -56,17 +56,32 @@ function SubagentElapsed({ agents }: { readonly agents: ReadonlyArray; + readonly config: ServerConfig | null | undefined; }) { - const provider = props.providers.find( + const providers = props.config?.providers ?? []; + const provider = providers.find( + (candidate) => candidate.instanceId === props.item.providerInstanceId, + ); + 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 && - shouldShowInstanceBadge( - { ...provider, driverKind: provider.driver }, - props.providers.map((candidate) => ({ ...candidate, driverKind: candidate.driver })), - ); + badgeEntry !== undefined && + shouldShowInstanceBadge(badgeEntry, badgeEntries); return ( {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 626355903ecb4..00c73a8018d71 100644 --- a/apps/web/src/components/chat/V2LifecycleRow.tsx +++ b/apps/web/src/components/chat/V2LifecycleRow.tsx @@ -319,8 +319,11 @@ export function EnvironmentSubagentAvatar({ status?: OrchestrationV2TurnItem["status"] | undefined; }) { const providersAtom = serverEnvironment.providersValueAtom(environmentId); - const entries = (providers: ReadonlyArray | null | undefined) => - deriveProviderInstanceEntries(providers ?? []); + const settings = useAtomValue(serverEnvironment.settingsValueAtom(environmentId)); + const entries = (providers: ReadonlyArray | null | undefined) => { + const derived = deriveProviderInstanceEntries(providers ?? []); + return settings ? applyProviderInstanceSettings(derived, settings, null) : derived; + }; const displayName = useAtomValue( providersAtom, (providers) => From 23e0eb4a2183ccf1cfe5c1ecd392d6f76ce4a24c Mon Sep 17 00:00:00 2001 From: Vitalii Yehorov Date: Mon, 26 Oct 2026 17:28:56 +0100 Subject: [PATCH 4/4] fix(web): subagent avatars count accounts a project turns on Co-Authored-By: Claude Opus 5.5 --- .../src/components/chat/V2LifecycleRow.tsx | 3 ++- apps/web/src/providerInstances.test.ts | 27 +++++++++++++++++++ apps/web/src/providerInstances.ts | 13 ++++++--- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/apps/web/src/components/chat/V2LifecycleRow.tsx b/apps/web/src/components/chat/V2LifecycleRow.tsx index 00c73a8018d71..9c5649bd28f7a 100644 --- a/apps/web/src/components/chat/V2LifecycleRow.tsx +++ b/apps/web/src/components/chat/V2LifecycleRow.tsx @@ -44,6 +44,7 @@ import { import { Tooltip, TooltipTrigger, TooltipPopup } from "../ui/tooltip"; import { applyProviderInstanceSettings, + brandProviderInstanceEntries, deriveProviderInstanceEntries, shouldShowInstanceBadge, type ProviderInstanceEntry, @@ -322,7 +323,7 @@ export function EnvironmentSubagentAvatar({ const settings = useAtomValue(serverEnvironment.settingsValueAtom(environmentId)); const entries = (providers: ReadonlyArray | null | undefined) => { const derived = deriveProviderInstanceEntries(providers ?? []); - return settings ? applyProviderInstanceSettings(derived, settings, null) : derived; + return settings ? brandProviderInstanceEntries(derived, settings) : derived; }; const displayName = useAtomValue( providersAtom, diff --git a/apps/web/src/providerInstances.test.ts b/apps/web/src/providerInstances.test.ts index 1ab2efb2bb016..cec93b8258016 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, @@ -295,6 +296,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 60c7de4eccac5..0a807808cfe41 100644 --- a/apps/web/src/providerInstances.ts +++ b/apps/web/src/providerInstances.ts @@ -105,10 +105,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)), @@ -117,6 +114,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,