Skip to content

Commit 16df63c

Browse files
Merge pull request #2 from ScriptedAlchemy/codex/provider-reliability-root-causes
fix(providers): recover update failures and preserve PR watches
2 parents a31728f + 4994b5e commit 16df63c

14 files changed

Lines changed: 863 additions & 110 deletions

File tree

‎apps/mobile/src/features/threads/ThreadSettingsSheet.tsx‎

Lines changed: 37 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,9 @@ import { ProviderIcon } from "../../components/ProviderIcon";
5555
import { ThemedSwitch } from "../../components/ui/ThemedSwitch";
5656
import { cn } from "../../lib/cn";
5757
import {
58+
buildProviderUpdateRequiredGroups,
5859
buildModelOptions,
59-
groupByProvider,
60+
groupModelOptionsWithProviderNotices,
6061
type ModelOption,
6162
type ProviderGroup,
6263
} from "../../lib/modelOptions";
@@ -95,11 +96,10 @@ import {
9596
} from "./thread-settings-options";
9697
import {
9798
canCommitPendingModel,
98-
favoritesFirst,
9999
modelFavoriteKey,
100-
modelMatchesCatalogQuery,
101100
pendingModelAfterPress,
102101
providerSectionIsCollapsed,
102+
visibleModelsForCatalog,
103103
} from "./thread-settings-sheet-state";
104104
import { formatProviderUpdateRequiredNotice } from "@t3tools/client-runtime/providerUpdateRequiredModels";
105105

