diff --git a/packages/desktop/src/renderer/components/agent/AcpModelSelector.tsx b/packages/desktop/src/renderer/components/agent/AcpModelSelector.tsx
index 52b048d3553..18d98f5fc90 100644
--- a/packages/desktop/src/renderer/components/agent/AcpModelSelector.tsx
+++ b/packages/desktop/src/renderer/components/agent/AcpModelSelector.tsx
@@ -81,7 +81,12 @@ const AcpModelSelector: React.FC<{
defaultModelLabel,
fallbackLabel: t('conversation.welcome.useCliModel'),
});
- const combinedLabel = composeRuntimeSelectorLabel({ modelLabel: display_label, thoughtLevel });
+ const defaultThoughtLevelLabel = t('common.default');
+ const combinedLabel = composeRuntimeSelectorLabel({
+ modelLabel: display_label,
+ thoughtLevel,
+ defaultThoughtLevelLabel,
+ });
const isRuntimeSetting = isConfigSetting(setStatus);
const handleThoughtLevelSelect = useCallback(
async (value: string) => {
@@ -171,7 +176,7 @@ const AcpModelSelector: React.FC<{
title={
}
>
diff --git a/packages/desktop/src/renderer/components/agent/runtimeSelectorOptions.tsx b/packages/desktop/src/renderer/components/agent/runtimeSelectorOptions.tsx
index 733e312c78c..9e934ce3b9e 100644
--- a/packages/desktop/src/renderer/components/agent/runtimeSelectorOptions.tsx
+++ b/packages/desktop/src/renderer/components/agent/runtimeSelectorOptions.tsx
@@ -24,23 +24,39 @@ export type RuntimeSelectorModelGroup = { key: string; title: string; models: Ru
const matchesModelQuery = (model: RuntimeSelectorModel, keyword: string): boolean =>
(model.label || model.id).toLowerCase().includes(keyword);
-export const getCurrentThoughtLevelLabel = (thoughtLevel: AcpDerivedOption | null | undefined): string => {
+/** Structural subset both AcpDerivedOption and AgentRuntimeDerivedOption satisfy. */
+type ThoughtLevelLike = Pick & { currentValue?: string | null };
+
+/**
+ * Resolve the display label for the ACTIVE thinking level. The backend's
+ * `current_value` is the single source of truth: a known value maps to its
+ * option label (or itself). When the axis exists but no current is known,
+ * return `defaultLabel` (the caller passes the localized "Default") — an
+ * honest neutral, never a guess like `options[0]`, which may not be what the
+ * backend actually runs. No axis at all → empty (no suffix).
+ */
+export const getCurrentThoughtLevelLabel = (
+ thoughtLevel: ThoughtLevelLike | null | undefined,
+ defaultLabel = ''
+): string => {
if (!thoughtLevel) return '';
+ if (!thoughtLevel.currentValue) return defaultLabel;
return (
- thoughtLevel.options.find((item) => item.value === thoughtLevel.currentValue)?.label ||
- thoughtLevel.currentValue ||
- ''
+ thoughtLevel.options.find((item) => item.value === thoughtLevel.currentValue)?.label || thoughtLevel.currentValue
);
};
export const composeRuntimeSelectorLabel = ({
modelLabel,
thoughtLevel,
+ defaultThoughtLevelLabel,
}: {
modelLabel: string;
- thoughtLevel?: AcpDerivedOption | null;
+ thoughtLevel?: ThoughtLevelLike | null;
+ /** Localized "Default" shown when the thought axis exists but no current is known. */
+ defaultThoughtLevelLabel?: string;
}): string => {
- const thoughtLevelLabel = getCurrentThoughtLevelLabel(thoughtLevel);
+ const thoughtLevelLabel = getCurrentThoughtLevelLabel(thoughtLevel, defaultThoughtLevelLabel);
if (!thoughtLevelLabel) return modelLabel;
return `${modelLabel} · ${thoughtLevelLabel}`;
};
diff --git a/packages/desktop/src/renderer/hooks/agent/useAcpConfigOptions.ts b/packages/desktop/src/renderer/hooks/agent/useAcpConfigOptions.ts
index 2131fd9614b..a3d5b53f024 100644
--- a/packages/desktop/src/renderer/hooks/agent/useAcpConfigOptions.ts
+++ b/packages/desktop/src/renderer/hooks/agent/useAcpConfigOptions.ts
@@ -76,6 +76,59 @@ export function deriveSelectOption(
};
}
+/**
+ * Fallback option ids for the thought-level axis, matched only when no option
+ * carries `category: 'thought_level'` (the category stays authoritative).
+ * Covers every id the known backends emit: our Core's `reasoning_effort`, the
+ * legacy ACP aliases `effort`/`thinking_budget`, and `thinking` (also the id
+ * upstream PR #3597 matches, so the two changes stay compatible).
+ */
+export const THOUGHT_LEVEL_FALLBACK_IDS = [
+ 'thought_level',
+ 'reasoning_effort',
+ 'effort',
+ 'thinking',
+ 'thinking_budget',
+];
+
+/**
+ * Anti-flicker merge for whole-snapshot replaces (`acp_config_option` push /
+ * REST reload): keep a known non-null `current_value` when the incoming frame
+ * carries NO current information at all.
+ *
+ * A frame where EVERY option's `current_value` is null is "informationless" —
+ * the backend simply had nothing selected to report yet (e.g. an early catalog
+ * push before its currents landed, or an older Core that never stamped
+ * currents) — and letting it clobber a current the UI already observed makes
+ * the picker flash Model-only. For those frames the previous per-option
+ * current is preserved (matched by category, then id).
+ *
+ * A frame with AT LEAST ONE non-null current is an informed snapshot: its
+ * nulls are authoritative and pass through. This is what keeps the Core's
+ * reject re-push working — after a backend refuses an effort set, the
+ * corrected frame still carries the model current, so its effort null WIPES
+ * the stale highlight instead of being "protected".
+ */
+export function mergeSnapshotPreservingKnownCurrents(
+ previous: AcpConfigOptionDto[] | null | undefined,
+ next: AcpConfigOptionDto[]
+): AcpConfigOptionDto[] {
+ if (!previous?.length) return next;
+ const informed = next.some((option) => option.current_value != null);
+ if (informed) return next;
+ return next.map((option) => {
+ if (option.current_value != null) return option;
+ const prior = previous.find((candidate) =>
+ option.category ? candidate.category === option.category : candidate.id === option.id
+ );
+ if (prior?.current_value == null) return option;
+ // Only revive a current the incoming option can still represent — a stale
+ // value outside the new choice list would be its own lie.
+ const stillSelectable = option.options?.some((choice) => choice.value === prior.current_value);
+ return stillSelectable ? { ...option, current_value: prior.current_value } : option;
+ });
+}
+
export function hasObservedValue(
response: SetConfigOptionResponse,
optionId: string,
@@ -200,8 +253,9 @@ export function useAcpConfigOptions({
const replaceSnapshot = useCallback(
(next: AcpConfigOptionDto[]) => {
- optionsRef.current = next;
- void mutate(next, false);
+ const merged = mergeSnapshotPreservingKnownCurrents(optionsRef.current, next);
+ optionsRef.current = merged;
+ void mutate(merged, false);
},
[mutate]
);
@@ -276,7 +330,7 @@ export function useAcpConfigOptions({
setStatus,
mode: deriveSelectOption(configOptions, 'mode', ['mode']),
model: deriveSelectOption(configOptions, 'model', ['model']),
- thoughtLevel: deriveSelectOption(configOptions, 'thought_level', ['thought_level', 'reasoning_effort']),
+ thoughtLevel: deriveSelectOption(configOptions, 'thought_level', THOUGHT_LEVEL_FALLBACK_IDS),
reload,
setConfigOption,
};
diff --git a/packages/desktop/src/renderer/pages/conversation/platforms/aionrs/AionrsModelSelector.tsx b/packages/desktop/src/renderer/pages/conversation/platforms/aionrs/AionrsModelSelector.tsx
index b688ee6234b..73ed75e22d9 100644
--- a/packages/desktop/src/renderer/pages/conversation/platforms/aionrs/AionrsModelSelector.tsx
+++ b/packages/desktop/src/renderer/pages/conversation/platforms/aionrs/AionrsModelSelector.tsx
@@ -77,7 +77,8 @@ const AionrsModelSelector: React.FC<{
defaultModelLabel,
fallbackLabel: t('conversation.welcome.selectModel'),
});
- const combinedLabel = composeRuntimeSelectorLabel({ modelLabel: label, thoughtLevel });
+ const defaultThoughtLevelLabel = t('common.default');
+ const combinedLabel = composeRuntimeSelectorLabel({ modelLabel: label, thoughtLevel, defaultThoughtLevelLabel });
const handleThoughtLevelSelect = (value: string) => {
if (!thoughtLevel || value === thoughtLevel.currentValue || !onSetThoughtLevel) return;
void onSetThoughtLevel(thoughtLevel.id, value);
@@ -135,7 +136,7 @@ const AionrsModelSelector: React.FC<{
title={
}
>
diff --git a/packages/desktop/src/renderer/pages/guid/GuidPage.tsx b/packages/desktop/src/renderer/pages/guid/GuidPage.tsx
index 3e700e6761b..335e7467c3b 100644
--- a/packages/desktop/src/renderer/pages/guid/GuidPage.tsx
+++ b/packages/desktop/src/renderer/pages/guid/GuidPage.tsx
@@ -445,10 +445,13 @@ const GuidPage: React.FC = () => {
if (resolvedDefaults.thoughtLevel && availableThoughtLevelValues.has(resolvedDefaults.thoughtLevel)) {
agentSelection.setSelectedThoughtLevelValue(resolvedDefaults.thoughtLevel, { persistPreference: false });
} else {
- const fallbackThoughtLevel =
- agentSelection.currentThoughtLevelOption.currentValue ||
- agentSelection.currentThoughtLevelOption.options[0]?.value ||
- '';
+ // No resolved default: mirror the backend current if it reported one,
+ // otherwise leave the selection empty (`''`). Falling back to
+ // `options[0]` here re-seeded the implicit override that the send path
+ // then sent as an explicit `thought_level`, silently defeating the
+ // assistant's backend default — the exact behavior this change removes
+ // (matches useGuidAssistantSelection's `''` fallback).
+ const fallbackThoughtLevel = agentSelection.currentThoughtLevelOption.currentValue || '';
agentSelection.setSelectedThoughtLevelValue(fallbackThoughtLevel, { persistPreference: false });
}
}
diff --git a/packages/desktop/src/renderer/pages/guid/components/GuidModelSelector.tsx b/packages/desktop/src/renderer/pages/guid/components/GuidModelSelector.tsx
index db02327eed6..729299beb65 100644
--- a/packages/desktop/src/renderer/pages/guid/components/GuidModelSelector.tsx
+++ b/packages/desktop/src/renderer/pages/guid/components/GuidModelSelector.tsx
@@ -99,17 +99,18 @@ const GuidModelSelector: React.FC = ({
fallbackLabel: defaultModelLabel,
});
}, [acpSelectedLabel, currentAcpCachedModelInfo?.current_model_id, defaultModelLabel, selectedAcpModel]);
- const selectedThoughtLevelValue = thoughtLevelOption?.currentValue || thoughtLevelOption?.options[0]?.value || '';
- const normalizedThoughtLevelOption =
- thoughtLevelOption && thoughtLevelOption.options.length > 0
- ? {
- ...thoughtLevelOption,
- currentValue: selectedThoughtLevelValue || null,
- }
- : null;
+ // The thought-level current is HONEST: only a real known value (user pick or
+ // backend-reported current) highlights; an unknown current renders as the
+ // localized "Default" instead of pretending options[0] is active — the
+ // backend resolves the actual default (assistant fixed default / its own
+ // launch default), and options[0] may not be it.
+ const defaultThoughtLevelLabel = t('common.default');
+ const visibleThoughtLevelOption =
+ thoughtLevelOption && thoughtLevelOption.options.length > 0 ? thoughtLevelOption : null;
const combinedAcpButtonLabel = composeRuntimeSelectorLabel({
modelLabel: acpButtonLabel,
- thoughtLevel: normalizedThoughtLevelOption,
+ thoughtLevel: visibleThoughtLevelOption,
+ defaultThoughtLevelLabel,
});
if (isGeminiMode) {
@@ -208,7 +209,7 @@ const GuidModelSelector: React.FC = ({
trigger='click'
droplist={