From 1c38193d5ab6938f7de160a4ee0bc1fda43929d6 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Fri, 6 Nov 2026 02:17:42 -0800 Subject: [PATCH 1/2] fix(mobile): keep providers listed when their health probe lags On iOS, Claude disappeared from both the model picker and @opus autocomplete while Claude subagents were running fine in the same thread. Mobile removed a provider's whole group whenever the snapshot said not-installed or signed-out (states a pending or failed account probe reports), and model mentions additionally required status ready, so a warning probe hid the provider there too. Web keeps an enabled provider visible with its reason. Keep enabled providers in the mobile picker and mark their models unavailable instead of erasing the group (Antigravity keeps its setup gate, since its catalog only exists once set up). Let mobile and web mentions name models from warning-status providers, matching what the server still delegates through. Also forward the model short name in mobile mention search, like web already does. Done with Muse Code (muse-wonder-full-1.0). --- .../threads/use-composer-command-menu.test.ts | 25 +++++++++++++ .../threads/use-composer-command-menu.ts | 4 +- apps/mobile/src/lib/modelOptions.test.ts | 37 +++++++++++++++++++ apps/mobile/src/lib/modelOptions.ts | 11 ++++-- .../chat/composerModelMentions.test.ts | 6 +++ .../components/chat/composerModelMentions.ts | 13 ++++++- 6 files changed, 90 insertions(+), 6 deletions(-) diff --git a/apps/mobile/src/features/threads/use-composer-command-menu.test.ts b/apps/mobile/src/features/threads/use-composer-command-menu.test.ts index 14c807c6efee1..8ea9e0bfaef6a 100644 --- a/apps/mobile/src/features/threads/use-composer-command-menu.test.ts +++ b/apps/mobile/src/features/threads/use-composer-command-menu.test.ts @@ -164,6 +164,18 @@ describe("mobile model mentions", () => { }; }; + it("matches Claude models by their short name", () => { + const opus = option("claude:claude-opus", { + label: "Claude flagship", + shortName: "Opus", + providerDriver: "claudeAgent", + providerLabel: "Claude", + }); + expect(buildComposerModelItems([opus], "opus", [provider("claude")])).toEqual([ + expect.objectContaining({ option: opus }), + ]); + }); + it("replaces a model mention with prompt text and places the cursor after it", () => { const modelOption = option("codex:gpt-5.4"); expect( @@ -213,4 +225,17 @@ describe("mobile model mentions", () => { ] as const)("does not suggest a preserved selection whose provider is %s", (_, providers) => { expect(buildComposerModelItems([option("traex:gpt-6.1")], "gpt", providers)).toEqual([]); }); + + it("suggests models from a provider whose health probe only warns", () => { + const opus = option("claude:claude-opus", { + label: "Claude Opus", + providerDriver: "claudeAgent", + providerLabel: "Claude", + }); + expect( + buildComposerModelItems([opus], "opus", [ + provider("claude", { status: "warning", auth: { status: "unknown" } }), + ]), + ).toEqual([expect.objectContaining({ option: opus })]); + }); }); diff --git a/apps/mobile/src/features/threads/use-composer-command-menu.ts b/apps/mobile/src/features/threads/use-composer-command-menu.ts index 6a08333bf965b..4c4d07c4175a7 100644 --- a/apps/mobile/src/features/threads/use-composer-command-menu.ts +++ b/apps/mobile/src/features/threads/use-composer-command-menu.ts @@ -184,7 +184,8 @@ export function buildComposerModelItems( .filter( (provider) => provider.installed && - provider.status === "ready" && + provider.status !== "error" && + provider.status !== "disabled" && provider.auth.status !== "unauthenticated" && provider.availability !== "unavailable", ) @@ -201,6 +202,7 @@ export function buildComposerModelItems( query, (option) => ({ name: option.label, + ...(option.shortName ? { shortName: option.shortName } : {}), ...(option.subtitle ? { subProvider: option.subtitle } : {}), driverKind: option.providerDriver, providerDisplayName: option.providerLabel, diff --git a/apps/mobile/src/lib/modelOptions.test.ts b/apps/mobile/src/lib/modelOptions.test.ts index 717eed3403365..c4d0287316537 100644 --- a/apps/mobile/src/lib/modelOptions.test.ts +++ b/apps/mobile/src/lib/modelOptions.test.ts @@ -929,6 +929,43 @@ describe("synchronized mobile model visibility", () => { }); }); +describe("mobile unavailable provider model options", () => { + it.each([ + ["signed out", { auth: { status: "unauthenticated" as const } }], + ["not checked yet", { installed: false }], + ])("keeps an enabled %s provider listed with unavailable models", (_state, probe) => { + const config = { + providers: [ + { + instanceId: "claudeAgent", + driver: "claudeAgent", + enabled: true, + installed: true, + auth: { status: "authenticated" as const }, + ...probe, + models: [ + { slug: "claude-opus", name: "Claude Opus", isCustom: false, capabilities: null }, + ], + }, + ], + } as unknown as ServerConfig; + + const options = buildModelOptions(config, null, undefined, null, "composer"); + + expect(options).toMatchObject([ + { providerKey: "claudeAgent", providerLabel: "Claude", isUnavailable: true }, + ]); + expect( + resolveNewTaskModelSelection({ + draftSelection: null, + projectDefaultSelection: null, + stickySelection: null, + modelOptions: options, + }), + ).toBeNull(); + }); +}); + describe("mobile account pool model options", () => { const memberId = ProviderInstanceId.make("claudeAgent_work"); const poolId = ProviderInstanceId.make("pool_claudeAgent"); diff --git a/apps/mobile/src/lib/modelOptions.ts b/apps/mobile/src/lib/modelOptions.ts index e08d3640cfb9e..dcfdf1977c692 100644 --- a/apps/mobile/src/lib/modelOptions.ts +++ b/apps/mobile/src/lib/modelOptions.ts @@ -194,9 +194,12 @@ function modelOptionProviderContext( (provider) => matchesLockedProvider(provider) && isProviderEnabledForProject(config, provider, projectId) && - provider.installed && - provider.auth.status !== "unauthenticated" && - !(provider.driver === "antigravity" && provider.availability === "unavailable"), + // Antigravity's catalog only exists once it is set up; other providers stay + // listed as unavailable so a pending or failed probe cannot erase them. + (provider.driver !== "antigravity" || + (provider.installed && + provider.auth.status !== "unauthenticated" && + provider.availability !== "unavailable")), ), }; } @@ -390,6 +393,7 @@ export function buildModelOptions( continue; } const providerLabel = providerDisplayLabel(provider); + const providerUnavailable = !provider.installed || provider.auth.status === "unauthenticated"; const providerBadge = providerBadgeFor(provider.instanceId); const updateRequired = provider.updateRequiredModels?.length ? { driver: provider.driver, updateRequiredModels: provider.updateRequiredModels } @@ -420,6 +424,7 @@ export function buildModelOptions( ...(provider.iconUrl ? { providerIconUrl: provider.iconUrl } : {}), isHidden: !model.isCustom && hiddenModels.has(model.slug), canHide: !model.isCustom, + ...(providerUnavailable ? { isUnavailable: true } : {}), isDefault: model.isDefault === true, isLegacy: model.isLegacy === true, ...(updateRequired ? { providerUpdateRequired: updateRequired } : {}), diff --git a/apps/web/src/components/chat/composerModelMentions.test.ts b/apps/web/src/components/chat/composerModelMentions.test.ts index 6f1f59998edc7..bc61c303dad12 100644 --- a/apps/web/src/components/chat/composerModelMentions.test.ts +++ b/apps/web/src/components/chat/composerModelMentions.test.ts @@ -68,4 +68,10 @@ describe("matchComposerModelItems", () => { match("gpt", { instanceEntries: [{ ...traex, status: "error" as const }, opencode] }), ).toEqual(["opencode:traex/gpt-6.1"]); }); + + it("keeps providers whose health probe only warns", () => { + expect(match("opus", { instanceEntries: [{ ...claude, status: "warning" as const }] })).toEqual( + ["claudeAgent:claude-opus-5-5"], + ); + }); }); diff --git a/apps/web/src/components/chat/composerModelMentions.ts b/apps/web/src/components/chat/composerModelMentions.ts index 770697c23358f..ab7653a7b2a8c 100644 --- a/apps/web/src/components/chat/composerModelMentions.ts +++ b/apps/web/src/components/chat/composerModelMentions.ts @@ -1,7 +1,7 @@ import { rankComposerModelMentions } from "@t3tools/client-runtime/composerModelMentions"; import type { ProviderInstanceId } from "@t3tools/contracts"; -import { isProviderInstancePickerReady, type ProviderInstanceEntry } from "../../providerInstances"; +import type { ProviderInstanceEntry } from "../../providerInstances"; import type { ComposerCommandItem } from "./ComposerCommandMenu"; import { getDisplayModelName, getProviderRowLabel, type ModelEsque } from "./providerIconUtils"; @@ -26,7 +26,16 @@ export function matchComposerModelItems(input: { }): ComposerModelItem[] { const candidates: Array<{ entry: ComposerModelMentionProvider; option: ModelEsque }> = []; for (const entry of input.instanceEntries) { - if (!isProviderInstancePickerReady(entry)) continue; + // Mentions only name a model for the agent; a warning probe (which the server + // still delegates through) must not hide the provider the way an error does. + if ( + !entry.enabled || + !entry.isAvailable || + entry.status === "error" || + entry.status === "disabled" + ) { + continue; + } for (const option of input.modelOptionsByInstance.get(entry.instanceId) ?? []) { // The picker hides legacy models by default; skip them so they don't fill the result cap. if (option.isLegacy || option.isUnavailable) continue; From 1030f63bf052a6bc2083d920d8683896f6511f0d Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Fri, 6 Nov 2026 03:14:07 -0800 Subject: [PATCH 2/2] fix(test): stop scrubbing the mobile test platform flag providerEnv deletes every T3_-prefixed variable, which removes T3_MOBILE_TEST_PLATFORM before tests read it. The mobile Vite config still transformed components for Android, so FilePreviewModal tests took iOS branches against Android-rendered code and failed the Client and package tests job on this PR and on main. Keep the flag, like the other test-only opt-ins. Done with Muse Code (muse-wonder-full-1.0). --- packages/shared/src/testing/providerEnv.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/shared/src/testing/providerEnv.ts b/packages/shared/src/testing/providerEnv.ts index ac07867c20473..7ff440879ca77 100644 --- a/packages/shared/src/testing/providerEnv.ts +++ b/packages/shared/src/testing/providerEnv.ts @@ -5,6 +5,8 @@ const PROVIDER_ENV_PREFIXES = ["ANTHROPIC_", "CLAUDE_", "CODEX_", "OPENAI_", "T3CODE_", "T3_"]; const KEPT_ENV = new Set([ + // Selects the mobile test platform; the Vite config and platform-branched tests must agree on it. + "T3_MOBILE_TEST_PLATFORM", // CI sets these so the transfer budget integration test writes its report for the job summary. "T3CODE_TRANSFER_BUDGET_REPORT_PATH", "T3CODE_TRANSFER_BUDGET_RESULT_PATH",