From acb322926277680a382eb90af174068e9981add2 Mon Sep 17 00:00:00 2001 From: Vitalii Yehorov Date: Mon, 5 Oct 2026 07:33:34 +0200 Subject: [PATCH 1/7] fix(mobile): the composer shows which account a thread uses The mobile composer's model control drew only the provider glyph, so two accounts of one provider looked identical there, while web shows the account badge. Model options now carry the badge, decided by the shared shouldShowInstanceBadge rule the thread rows use, and the composer and the new-task draft draw it with the existing ProviderInstanceIcon. Fixes #15910 Co-Authored-By: Claude Opus 5.5 (1M context) --- apps/mobile/src/components/ProviderIcon.tsx | 30 +++++++++++++++++++ .../features/threads/NewTaskDraftScreen.tsx | 9 +++--- .../src/features/threads/ThreadComposer.tsx | 12 ++++---- apps/mobile/src/lib/modelOptions.test.ts | 29 ++++++++++++++++++ apps/mobile/src/lib/modelOptions.ts | 21 +++++++++++++ 5 files changed, 92 insertions(+), 9 deletions(-) diff --git a/apps/mobile/src/components/ProviderIcon.tsx b/apps/mobile/src/components/ProviderIcon.tsx index f833baacd7c37..2815c16e3774c 100644 --- a/apps/mobile/src/components/ProviderIcon.tsx +++ b/apps/mobile/src/components/ProviderIcon.tsx @@ -5,6 +5,7 @@ import { providerInstanceInitials } from "@t3tools/client-runtime/state/provider import { useState } from "react"; import { resolveOfficialAcpRegistryIconUrl } from "@t3tools/contracts"; import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider"; +import type { ModelOption } from "../lib/modelOptions"; import { AppText as Text } from "./AppText"; type ProviderIconProps = { @@ -222,3 +223,32 @@ export function ProviderInstanceIcon(props: { ); } + +/** A model option's provider glyph, with its account badge when the option carries one. */ +export function ModelOptionProviderIcon(props: { + readonly option: + | Pick + | null + | undefined; + readonly size: number; + readonly surfaceColor: string; +}) { + const badge = props.option?.providerBadge; + return badge ? ( + + ) : ( + + ); +} diff --git a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx index 181c257cbc175..1389f2ccb63fc 100644 --- a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx +++ b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx @@ -63,7 +63,7 @@ import { } from "../../state/composer-attachment-uploads"; import { FilePreviewModal, type FilePreviewSource } from "../../components/FilePreviewModal"; import { VideoPreviewModal, type VideoPreviewSource } from "../../components/VideoPreviewModal"; -import { ProviderIcon } from "../../components/ProviderIcon"; +import { ModelOptionProviderIcon } from "../../components/ProviderIcon"; import { SymbolView } from "../../components/AppSymbol"; import { AppText as Text } from "../../components/AppText"; import { hasProviderUsageLimits, isUsageLimitsCommand } from "@t3tools/shared/usageLimits"; @@ -659,6 +659,7 @@ export function NewTaskDraftScreen(props: { const theme = useUniwindTheme(); const foregroundColor = theme["--color-foreground"]; + const composerSurfaceColor = theme["--color-composer-surface"]; const regularFontFamily = useFontFamily("regular"); const bodyText = useScaledTextRole("body"); @@ -1767,10 +1768,10 @@ export function NewTaskDraftScreen(props: { disabled={isComposerInteractionLocked} emphasized renderIcon={(size) => ( - )} label={flow.selectedModelOption?.label ?? "Choose model"} diff --git a/apps/mobile/src/features/threads/ThreadComposer.tsx b/apps/mobile/src/features/threads/ThreadComposer.tsx index 6873926fd5dd1..1ac7c892c9727 100644 --- a/apps/mobile/src/features/threads/ThreadComposer.tsx +++ b/apps/mobile/src/features/threads/ThreadComposer.tsx @@ -80,7 +80,7 @@ import { ComposerInlineControl, ComposerToolbarRow, } from "../../components/ComposerToolbar"; -import { ProviderIcon } from "../../components/ProviderIcon"; +import { ModelOptionProviderIcon } from "../../components/ProviderIcon"; import { composerStripAttachments, type DraftComposerAttachment, @@ -379,7 +379,9 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer const { themeVariables: materialTheme } = useAppearancePreferences(); const composerPanel = materialTheme["--color-composer-panel"]; const navigation = useNavigation(); - const foregroundColor = useUniwindTheme()["--color-foreground"]; + const theme = useUniwindTheme(); + const foregroundColor = theme["--color-foreground"]; + const composerSurfaceColor = theme["--color-composer-surface"]; const bodyText = useScaledTextRole("body"); const fallbackInputRef = useRef(null); const inputRef = props.editorRef ?? fallbackInputRef; @@ -1127,10 +1129,10 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer accessibilityLabel="Model and reasoning settings" emphasized renderIcon={(size) => ( - )} label={currentModelOption?.label ?? currentModelSelection.model} diff --git a/apps/mobile/src/lib/modelOptions.test.ts b/apps/mobile/src/lib/modelOptions.test.ts index 463ff5cc36623..1598c715e8b54 100644 --- a/apps/mobile/src/lib/modelOptions.test.ts +++ b/apps/mobile/src/lib/modelOptions.test.ts @@ -54,6 +54,35 @@ describe("mobile model options", () => { ]); }); + it("badges the account when two instances share a provider or one has an accent", () => { + const claude = (instanceId: string, extra: Record = {}) => ({ + instanceId, + driver: "claudeAgent", + enabled: true, + installed: true, + auth: { status: "authenticated" }, + models: [ + { slug: "claude-opus-5-5", name: "Claude Opus 5.5", isCustom: false, capabilities: null }, + ], + ...extra, + }); + const badges = (providers: ReadonlyArray) => + buildModelOptions({ providers } as unknown as ServerConfig, null).map( + (option) => option.providerBadge, + ); + + expect(badges([claude("claudeAgent")])).toEqual([undefined]); + expect( + badges([claude("claudeAgent", { displayName: "Work", accentColor: "#eae10c" })]), + ).toEqual([{ displayName: "Work", accentColor: "#eae10c" }]); + expect( + badges([ + claude("claudeAgent", { displayName: "Personal" }), + claude("claude_work", { displayName: "Work" }), + ]), + ).toEqual([{ displayName: "Personal" }, { displayName: "Work" }]); + }); + it("carries configured ACP identity into model and provider catalogs", () => { const iconUrl = "https://cdn.agentclientprotocol.com/registry/v1/latest/antigravity-acp.svg"; const config = { diff --git a/apps/mobile/src/lib/modelOptions.ts b/apps/mobile/src/lib/modelOptions.ts index 4469becee1a2d..62e8b299a98ab 100644 --- a/apps/mobile/src/lib/modelOptions.ts +++ b/apps/mobile/src/lib/modelOptions.ts @@ -9,6 +9,11 @@ import { buildExplicitProviderOptionSelectionsFromDescriptors, getProviderOptionDescriptors, } from "@t3tools/shared/model"; +import { + normalizeProviderAccentColor, + resolveProviderInstanceDisplayName, + shouldShowInstanceBadge, +} from "@t3tools/client-runtime/state/provider-instance-display"; export type ModelOption = { readonly key: string; @@ -19,6 +24,8 @@ export type ModelOption = { readonly providerDriver: string; readonly supportedRuntimeModes?: ReadonlyArray; readonly providerIconUrl?: string | undefined; + /** Set when the instance needs the account badge, by the rule the thread rows use. */ + readonly providerBadge?: { readonly displayName: string; readonly accentColor?: string }; readonly isDefault: boolean; readonly isLegacy: boolean; readonly isUnavailable?: boolean; @@ -161,6 +168,9 @@ export function buildModelOptions( providerInstanceId?: ModelSelection["instanceId"], ): ReadonlyArray { const options = new Map(); + const badgeEntries = (config?.providers ?? []).map((provider) => ({ + driverKind: provider.driver, + })); for (const provider of config?.providers ?? []) { if ( @@ -174,6 +184,16 @@ export function buildModelOptions( } const providerLabel = providerDisplayLabel(provider); + const accentColor = normalizeProviderAccentColor(provider.accentColor); + const providerBadge = shouldShowInstanceBadge( + { driverKind: provider.driver, accentColor }, + badgeEntries, + ) + ? { + displayName: resolveProviderInstanceDisplayName(provider), + ...(accentColor ? { accentColor } : {}), + } + : undefined; for (const model of provider.models) { const key = `${provider.instanceId}:${model.slug}`; options.set(key, { @@ -187,6 +207,7 @@ export function buildModelOptions( ? {} : { supportedRuntimeModes: provider.supportedRuntimeModes }), ...(provider.iconUrl ? { providerIconUrl: provider.iconUrl } : {}), + ...(providerBadge ? { providerBadge } : {}), isDefault: model.isDefault === true, isLegacy: model.isLegacy === true, capabilities: model.capabilities, From b63067d46221a4e5103dbe962d61719975565452 Mon Sep 17 00:00:00 2001 From: Vitalii Yehorov Date: Mon, 5 Oct 2026 07:43:57 +0200 Subject: [PATCH 2/7] fix(mobile): composer badges skip distinct ACP agents and keep fallback options badged Co-Authored-By: Claude Opus 5.5 (1M context) --- apps/mobile/src/lib/modelOptions.test.ts | 19 +++++++++++++ apps/mobile/src/lib/modelOptions.ts | 35 +++++++++++++++--------- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/apps/mobile/src/lib/modelOptions.test.ts b/apps/mobile/src/lib/modelOptions.test.ts index 1598c715e8b54..fd45b037c0c2a 100644 --- a/apps/mobile/src/lib/modelOptions.test.ts +++ b/apps/mobile/src/lib/modelOptions.test.ts @@ -81,6 +81,25 @@ describe("mobile model options", () => { claude("claude_work", { displayName: "Work" }), ]), ).toEqual([{ displayName: "Personal" }, { displayName: "Work" }]); + + // Different ACP agents have their own glyphs, so they need no badge. + const acp = (instanceId: string) => ({ + ...claude(instanceId), + driver: "acpRegistry", + }); + const acpConfig = { + providers: [acp("acp_a"), acp("acp_b")], + settings: { + providerInstances: { + acp_a: { driver: "acpRegistry", config: { agentId: "agent-a" } }, + acp_b: { driver: "acpRegistry", config: { agentId: "agent-b" } }, + }, + }, + } as unknown as ServerConfig; + expect(buildModelOptions(acpConfig, null).map((option) => option.providerBadge)).toEqual([ + undefined, + undefined, + ]); }); it("carries configured ACP identity into model and provider catalogs", () => { diff --git a/apps/mobile/src/lib/modelOptions.ts b/apps/mobile/src/lib/modelOptions.ts index 62e8b299a98ab..1848857389c72 100644 --- a/apps/mobile/src/lib/modelOptions.ts +++ b/apps/mobile/src/lib/modelOptions.ts @@ -168,9 +168,25 @@ export function buildModelOptions( providerInstanceId?: ModelSelection["instanceId"], ): ReadonlyArray { const options = new Map(); - const badgeEntries = (config?.providers ?? []).map((provider) => ({ - driverKind: provider.driver, - })); + // Settings carry each ACP instance's agent, whose own glyph already tells it apart. + const badgeEntry = (provider: T3ServerConfig["providers"][number]) => { + const settings = config?.settings?.providerInstances[provider.instanceId]?.config; + const agentId = typeof settings === "object" && settings ? Reflect.get(settings, "agentId") : null; + return { + driverKind: provider.driver, + ...(typeof agentId === "string" && agentId.trim() ? { acpRegistryAgentId: agentId.trim() } : {}), + }; + }; + const badgeEntries = (config?.providers ?? []).map(badgeEntry); + const providerBadgeFor = (provider: T3ServerConfig["providers"][number]) => { + const accentColor = normalizeProviderAccentColor(provider.accentColor); + return shouldShowInstanceBadge({ ...badgeEntry(provider), accentColor }, badgeEntries) + ? { + displayName: resolveProviderInstanceDisplayName(provider), + ...(accentColor ? { accentColor } : {}), + } + : undefined; + }; for (const provider of config?.providers ?? []) { if ( @@ -184,16 +200,7 @@ export function buildModelOptions( } const providerLabel = providerDisplayLabel(provider); - const accentColor = normalizeProviderAccentColor(provider.accentColor); - const providerBadge = shouldShowInstanceBadge( - { driverKind: provider.driver, accentColor }, - badgeEntries, - ) - ? { - displayName: resolveProviderInstanceDisplayName(provider), - ...(accentColor ? { accentColor } : {}), - } - : undefined; + const providerBadge = providerBadgeFor(provider); for (const model of provider.models) { const key = `${provider.instanceId}:${model.slug}`; options.set(key, { @@ -251,6 +258,7 @@ export function buildModelOptions( displayName: provider?.displayName ?? instanceConfig?.displayName, instanceId: fallbackModelSelection.instanceId, }); + const providerBadge = provider ? providerBadgeFor(provider) : undefined; options.set(key, { key, label: model?.name ?? fallbackModelSelection.model, @@ -258,6 +266,7 @@ export function buildModelOptions( providerKey: fallbackModelSelection.instanceId, providerLabel, providerDriver, + ...(providerBadge ? { providerBadge } : {}), isDefault: false, isLegacy: model?.isLegacy === true, ...(isModelSelectionUnavailable(config, fallbackModelSelection) From bb3580e38bf4a168531e3af4df436875c2201134 Mon Sep 17 00:00:00 2001 From: Vitalii Yehorov Date: Mon, 5 Oct 2026 07:52:10 +0200 Subject: [PATCH 3/7] fix(mobile): the model sheet and subagent bar show the account badge too One resolveProviderBadge decides the badge for model options and the subagent bar, and one ProviderAccountIcon draws it. The model sheet's provider header and the subagent bar now use them, as triaged on #15910. Co-Authored-By: Claude Opus 5.5 (1M context) --- apps/mobile/src/components/ProviderIcon.tsx | 30 ++++----- .../features/threads/NewTaskDraftScreen.tsx | 8 ++- .../features/threads/ProviderSubagentBar.tsx | 18 ++++-- .../src/features/threads/ThreadComposer.tsx | 8 ++- .../features/threads/ThreadDetailScreen.tsx | 13 +++- .../features/threads/ThreadSettingsSheet.tsx | 17 ++++- apps/mobile/src/lib/modelOptions.ts | 64 ++++++++++++------- 7 files changed, 102 insertions(+), 56 deletions(-) diff --git a/apps/mobile/src/components/ProviderIcon.tsx b/apps/mobile/src/components/ProviderIcon.tsx index 2815c16e3774c..adcba32489b53 100644 --- a/apps/mobile/src/components/ProviderIcon.tsx +++ b/apps/mobile/src/components/ProviderIcon.tsx @@ -5,7 +5,7 @@ import { providerInstanceInitials } from "@t3tools/client-runtime/state/provider import { useState } from "react"; import { resolveOfficialAcpRegistryIconUrl } from "@t3tools/contracts"; import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider"; -import type { ModelOption } from "../lib/modelOptions"; +import type { ProviderBadge } from "../lib/modelOptions"; import { AppText as Text } from "./AppText"; type ProviderIconProps = { @@ -224,31 +224,25 @@ export function ProviderInstanceIcon(props: { ); } -/** A model option's provider glyph, with its account badge when the option carries one. */ -export function ModelOptionProviderIcon(props: { - readonly option: - | Pick - | null - | undefined; +/** A provider glyph that carries the account badge when its instance needs one. */ +export function ProviderAccountIcon(props: { + readonly provider: string | null | undefined; + readonly iconUrl?: string | null | undefined; + readonly badge: ProviderBadge | undefined; readonly size: number; readonly surfaceColor: string; }) { - const badge = props.option?.providerBadge; - return badge ? ( + return props.badge ? ( ) : ( - + ); } diff --git a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx index 1389f2ccb63fc..25721e2ce34db 100644 --- a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx +++ b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx @@ -63,7 +63,7 @@ import { } from "../../state/composer-attachment-uploads"; import { FilePreviewModal, type FilePreviewSource } from "../../components/FilePreviewModal"; import { VideoPreviewModal, type VideoPreviewSource } from "../../components/VideoPreviewModal"; -import { ModelOptionProviderIcon } from "../../components/ProviderIcon"; +import { ProviderAccountIcon } from "../../components/ProviderIcon"; import { SymbolView } from "../../components/AppSymbol"; import { AppText as Text } from "../../components/AppText"; import { hasProviderUsageLimits, isUsageLimitsCommand } from "@t3tools/shared/usageLimits"; @@ -1768,8 +1768,10 @@ export function NewTaskDraftScreen(props: { disabled={isComposerInteractionLocked} emphasized renderIcon={(size) => ( - diff --git a/apps/mobile/src/features/threads/ProviderSubagentBar.tsx b/apps/mobile/src/features/threads/ProviderSubagentBar.tsx index fc93df0cd7a94..af456263002ab 100644 --- a/apps/mobile/src/features/threads/ProviderSubagentBar.tsx +++ b/apps/mobile/src/features/threads/ProviderSubagentBar.tsx @@ -6,7 +6,8 @@ import { isOrchestrationV2WorkActive } from "@t3tools/contracts"; import { View } from "react-native"; import { AppText as Text } from "../../components/AppText"; -import { ProviderIcon } from "../../components/ProviderIcon"; +import { ProviderAccountIcon } from "../../components/ProviderIcon"; +import type { ProviderBadge } from "../../lib/modelOptions"; import { RequestActionButton } from "./RequestActionButton"; import { useVisibleSecondClock } from "./use-visible-second-clock"; @@ -16,8 +17,12 @@ import { useVisibleSecondClock } from "./use-visible-second-clock"; * model is working, for how long, and leads back to the parent. */ export function ProviderSubagentBar(props: { - /** Driver and catalog icon of the provider running the subagent. */ - readonly provider: { readonly driver: string; readonly iconUrl?: string | undefined } | null; + /** Driver, catalog icon and account badge of the provider running the subagent. */ + readonly provider: { + readonly driver: string; + readonly iconUrl?: string | undefined; + readonly badge?: ProviderBadge | undefined; + } | null; readonly modelLabel: string; /** Reasoning effort as the composer names it, when the subagent has one. */ readonly effortLabel: string | null; @@ -41,10 +46,13 @@ export function ProviderSubagentBar(props: { > {props.provider ? ( - ) : null} diff --git a/apps/mobile/src/features/threads/ThreadComposer.tsx b/apps/mobile/src/features/threads/ThreadComposer.tsx index 1ac7c892c9727..41d260675dfec 100644 --- a/apps/mobile/src/features/threads/ThreadComposer.tsx +++ b/apps/mobile/src/features/threads/ThreadComposer.tsx @@ -80,7 +80,7 @@ import { ComposerInlineControl, ComposerToolbarRow, } from "../../components/ComposerToolbar"; -import { ModelOptionProviderIcon } from "../../components/ProviderIcon"; +import { ProviderAccountIcon } from "../../components/ProviderIcon"; import { composerStripAttachments, type DraftComposerAttachment, @@ -1129,8 +1129,10 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer accessibilityLabel="Model and reasoning settings" emphasized renderIcon={(size) => ( - diff --git a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx index b6930deb5e864..e6bef57028293 100644 --- a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx @@ -91,6 +91,7 @@ import type { StatusTone } from "../../components/StatusPill"; import type { DraftComposerAttachment } from "../../lib/composerImages"; import { RenderErrorBoundary, RenderFailureView } from "../../components/RenderErrorBoundary"; import { CHAT_CONTENT_MAX_WIDTH, type LayoutVariant } from "../../lib/layout"; +import { resolveProviderBadge } from "../../lib/modelOptions"; import { editPendingThreadMessage } from "../../state/edit-pending-thread-message"; import { deviceEnvironment } from "../../state/device"; import { useEnvironmentQuery } from "../../state/query"; @@ -1348,7 +1349,17 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread }} > void; }) { + const sheetColor = useUniwindTheme()["--color-sheet-solid"]; const content = ( <> - + {props.label} {props.collapsible ? ( <> @@ -545,6 +553,7 @@ type ThreadSettingsProviderCatalog = { readonly key: string; readonly driver: string | undefined; readonly iconUrl: string | undefined; + readonly badge: ProviderBadge | undefined; readonly label: string; readonly collapsible: boolean; readonly collapsed: boolean; @@ -614,6 +623,7 @@ function ThreadSettingsProviderListHeader(props: { collapsed={props.provider.collapsed} driver={props.provider.driver} iconUrl={props.provider.iconUrl} + badge={props.provider.badge} label={props.provider.label} modelCount={props.provider.modelCount} onToggle={onToggle} @@ -676,6 +686,7 @@ function useThreadSettingsCatalogItems( key: group.providerKey, driver, iconUrl: group.models[0]?.providerIconUrl, + badge: group.models[0]?.providerBadge, label: group.providerLabel, collapsible, collapsed, diff --git a/apps/mobile/src/lib/modelOptions.ts b/apps/mobile/src/lib/modelOptions.ts index 1848857389c72..6922e096cbc76 100644 --- a/apps/mobile/src/lib/modelOptions.ts +++ b/apps/mobile/src/lib/modelOptions.ts @@ -24,8 +24,8 @@ export type ModelOption = { readonly providerDriver: string; readonly supportedRuntimeModes?: ReadonlyArray; readonly providerIconUrl?: string | undefined; - /** Set when the instance needs the account badge, by the rule the thread rows use. */ - readonly providerBadge?: { readonly displayName: string; readonly accentColor?: string }; + /** Set when the instance needs the account badge. */ + readonly providerBadge?: ProviderBadge; readonly isDefault: boolean; readonly isLegacy: boolean; readonly isUnavailable?: boolean; @@ -33,6 +33,43 @@ export type ModelOption = { readonly selection: ModelSelection; }; +/** The account badge an instance's glyph carries. */ +export type ProviderBadge = { readonly displayName: string; readonly accentColor?: string }; + +type ServerProvider = T3ServerConfig["providers"][number]; + +/** + * The badge an instance needs by the shared rule: an accent colour, or another + * instance of the same provider. Settings carry each ACP instance's agent, + * whose own glyph already tells it apart. + */ +export function resolveProviderBadge( + config: T3ServerConfig | null | undefined, + provider: ServerProvider, +): ProviderBadge | undefined { + const badgeEntry = (candidate: ServerProvider) => { + const settings = config?.settings?.providerInstances[candidate.instanceId]?.config; + const agentId = + typeof settings === "object" && settings ? Reflect.get(settings, "agentId") : null; + return { + driverKind: candidate.driver, + ...(typeof agentId === "string" && agentId.trim() + ? { acpRegistryAgentId: agentId.trim() } + : {}), + }; + }; + const accentColor = normalizeProviderAccentColor(provider.accentColor); + return shouldShowInstanceBadge( + { ...badgeEntry(provider), accentColor }, + (config?.providers ?? []).map(badgeEntry), + ) + ? { + displayName: resolveProviderInstanceDisplayName(provider), + ...(accentColor ? { accentColor } : {}), + } + : undefined; +} + export type ProviderGroup = { readonly providerKey: string; readonly providerLabel: string; @@ -168,25 +205,6 @@ export function buildModelOptions( providerInstanceId?: ModelSelection["instanceId"], ): ReadonlyArray { const options = new Map(); - // Settings carry each ACP instance's agent, whose own glyph already tells it apart. - const badgeEntry = (provider: T3ServerConfig["providers"][number]) => { - const settings = config?.settings?.providerInstances[provider.instanceId]?.config; - const agentId = typeof settings === "object" && settings ? Reflect.get(settings, "agentId") : null; - return { - driverKind: provider.driver, - ...(typeof agentId === "string" && agentId.trim() ? { acpRegistryAgentId: agentId.trim() } : {}), - }; - }; - const badgeEntries = (config?.providers ?? []).map(badgeEntry); - const providerBadgeFor = (provider: T3ServerConfig["providers"][number]) => { - const accentColor = normalizeProviderAccentColor(provider.accentColor); - return shouldShowInstanceBadge({ ...badgeEntry(provider), accentColor }, badgeEntries) - ? { - displayName: resolveProviderInstanceDisplayName(provider), - ...(accentColor ? { accentColor } : {}), - } - : undefined; - }; for (const provider of config?.providers ?? []) { if ( @@ -200,7 +218,7 @@ export function buildModelOptions( } const providerLabel = providerDisplayLabel(provider); - const providerBadge = providerBadgeFor(provider); + const providerBadge = resolveProviderBadge(config, provider); for (const model of provider.models) { const key = `${provider.instanceId}:${model.slug}`; options.set(key, { @@ -258,7 +276,7 @@ export function buildModelOptions( displayName: provider?.displayName ?? instanceConfig?.displayName, instanceId: fallbackModelSelection.instanceId, }); - const providerBadge = provider ? providerBadgeFor(provider) : undefined; + const providerBadge = provider ? resolveProviderBadge(config, provider) : undefined; options.set(key, { key, label: model?.name ?? fallbackModelSelection.model, From 6d07a38fadd05250814e0f952b5b7b550c6b454c Mon Sep 17 00:00:00 2001 From: Vitalii Yehorov Date: Mon, 5 Oct 2026 08:42:34 +0200 Subject: [PATCH 4/7] fix(mobile): screen readers hear the account the badge shows Co-Authored-By: Claude Opus 5.5 (1M context) --- apps/mobile/src/features/threads/NewTaskDraftScreen.tsx | 6 +++++- apps/mobile/src/features/threads/ProviderSubagentBar.tsx | 2 +- apps/mobile/src/features/threads/ThreadComposer.tsx | 6 +++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx index 25721e2ce34db..58fdf0ca714de 100644 --- a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx +++ b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx @@ -1764,7 +1764,11 @@ export function NewTaskDraftScreen(props: { ( diff --git a/apps/mobile/src/features/threads/ProviderSubagentBar.tsx b/apps/mobile/src/features/threads/ProviderSubagentBar.tsx index af456263002ab..9b79fc3ed730e 100644 --- a/apps/mobile/src/features/threads/ProviderSubagentBar.tsx +++ b/apps/mobile/src/features/threads/ProviderSubagentBar.tsx @@ -41,7 +41,7 @@ export function ProviderSubagentBar(props: { {/* Only the text is one element, so "Open parent" stays reachable. */} diff --git a/apps/mobile/src/features/threads/ThreadComposer.tsx b/apps/mobile/src/features/threads/ThreadComposer.tsx index 41d260675dfec..fddc878be4476 100644 --- a/apps/mobile/src/features/threads/ThreadComposer.tsx +++ b/apps/mobile/src/features/threads/ThreadComposer.tsx @@ -1126,7 +1126,11 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer /> ( Date: Tue, 6 Oct 2026 05:25:31 +0200 Subject: [PATCH 5/7] fix(mobile): local ACP commands count as one provider for the account badge, as on web Co-Authored-By: Claude Opus 5.5 (1M context) --- apps/mobile/src/lib/modelOptions.test.ts | 14 ++++++++++++++ apps/mobile/src/lib/modelOptions.ts | 5 ++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/apps/mobile/src/lib/modelOptions.test.ts b/apps/mobile/src/lib/modelOptions.test.ts index fd45b037c0c2a..edc268c249dca 100644 --- a/apps/mobile/src/lib/modelOptions.test.ts +++ b/apps/mobile/src/lib/modelOptions.test.ts @@ -100,6 +100,20 @@ describe("mobile model options", () => { undefined, undefined, ]); + + // Local ACP commands have no registry agent, so two of them share a glyph. + const localConfig = { + ...acpConfig, + settings: { + providerInstances: { + acp_a: { driver: "acpRegistry", config: { source: "local", agentId: "agent-a" } }, + acp_b: { driver: "acpRegistry", config: { source: "local", agentId: "agent-b" } }, + }, + }, + } as unknown as ServerConfig; + expect( + buildModelOptions(localConfig, null).every((option) => option.providerBadge !== undefined), + ).toBe(true); }); it("carries configured ACP identity into model and provider catalogs", () => { diff --git a/apps/mobile/src/lib/modelOptions.ts b/apps/mobile/src/lib/modelOptions.ts index 6922e096cbc76..1dd179e2447a1 100644 --- a/apps/mobile/src/lib/modelOptions.ts +++ b/apps/mobile/src/lib/modelOptions.ts @@ -49,8 +49,11 @@ export function resolveProviderBadge( ): ProviderBadge | undefined { const badgeEntry = (candidate: ServerProvider) => { const settings = config?.settings?.providerInstances[candidate.instanceId]?.config; + // A local ACP command has no registry agent, as on web. const agentId = - typeof settings === "object" && settings ? Reflect.get(settings, "agentId") : null; + typeof settings === "object" && settings && Reflect.get(settings, "source") !== "local" + ? Reflect.get(settings, "agentId") + : null; return { driverKind: candidate.driver, ...(typeof agentId === "string" && agentId.trim() From a06959f3a1d6d48170d26a7a30516c197a46a948 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 19 Oct 2026 04:13:01 +0000 Subject: [PATCH 6/7] fix(mobile): identify provider accounts in thread model controls --- apps/mobile/src/components/ProviderIcon.tsx | 84 +++---- .../features/threads/NewTaskDraftScreen.tsx | 7 +- .../features/threads/ProviderSubagentBar.tsx | 20 +- .../src/features/threads/ThreadComposer.tsx | 7 +- .../features/threads/ThreadDetailScreen.tsx | 15 +- .../features/threads/ThreadSettingsSheet.tsx | 18 +- apps/mobile/src/lib/modelOptions.test.ts | 209 ++++++++++++------ apps/mobile/src/lib/modelOptions.ts | 47 +--- apps/mobile/src/lib/providerInstances.ts | 46 ++++ 9 files changed, 255 insertions(+), 198 deletions(-) create mode 100644 apps/mobile/src/lib/providerInstances.ts diff --git a/apps/mobile/src/components/ProviderIcon.tsx b/apps/mobile/src/components/ProviderIcon.tsx index 2b9c0701f01ee..cd16885bfd282 100644 --- a/apps/mobile/src/components/ProviderIcon.tsx +++ b/apps/mobile/src/components/ProviderIcon.tsx @@ -5,7 +5,6 @@ import { providerInstanceInitials } from "@t3tools/client-runtime/state/provider import { useId, useState } from "react"; import { resolveOfficialAcpRegistryIconUrl } from "@t3tools/contracts"; import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider"; -import type { ProviderBadge } from "../lib/modelOptions"; import { AppText as Text } from "./ui/AppText"; import { getProviderClient } from "../lib/providerClients"; @@ -152,74 +151,51 @@ export function ProviderInstanceIcon(props: { readonly iconUrl?: string | null; readonly provider: string | null | undefined; readonly size?: number; - readonly displayName: string; + readonly displayName?: string; readonly accentColor?: string; readonly showBadge?: boolean; readonly surfaceColor: string; }) { + if (!props.showBadge) { + return ; + } return ( - {props.showBadge ? ( - // A wide right-aligned box lets the badge fit its initials. Alone, an - // absolute badge is capped at the icon's width, which clips "WO". + {/* A wide box lets initials extend beyond the glyph without clipping. */} + - - - {providerInstanceInitials(props.displayName)} - - + {providerInstanceInitials(props.displayName ?? "")} + - ) : null} + ); } - -/** A provider glyph that carries the account badge when its instance needs one. */ -export function ProviderAccountIcon(props: { - readonly provider: string | null | undefined; - readonly iconUrl?: string | null | undefined; - readonly badge: ProviderBadge | undefined; - readonly size: number; - readonly surfaceColor: string; -}) { - return props.badge ? ( - - ) : ( - - ); -} diff --git a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx index 9c2da962a91e5..a4e60c2a31e45 100644 --- a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx +++ b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx @@ -66,7 +66,7 @@ import { } from "../../state/composer-attachment-uploads"; import { FilePreviewModal, type FilePreviewSource } from "../../components/FilePreviewModal"; import { VideoPreviewModal, type VideoPreviewSource } from "../../components/VideoPreviewModal"; -import { ProviderAccountIcon } from "../../components/ProviderIcon"; +import { ProviderInstanceIcon } from "../../components/ProviderIcon"; import { SymbolView } from "../../components/ui/AppSymbol"; import { AppText as Text } from "../../components/ui/AppText"; import { hasProviderUsageLimits, isUsageLimitsCommand } from "@t3tools/shared/usageLimits"; @@ -1709,10 +1709,11 @@ export function NewTaskDraftScreen(props: { disabled={isComposerInteractionLocked} emphasized renderIcon={(size) => ( - diff --git a/apps/mobile/src/features/threads/ProviderSubagentBar.tsx b/apps/mobile/src/features/threads/ProviderSubagentBar.tsx index 1d89dbccaf108..bc785a188538d 100644 --- a/apps/mobile/src/features/threads/ProviderSubagentBar.tsx +++ b/apps/mobile/src/features/threads/ProviderSubagentBar.tsx @@ -6,18 +6,14 @@ import { isOrchestrationV2WorkActive } from "@t3tools/contracts"; import { View } from "react-native"; import { AppText as Text } from "../../components/ui/AppText"; -import { ProviderAccountIcon } from "../../components/ProviderIcon"; -import type { ProviderBadge } from "../../lib/modelOptions"; +import { ProviderInstanceIcon } from "../../components/ProviderIcon"; +import type { ProviderBadge } from "../../lib/providerInstances"; import { RequestActionButton } from "./RequestActionButton"; import { useVisibleSecondClock } from "./use-visible-second-clock"; export function ProviderSubagentBar(props: { - /** Driver, catalog icon and account badge of the provider running the subagent. */ - readonly provider: { - readonly driver: string; - readonly iconUrl?: string | undefined; - readonly badge?: ProviderBadge | undefined; - } | null; + readonly provider: { readonly driver: string; readonly iconUrl?: string | undefined } | null; + readonly providerBadge: ProviderBadge | undefined; readonly modelLabel: string; readonly effortLabel: string | null; readonly status: ProviderSubagentStatus | null; @@ -34,17 +30,17 @@ export function ProviderSubagentBar(props: { {} {props.provider ? ( - ) : null} diff --git a/apps/mobile/src/features/threads/ThreadComposer.tsx b/apps/mobile/src/features/threads/ThreadComposer.tsx index b1a84d4d0005b..af3aaed00e652 100644 --- a/apps/mobile/src/features/threads/ThreadComposer.tsx +++ b/apps/mobile/src/features/threads/ThreadComposer.tsx @@ -77,7 +77,7 @@ import { ComposerInlineControl, ComposerToolbarRow, } from "../../components/ComposerToolbar"; -import { ProviderAccountIcon } from "../../components/ProviderIcon"; +import { ProviderInstanceIcon } from "../../components/ProviderIcon"; import { composerStripAttachments, type DraftComposerFileAttachment, @@ -1026,10 +1026,11 @@ export const ThreadComposer = function ThreadComposer(props: ThreadComposerProps } emphasized renderIcon={(size) => ( - diff --git a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx index 9ee30f3a4ed4d..d4e3e145197ae 100644 --- a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx @@ -95,7 +95,7 @@ import { CHAT_CONTENT_MAX_WIDTH, type LayoutVariant, } from "../../lib/layout"; -import { resolveProviderBadge } from "../../lib/modelOptions"; +import { resolveProviderBadge } from "../../lib/providerInstances"; import { editPendingThreadMessage } from "../../state/edit-pending-thread-message"; import { deviceEnvironment } from "../../state/device"; import { useEnvironmentQuery } from "../../state/query"; @@ -1273,16 +1273,11 @@ export const ThreadDetailScreen = function ThreadDetailScreen(props: ThreadDetai }} > - @@ -157,7 +158,7 @@ function ProviderHeader(props: { if (props.collapsible) { return ( + {content} ); diff --git a/apps/mobile/src/lib/modelOptions.test.ts b/apps/mobile/src/lib/modelOptions.test.ts index f3dcf019551ea..875292fd6ac4b 100644 --- a/apps/mobile/src/lib/modelOptions.test.ts +++ b/apps/mobile/src/lib/modelOptions.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vite-plus/test"; import { DEFAULT_SERVER_SETTINGS, + EnvironmentId, ProjectId, ProviderDriverKind, ProviderInstanceId, @@ -21,6 +22,53 @@ import { type ModelOption, } from "./modelOptions"; +function configWith(settings: ServerSettings): ServerConfig { + return { + environment: { + environmentId: EnvironmentId.make("environment-test"), + label: "Test", + platform: { os: "linux", arch: "x64" }, + serverVersion: "1.0.0", + capabilities: { repositoryIdentity: false }, + }, + auth: { + policy: "loopback-browser", + bootstrapMethods: [], + sessionMethods: [], + sessionCookieName: "test-session", + }, + cwd: "/project", + keybindingsConfigPath: "/keybindings.json", + keybindings: [], + issues: [], + availableEditors: [], + observability: { + logsDirectoryPath: "/logs", + localTracingEnabled: false, + otlpTracesEnabled: false, + otlpMetricsEnabled: false, + otlpLogsEnabled: false, + }, + providers: [ + { + instanceId: ProviderInstanceId.make("codex_work"), + driver: ProviderDriverKind.make("codex"), + displayName: "Codex Work", + enabled: true, + installed: true, + version: null, + status: "ready", + auth: { status: "authenticated" }, + checkedAt: "2026-01-01T00:00:00.000Z", + models: [{ slug: "model-a", name: "Model A", isCustom: false, capabilities: null }], + slashCommands: [], + skills: [], + }, + ], + settings, + }; +} + describe("mobile model options", () => { it("groups models by provider and flags legacy entries", () => { const config = { @@ -63,66 +111,114 @@ describe("mobile model options", () => { ]); }); - it("badges the account when two instances share a provider or one has an accent", () => { - const claude = (instanceId: string, extra: Record = {}) => ({ - instanceId, - driver: "claudeAgent", - enabled: true, - installed: true, - auth: { status: "authenticated" }, - models: [ - { slug: "claude-opus-5-5", name: "Claude Opus 5.5", isCustom: false, capabilities: null }, - ], - ...extra, + it("keeps account identity in new-task, provider-filtered, and fallback model options", () => { + const driver = ProviderDriverKind.make("codex"); + const config = configWith({ + ...DEFAULT_SERVER_SETTINGS, + providerInstances: { [ProviderInstanceId.make("codex_work")]: { driver, enabled: true } }, }); - const badges = (providers: ReadonlyArray) => - buildModelOptions({ providers } as unknown as ServerConfig, null).map( - (option) => option.providerBadge, - ); - - expect(badges([claude("claudeAgent")])).toEqual([undefined]); + const work = config.providers[0]!; + expect(buildModelOptions(config, null, undefined, null)[0]?.providerBadge).toBeUndefined(); expect( - badges([claude("claudeAgent", { displayName: "Work", accentColor: "#eae10c" })]), - ).toEqual([{ displayName: "Work", accentColor: "#eae10c" }]); - expect( - badges([ - claude("claudeAgent", { displayName: "Personal" }), - claude("claude_work", { displayName: "Work" }), - ]), - ).toEqual([{ displayName: "Personal" }, { displayName: "Work" }]); - - // Different ACP agents have their own glyphs, so they need no badge. - const acp = (instanceId: string) => ({ - ...claude(instanceId), - driver: "acpRegistry", - }); - const acpConfig = { - providers: [acp("acp_a"), acp("acp_b")], + buildModelOptions( + { ...config, providers: [{ ...work, accentColor: "#eae10c" }] }, + null, + undefined, + null, + )[0]?.providerBadge, + ).toEqual({ displayName: "Codex Work", accentColor: "#eae10c" }); + + const accounts = { + ...config, settings: { + ...config.settings, providerInstances: { - acp_a: { driver: "acpRegistry", config: { agentId: "agent-a" } }, - acp_b: { driver: "acpRegistry", config: { agentId: "agent-b" } }, + ...config.settings.providerInstances, + [ProviderInstanceId.make("codex_personal")]: { driver, enabled: true }, }, }, - } as unknown as ServerConfig; - expect(buildModelOptions(acpConfig, null).map((option) => option.providerBadge)).toEqual([ - undefined, - undefined, - ]); + providers: [ + work, + { ...work, instanceId: ProviderInstanceId.make("codex_personal"), displayName: "Personal" }, + ], + }; + expect( + groupByProvider(buildModelOptions(accounts, null, undefined, null)).map( + (group) => group.models[0]?.providerBadge, + ), + ).toEqual([{ displayName: "Codex Work" }, { displayName: "Personal" }]); + expect( + buildModelOptions(accounts, null, work.instanceId, null).map( + (option) => option.providerBadge, + ), + ).toEqual([{ displayName: "Codex Work" }]); + const selection = { instanceId: work.instanceId, model: "saved-model" }; + expect( + buildModelOptions(accounts, selection, work.instanceId, null).find( + (option) => option.selection.model === selection.model, + ), + ).toMatchObject({ selection, providerBadge: { displayName: "Codex Work" } }); + }); - // Local ACP commands have no registry agent, so two of them share a glyph. - const localConfig = { - ...acpConfig, + it("badges local ACP accounts while distinct registry agents keep their own glyph identity", () => { + const driver = ProviderDriverKind.make("acpRegistry"); + const config = configWith({ + ...DEFAULT_SERVER_SETTINGS, + providerInstances: { + [ProviderInstanceId.make("acp_work")]: { + driver, + enabled: true, + config: { agentId: "agent-a" }, + }, + [ProviderInstanceId.make("acp_personal")]: { + driver, + enabled: true, + config: { agentId: "agent-b" }, + }, + }, + }); + const provider = config.providers[0]!; + const registry = { + ...config, + providers: [ + { + ...provider, + driver, + instanceId: ProviderInstanceId.make("acp_work"), + displayName: "Work", + }, + { + ...provider, + driver, + instanceId: ProviderInstanceId.make("acp_personal"), + displayName: "Personal", + }, + ], + }; + expect( + buildModelOptions(registry, null, undefined, null).map((option) => option.providerBadge), + ).toEqual([undefined, undefined]); + const local = { + ...registry, settings: { + ...registry.settings, providerInstances: { - acp_a: { driver: "acpRegistry", config: { source: "local", agentId: "agent-a" } }, - acp_b: { driver: "acpRegistry", config: { source: "local", agentId: "agent-b" } }, + [ProviderInstanceId.make("acp_work")]: { + driver, + enabled: true, + config: { source: "local", agentId: "agent-a" }, + }, + [ProviderInstanceId.make("acp_personal")]: { + driver, + enabled: true, + config: { source: "local", agentId: "agent-b" }, + }, }, }, - } as unknown as ServerConfig; + }; expect( - buildModelOptions(localConfig, null).every((option) => option.providerBadge !== undefined), - ).toBe(true); + buildModelOptions(local, null, undefined, null).map((option) => option.providerBadge), + ).toEqual([{ displayName: "Work" }, { displayName: "Personal" }]); }); it("carries configured ACP identity into model and provider catalogs", () => { @@ -597,23 +693,6 @@ describe("mobile model options", () => { const projectId = ProjectId.make("project-a"); const instanceId = ProviderInstanceId.make("codex_work"); - function configWith(settings: ServerSettings): ServerConfig { - return { - providers: [ - { - instanceId, - driver: "codex", - displayName: "Codex Work", - enabled: true, - installed: true, - auth: { status: "authenticated" }, - models: [{ slug: "model-a", name: "Model A", isCustom: false, capabilities: null }], - }, - ], - settings, - } as unknown as ServerConfig; - } - it("words a project's own turn-off apart from missing setup", () => { const settings: ServerSettings = { ...DEFAULT_SERVER_SETTINGS, diff --git a/apps/mobile/src/lib/modelOptions.ts b/apps/mobile/src/lib/modelOptions.ts index a7e1771ac972a..d0f6661ba9d28 100644 --- a/apps/mobile/src/lib/modelOptions.ts +++ b/apps/mobile/src/lib/modelOptions.ts @@ -11,11 +11,7 @@ import { buildExplicitProviderOptionSelectionsFromDescriptors, getProviderOptionDescriptors, } from "@t3tools/shared/model"; -import { - normalizeProviderAccentColor, - resolveProviderInstanceDisplayName, - shouldShowInstanceBadge, -} from "@t3tools/client-runtime/state/provider-instance-display"; +import { resolveProviderBadge, type ProviderBadge } from "./providerInstances"; export type ModelOption = { readonly key: string; @@ -26,7 +22,6 @@ export type ModelOption = { readonly providerDriver: string; readonly supportedRuntimeModes?: ReadonlyArray; readonly providerIconUrl?: string | undefined; - /** Set when the instance needs the account badge. */ readonly providerBadge?: ProviderBadge; readonly isDefault: boolean; readonly isLegacy: boolean; @@ -38,45 +33,6 @@ export type ModelOption = { readonly providerUpdateRequired?: ProviderUpdateRequired; }; -/** The account badge an instance's glyph carries. */ -export type ProviderBadge = { readonly displayName: string; readonly accentColor?: string }; - -type ServerProvider = T3ServerConfig["providers"][number]; - -/** - * The badge an instance needs by the shared rule: an accent colour, or another - * instance of the same provider. Settings carry each ACP instance's agent, - * whose own glyph already tells it apart. - */ -export function resolveProviderBadge( - config: T3ServerConfig | null | undefined, - provider: ServerProvider, -): ProviderBadge | undefined { - const badgeEntry = (candidate: ServerProvider) => { - const settings = config?.settings?.providerInstances[candidate.instanceId]?.config; - // A local ACP command has no registry agent, as on web. - const agentId = - typeof settings === "object" && settings && Reflect.get(settings, "source") !== "local" - ? Reflect.get(settings, "agentId") - : null; - return { - driverKind: candidate.driver, - ...(typeof agentId === "string" && agentId.trim() - ? { acpRegistryAgentId: agentId.trim() } - : {}), - }; - }; - const accentColor = normalizeProviderAccentColor(provider.accentColor); - return shouldShowInstanceBadge( - { ...badgeEntry(provider), accentColor }, - (config?.providers ?? []).map(badgeEntry), - ) - ? { - displayName: resolveProviderInstanceDisplayName(provider), - ...(accentColor ? { accentColor } : {}), - } - : undefined; -} type ProviderUpdateRequired = Pick< T3ServerConfig["providers"][number], "driver" | "updateRequiredModels" @@ -381,6 +337,7 @@ export function buildModelOptions( providerKey: fallbackModelSelection.instanceId, providerLabel, providerDriver, + ...(provider?.iconUrl ? { providerIconUrl: provider.iconUrl } : {}), ...(providerBadge ? { providerBadge } : {}), isDefault: false, isLegacy: model?.isLegacy === true, diff --git a/apps/mobile/src/lib/providerInstances.ts b/apps/mobile/src/lib/providerInstances.ts new file mode 100644 index 0000000000000..f91676d28aafc --- /dev/null +++ b/apps/mobile/src/lib/providerInstances.ts @@ -0,0 +1,46 @@ +import type { ServerConfig, ServerProvider } from "@t3tools/contracts"; +import { + normalizeProviderAccentColor, + resolveProviderInstanceDisplayName, + shouldShowInstanceBadge, +} from "@t3tools/client-runtime/state/provider-instance-display"; + +export type ProviderBadge = { + readonly displayName: string; + readonly accentColor?: string; +}; + +export function resolveProviderBadge( + config: ServerConfig | null | undefined, + provider: ServerProvider, +): ProviderBadge | undefined { + const badgeEntry = (candidate: ServerProvider) => { + const settings = config?.settings?.providerInstances[candidate.instanceId]?.config; + const agentId = + candidate.driver === "acpRegistry" && + typeof settings === "object" && + settings !== null && + !("source" in settings && settings.source === "local") && + "agentId" in settings && + typeof settings.agentId === "string" + ? settings.agentId.trim() + : undefined; + return { + driverKind: candidate.driver, + acpRegistryAgentId: agentId || undefined, + }; + }; + const accentColor = normalizeProviderAccentColor(provider.accentColor); + if ( + !shouldShowInstanceBadge( + { ...badgeEntry(provider), accentColor }, + (config?.providers ?? []).map(badgeEntry), + ) + ) { + return undefined; + } + return { + displayName: resolveProviderInstanceDisplayName(provider), + ...(accentColor ? { accentColor } : {}), + }; +} From 7c6b0da714dcfb2f7a4a83d1f27b9edfcea66226 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 19 Oct 2026 04:17:36 +0000 Subject: [PATCH 7/7] fix(mobile): preserve provider icon dimming in thread rows --- .../features/threads/thread-list-v2-items.tsx | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/apps/mobile/src/features/threads/thread-list-v2-items.tsx b/apps/mobile/src/features/threads/thread-list-v2-items.tsx index f49e96381d789..0019e87da2ab4 100644 --- a/apps/mobile/src/features/threads/thread-list-v2-items.tsx +++ b/apps/mobile/src/features/threads/thread-list-v2-items.tsx @@ -1115,15 +1115,17 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { ))} - + + + ) : null}