From acb322926277680a382eb90af174068e9981add2 Mon Sep 17 00:00:00 2001 From: Vitalii Yehorov Date: Mon, 5 Oct 2026 07:33:34 +0200 Subject: [PATCH 1/5] 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 f833baacd7c3..2815c16e3774 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 181c257cbc17..1389f2ccb63f 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 6873926fd5dd..1ac7c892c972 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 463ff5cc3662..1598c715e8b5 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 4469becee1a2..62e8b299a98a 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/5] 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 1598c715e8b5..fd45b037c0c2 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 62e8b299a98a..1848857389c7 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/5] 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 2815c16e3774..adcba32489b5 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 1389f2ccb63f..25721e2ce34d 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 fc93df0cd7a9..af456263002a 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 1ac7c892c972..41d260675dfe 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 b6930deb5e86..e6bef5702829 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 1848857389c7..6922e096cbc7 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/5] 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 25721e2ce34d..58fdf0ca714d 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 af456263002a..9b79fc3ed730 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 41d260675dfe..fddc878be447 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/5] 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 fd45b037c0c2..edc268c249dc 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 6922e096cbc7..1dd179e2447a 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()