From 523a9de1a7cb31087d7fe7940401d2381619b95d 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 964c60ebcbe2..e56fe80a2089 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 = { @@ -210,3 +211,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 b926db7326e1..5e2e0eae35ff 100644 --- a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx +++ b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx @@ -62,7 +62,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"; @@ -651,6 +651,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"); @@ -1748,10 +1749,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 5ea142d08b4d..2d682b9f2dee 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, @@ -377,7 +377,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; @@ -1123,10 +1125,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 7c8dc39f856be901f655114a21269da40d65b5fb 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 94a78c4dac3f565bd026c3fb06cf3d8b001313b0 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 e56fe80a2089..c72a2c2cdebe 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 = { @@ -212,31 +212,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 5e2e0eae35ff..681147d807a3 100644 --- a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx +++ b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx @@ -62,7 +62,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"; @@ -1749,8 +1749,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 813b64148901..5ee127c828a4 100644 --- a/apps/mobile/src/features/threads/ProviderSubagentBar.tsx +++ b/apps/mobile/src/features/threads/ProviderSubagentBar.tsx @@ -7,7 +7,8 @@ import { useEffect, useState } from "react"; 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"; /** @@ -16,8 +17,12 @@ import { RequestActionButton } from "./RequestActionButton"; * 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; @@ -46,10 +51,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 2d682b9f2dee..dcb703c7bc43 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, @@ -1125,8 +1125,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 129c24150b75..4d9171db3460 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"; @@ -1310,7 +1311,17 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread }} > void; }) { + const sheetColor = useUniwindTheme()["--color-sheet-solid"]; const content = ( <> - + {props.label} {props.collapsible ? ( <> @@ -547,6 +555,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; @@ -616,6 +625,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} @@ -678,6 +688,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 79df03a89467f4d3af130b423aabb09cccefbca3 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 681147d807a3..87a425ced849 100644 --- a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx +++ b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx @@ -1745,7 +1745,11 @@ export function NewTaskDraftScreen(props: { ( diff --git a/apps/mobile/src/features/threads/ProviderSubagentBar.tsx b/apps/mobile/src/features/threads/ProviderSubagentBar.tsx index 5ee127c828a4..e6cabaef1d4b 100644 --- a/apps/mobile/src/features/threads/ProviderSubagentBar.tsx +++ b/apps/mobile/src/features/threads/ProviderSubagentBar.tsx @@ -46,7 +46,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 dcb703c7bc43..e8bbefb9e0fe 100644 --- a/apps/mobile/src/features/threads/ThreadComposer.tsx +++ b/apps/mobile/src/features/threads/ThreadComposer.tsx @@ -1122,7 +1122,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()