fix(ui): let restore paths report no effort without pinning Default

`commitVariantSelectionForModel` turned every `undefined` into an explicit
`Default` (`null`), but it serves two kinds of caller. The picker means "the
user chose Default"; the history and manual-override restores mean "nothing
was found". Restoring a session therefore recorded a choice nobody made, and
`resolveModelVariantSelection` collapsed that `null` back to `undefined`, so
the next restore recorded it again. Because an explicit `Default` outranks the
agent and settings defaults by design, the session latched onto `Default` and
the concrete effort its own history carried could not come back.

Move the decision to the callers: the four picker paths pass `variant ?? null`,
the restore paths pass their result through, and the resolver returns the
selection store's three states instead of two. The follow-up write in the
history restore goes with it — the apply above it already recorded the same
agent and model, and a second write could only disagree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZuVVgziiLjD81W5vaxdH2
This commit is contained in:
Iuliia Ivashko
2026-09-04 17:32:40 +03:00
co-authored by Claude Opus 5
parent cb3bc3bd1b
commit 025cd3d46c
2 changed files with 47 additions and 23 deletions
@@ -725,7 +725,14 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
return candidates.find((candidate) => candidate !== undefined && variantOptions.includes(candidate));
}, [agents, currentAgentName, currentModelId, currentProviderId, currentSessionId, currentVariantSelection, effectiveCurrentVariant, getModelVariantOptions, settingsDefaultVariant, uiAgentName]);
const resolveModelVariantSelection = React.useCallback((providerId: string, modelId: string) => {
/**
* The session's recorded choice for this model, in the selection store's
* three states: an effort name, `null` for an explicit "Default", and
* `undefined` for no choice at all. Collapsing `null` into `undefined` here
* would hand a real "Default" back to the callers as "nothing chosen", and
* they would re-record it as a choice on the next write.
*/
const resolveModelVariantSelection = React.useCallback((providerId: string, modelId: string): string | null | undefined => {
const variantOptions = getModelVariantOptions(providerId, modelId);
if (variantOptions.length === 0) {
return undefined;
@@ -736,7 +743,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
const savedVariant = getAgentModelVariantForSession(currentSessionId, effectiveAgentName, providerId, modelId);
// An explicit "Default" is a choice: it stops the fallbacks below.
if (savedVariant === null) {
return undefined;
return null;
}
if (savedVariant && variantOptions.includes(savedVariant)) {
return savedVariant;
@@ -770,7 +777,16 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
return liveConfigAgentName || currentAgentName;
}, [currentAgentName, currentSessionId]);
const commitVariantSelectionForModel = React.useCallback((providerId: string, modelId: string, variant: string | undefined, agentNameOverride?: string | null) => {
/**
* Records `variant` as this session's effort for the model, in the same
* three states the selection store defines: an effort name, `null` for an
* explicit "Default", `undefined` for no choice so the inherited effort
* applies. Callers decide which one they mean — the picker turns its own
* "Default" into `null`, while restore paths pass `undefined` through when
* they found nothing, because "the history carries no effort" is not the
* user having chosen "Default".
*/
const commitVariantSelectionForModel = React.useCallback((providerId: string, modelId: string, variant: string | null | undefined, agentNameOverride?: string | null) => {
const variantOptions = getModelVariantOptions(providerId, modelId);
if (variantOptions.length === 0) {
manualVariantSelectionRef.current = false;
@@ -780,16 +796,14 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
manualVariantSelectionRef.current = true;
setCurrentVariantOverride(
variant ?? null,
variant,
resolveInheritedVariantForModel(providerId, modelId, agentNameOverride),
);
addRecentEffort(providerId, modelId, variant);
addRecentEffort(providerId, modelId, variant ?? undefined);
const effectiveAgentName = agentNameOverride ?? resolveLiveAgentName();
if (currentSessionId && effectiveAgentName) {
// `null`, not `undefined`: picking "Default" is a choice to record,
// not the absence of one.
saveAgentModelVariantForSession(currentSessionId, effectiveAgentName, providerId, modelId, variant ?? null);
saveAgentModelVariantForSession(currentSessionId, effectiveAgentName, providerId, modelId, variant);
}
}, [
addRecentEffort,
@@ -802,7 +816,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
setCurrentVariantOverride,
]);
const applyModelSelectionWithVariant = React.useCallback((providerId: string, modelId: string, variant: string | undefined, agentNameOverride?: string | null) => {
const applyModelSelectionWithVariant = React.useCallback((providerId: string, modelId: string, variant: string | null | undefined, agentNameOverride?: string | null) => {
const effectiveAgentName = agentNameOverride ?? resolveLiveAgentName() ?? undefined;
const result = tryApplyModelSelection(providerId, modelId, effectiveAgentName);
if (result !== 'applied') {
@@ -874,15 +888,11 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
return;
}
// The effort is not written again here: `applyModelSelectionWithVariant`
// above already recorded `historicalVariant` for this same agent and
// model, and a second write can only disagree with the first.
if (latestLoadedUserChoice.agent) {
saveSessionAgentSelection(currentSessionId, latestLoadedUserChoice.agent);
saveAgentModelVariantForSession(
currentSessionId,
latestLoadedUserChoice.agent,
latestLoadedUserChoice.providerID,
latestLoadedUserChoice.modelID,
historicalVariant,
);
}
saveSessionModelSelection(currentSessionId, latestLoadedUserChoice.providerID, latestLoadedUserChoice.modelID);
latestLoadedUserChoiceRestoreRef.current = restoreKey;
@@ -900,7 +910,6 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
getSessionModelSelection,
resolveModelVariantSelection,
saveSessionAgentSelection,
saveAgentModelVariantForSession,
saveSessionModelSelection,
]);
@@ -1184,7 +1193,9 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
const handleVariantSelect = React.useCallback((variant: string | undefined) => {
if (currentProviderId && currentModelId) {
commitVariantSelectionForModel(currentProviderId, currentModelId, variant);
// Picked in the effort menu, so no effort means the user picked
// "Default" — a choice, recorded as `null`.
commitVariantSelectionForModel(currentProviderId, currentModelId, variant ?? null);
}
}, [commitVariantSelectionForModel, currentModelId, currentProviderId]);
@@ -1246,8 +1257,10 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
) => {
try {
const effectiveAgentName = options?.agentName ?? resolveLiveAgentName() ?? undefined;
// `applyVariant` is only set when the user adjusted the effort in
// the model picker, so no effort means an explicit "Default".
const result = options?.applyVariant
? applyModelSelectionWithVariant(providerId, modelId, options.variant, effectiveAgentName)
? applyModelSelectionWithVariant(providerId, modelId, options.variant ?? null, effectiveAgentName)
: tryApplyModelSelection(providerId, modelId, effectiveAgentName);
if (result !== 'applied') {
if (result === 'provider-missing') {
@@ -1604,8 +1617,10 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
}
}
const handleMobileModelApply = (providerId: string, modelId: string, variant: string | undefined) => {
const result = applyModelSelectionWithVariant(providerId, modelId, variant);
const handleMobileModelApply = (providerId: string, modelId: string, variant: string | null | undefined) => {
// Chosen in the mobile model sheet, and the row already showed this
// effort: no effort there means the user is applying "Default".
const result = applyModelSelectionWithVariant(providerId, modelId, variant ?? null);
if (result !== 'applied') {
if (result === 'provider-missing') {
console.error('[ModelControls] Provider not available for selection:', providerId);
@@ -1642,7 +1657,8 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
const variantOptions = getModelVariantOptions(providerId, modelId);
const hasVariants = variantOptions.length > 0;
const resolvedVariant = resolveModelVariantSelection(providerId, modelId);
const variantLabel = hasVariants ? formatEffortLabel(resolvedVariant) : null;
// Both an explicit "Default" and no choice at all read as "Default".
const variantLabel = hasVariants ? formatEffortLabel(resolvedVariant ?? undefined) : null;
const isExpanded = expandedMobileModelKey === rowKey;
const inlineVariantOptions = [undefined, ...variantOptions].slice(0, MAX_INLINE_MOBILE_VARIANT_OPTIONS);
const hasVariantOverflow = inlineVariantOptions.length < variantOptions.length + 1;
@@ -1947,7 +1963,8 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
};
const handleSelect = (variant: string | undefined) => {
const result = applyModelSelectionWithVariant(targetProviderId, targetModelId, variant);
// Chosen in the mobile effort panel: no effort means "Default".
const result = applyModelSelectionWithVariant(targetProviderId, targetModelId, variant ?? null);
if (result !== 'applied') {
return;
}
+7
View File
@@ -232,6 +232,13 @@ per-session selection store records an explicit `Default` (as `null`) instead of
clearing the entry — a cleared entry is indistinguishable from never having
chosen, and the settings default wins again on the next agent or session switch.
Only a place where the user chose may write `null`. Restore paths — message
history, a preserved manual override — pass their own "found nothing" through
as `undefined`, because a session whose history carries no effort is not a
session where `Default` was picked. A restore that manufactures `null` latches
the session onto `Default`: `null` outranks the agent and settings defaults by
design, so the concrete effort it displaced can never come back.
Every write of `currentVariant` writes `currentVariantSelection` with it. They
are one selection; updating only the effective value leaves the picker showing
one effort while sends carry another.