@@ -358,19 +358,26 @@ function ThreadSettingsSessionProvider(
358358
new Map<string, { favorite: boolean; settings: ServerSettings; pending: boolean }>(),
359359
);
360360
const [pendingHiddenKeys, setPendingHiddenKeys] = useState<ReadonlySet<string>>(() => new Set());
361-
const providerGroups =
361+
const modelOptions =
362362
manageHidden && config
363-
? groupByProvider(
364-
buildModelOptions(
365-
config,
366-
props.selectedModel,
367-
props.lockedProviderInstanceId,
368-
props.projectId,
369-
"composer",
370-
true,
371-
),
363+
? buildModelOptions(
364+
config,
365+
props.selectedModel,
366+
props.lockedProviderInstanceId,
367+
props.projectId,
368+
"composer",
369+
true,
372370
)
373-
: props.providerGroups;
371+
: props.providerGroups.flatMap((group) => group.models);
372+
const providerGroups = groupModelOptionsWithProviderNotices(
373+
modelOptions,
374+
buildProviderUpdateRequiredGroups(
375+
config,
376+
props.lockedProviderInstanceId,
377+
props.projectId,
378+
"composer",
379+
),
380+
);
374381
const legacyFavorites = AsyncResult.isSuccess(preferences)
375382
? (preferences.value.modelFavorites ?? EMPTY_MODEL_FAVORITES)
376383
: EMPTY_MODEL_FAVORITES;
@@ -762,29 +769,18 @@ function buildThreadSettingsCatalogItems(
762769
) {
763770
return [];
764771
}
765-
const driver = group.models[0]?.providerDriver ?? group.providerKey;
766-
const catalogModels =
767-
session.manageHidden ||
768-
session.showLegacy ||
769-
session.providerFilter === FAVORITES_PROVIDER_FILTER
770-
? group.models
771-
: group.models.filter(
772-
(model) =>
773-
!model.isLegacy || session.isDisplayed(model) || session.favoriteKeys.has(model.key),
774-
);
775-
const visibleModels = favoritesFirst(
776-
catalogModels.filter(
777-
(model) =>
778-
(session.providerFilter !== FAVORITES_PROVIDER_FILTER ||
779-
session.favoriteKeys.has(model.key)) &&
780-
modelMatchesCatalogQuery({
781-
model,
782-
providerLabel: group.providerLabel,
783-
query: session.searchQuery,
784-
}),
785-
),
786-
session.favoriteKeys,
787-
);
772+
const driver =
773+
group.models[0]?.providerDriver ?? group.updateRequired?.driver ?? group.providerKey;
774+
const visibleModels = visibleModelsForCatalog({
775+
models: group.models,
776+
providerLabel: group.providerLabel,
777+
query: session.searchQuery,
778+
manageHidden: session.manageHidden,
779+
showLegacy: session.showLegacy,
780+
favoritesOnly: session.providerFilter === FAVORITES_PROVIDER_FILTER,
781+
favoriteKeys: session.favoriteKeys,
782+
isDisplayed: session.isDisplayed,
783+
});
788784
const updateRequiredNotice =
789785
group.updateRequired && session.providerFilter !== FAVORITES_PROVIDER_FILTER
790786
? formatProviderUpdateRequiredNotice(group.updateRequired, session.searchQuery)
@@ -797,7 +793,10 @@ function buildThreadSettingsCatalogItems(
797793
const isNarrowed = session.providerFilter !== null || session.searchQuery.trim().length > 0;
798794
const collapsible = !isNarrowed;
799795
const collapsed = providerSectionIsCollapsed({
800-
defaultExpanded: isPrimary || containsAppliedSelection,
796+
defaultExpanded:
797+
isPrimary ||
798+
containsAppliedSelection ||
799+
(visibleModels.length === 0 && updateRequiredNotice !== null),
801800
hasExpansionOverride: session.providerExpansionOverrides.has(group.providerKey),
802801
isNarrowed,
803802
});

‎apps/mobile/src/features/threads/thread-settings-sheet-state.test.ts‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ import {
1111
rememberModelOptions,
1212
withRememberedModelOptions,
1313
} from "../../state/use-model-option-memory";
14-
import { canCommitPendingModel, pendingModelAfterPress } from "./thread-settings-sheet-state";
14+
import {
15+
canCommitPendingModel,
16+
pendingModelAfterPress,
17+
visibleModelsForCatalog,
18+
} from "./thread-settings-sheet-state";
1519

1620
function modelOption(
1721
model: string,
@@ -69,6 +73,23 @@ afterEach(() => {
6973
});
7074

7175
describe("thread settings sheet state", () => {
76+
it("includes a matching legacy model in search while legacy models are hidden", () => {
77+
const legacyModel = { ...modelOption("gpt-5.4"), isLegacy: true };
78+
79+
expect(
80+
visibleModelsForCatalog({
81+
models: [legacyModel],
82+
providerLabel: "Codex",
83+
query: "gpt-5.4",
84+
manageHidden: false,
85+
showLegacy: false,
86+
favoritesOnly: false,
87+
favoriteKeys: new Set(),
88+
isDisplayed: () => false,
89+
}),
90+
).toEqual([legacyModel]);
91+
});
92+
7293
it("preserves in-progress edits when the staged model is pressed again", () => {
7394
rememberModelOptions("codex", "gpt-next", [{ id: "effort", value: "medium" }]);
7495
const pending = modelOption("gpt-next", [{ id: "effort", value: "high" }]);

‎apps/mobile/src/features/threads/thread-settings-sheet-state.ts‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,38 @@ export function modelMatchesCatalogQuery(input: {
3535
].some((value) => value.toLocaleLowerCase().includes(query));
3636
}
3737

38+
export function visibleModelsForCatalog(input: {
39+
readonly models: ReadonlyArray<ModelOption>;
40+
readonly providerLabel: string;
41+
readonly query: string;
42+
readonly manageHidden: boolean;
43+
readonly showLegacy: boolean;
44+
readonly favoritesOnly: boolean;
45+
readonly favoriteKeys: ReadonlySet<string>;
46+
readonly isDisplayed: (model: ModelOption) => boolean;
47+
}): ReadonlyArray<ModelOption> {
48+
const catalogModels =
49+
input.manageHidden || input.showLegacy || input.favoritesOnly || input.query.trim().length > 0
50+
? input.models
51+
: input.models.filter(
52+
(model) =>
53+
!model.isLegacy || input.isDisplayed(model) || input.favoriteKeys.has(model.key),
54+
);
55+
56+
return favoritesFirst(
57+
catalogModels.filter(
58+
(model) =>
59+
(!input.favoritesOnly || input.favoriteKeys.has(model.key)) &&
60+
modelMatchesCatalogQuery({
61+
model,
62+
providerLabel: input.providerLabel,
63+
query: input.query,
64+
}),
65+
),
66+
input.favoriteKeys,
67+
);
68+
}
69+
3870
export function pendingModelAfterPress(input: {
3971
readonly current: ModelOption | null;
4072
readonly pressed: ModelOption;

‎apps/mobile/src/lib/modelOptions.test.ts‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@ import {
1111
} from "@t3tools/contracts";
1212

1313
import {
14+
buildProviderUpdateRequiredGroups,
1415
buildModelOptions,
1516
describeUnavailableModelSelection,
1617
groupByProvider,
18+
groupModelOptionsWithProviderNotices,
1719
isModelSelectionUnavailable,
1820
resolveDefaultableModelSelection,
1921
resolveNewTaskModelSelection,
@@ -22,6 +24,45 @@ import {
2224
} from "./modelOptions";
2325

2426
describe("mobile model options", () => {
27+
it("keeps provider update notices when no model rows are visible", () => {
28+
const updateRequiredModels = [
29+
{
30+
slug: "claude-next",
31+
name: "Claude Next",
32+
minVersion: "2.0.0",
33+
},
34+
];
35+
const config = {
36+
providers: [
37+
{
38+
instanceId: "claudeAgent_work",
39+
driver: "claudeAgent",
40+
displayName: "Claude Work",
41+
enabled: true,
42+
installed: true,
43+
auth: { status: "authenticated" },
44+
models: [],
45+
updateRequiredModels,
46+
},
47+
],
48+
} as unknown as ServerConfig;
49+
const options = buildModelOptions(config, null, undefined, null, "composer");
50+
const providerNotices = buildProviderUpdateRequiredGroups(config, undefined, null, "composer");
51+
52+
expect(options).toEqual([]);
53+
expect(groupModelOptionsWithProviderNotices(options, providerNotices)).toEqual([
54+
{
55+
providerKey: "claudeAgent_work",
56+
providerLabel: "Claude Work",
57+
models: [],
58+
updateRequired: {
59+
driver: "claudeAgent",
60+
updateRequiredModels,
61+
},
62+
},
63+
]);
64+
});
65+
2566
it("distinguishes same-name OpenCode models without changing their routing", () => {
2667
const sources = [
2768
{ id: "anthropic", label: "Anthropic" },

0 commit comments

Comments
 (0)