Skip to content

Commit 9a861a7

Browse files
authored
fix(chat): model selection + multi-model follow-up correctness (onyx-dot-app#10075)
1 parent b4bc12f commit 9a861a7

4 files changed

Lines changed: 49 additions & 14 deletions

File tree

web/src/app/app/message/MultiModelResponseView.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,8 +210,10 @@ export default function MultiModelResponseView({
210210
const response = responses.find((r) => r.modelIndex === modelIndex);
211211
if (!response) return;
212212

213-
// Persist preferred response to backend + update local tree so the
214-
// input bar unblocks (awaitingPreferredSelection clears).
213+
// Persist preferred response + sync `latestChildNodeId`. Backend's
214+
// `set_preferred_response` updates `latest_child_message_id`; if the
215+
// frontend chain walk disagrees, the next follow-up fails with
216+
// "not on the latest mainline".
215217
if (parentMessage?.messageId && response.messageId && currentSessionId) {
216218
setPreferredResponse(parentMessage.messageId, response.messageId).catch(
217219
(err) => console.error("Failed to persist preferred response:", err)
@@ -227,6 +229,7 @@ export default function MultiModelResponseView({
227229
updated.set(parentMessage.nodeId, {
228230
...userMsg,
229231
preferredResponseId: response.messageId,
232+
latestChildNodeId: response.nodeId,
230233
});
231234
updateSessionMessageTree(currentSessionId, updated);
232235
}

web/src/lib/hooks.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,25 @@ export function useLlmManager(
694694
prevAgentIdRef.current = liveAgent?.id;
695695
}, [liveAgent?.id]);
696696

697+
// Clear manual override when arriving at a *different* existing session
698+
// from any previously-seen defined session. Tracks only the last
699+
// *defined* session id so a round-trip through new-chat (A → undefined
700+
// → B) still resets, while A → undefined (new-chat) preserves it.
701+
const prevDefinedSessionIdRef = useRef<string | undefined>(undefined);
702+
useEffect(() => {
703+
const nextId = currentChatSession?.id;
704+
if (
705+
nextId !== undefined &&
706+
prevDefinedSessionIdRef.current !== undefined &&
707+
nextId !== prevDefinedSessionIdRef.current
708+
) {
709+
setUserHasManuallyOverriddenLLM(false);
710+
}
711+
if (nextId !== undefined) {
712+
prevDefinedSessionIdRef.current = nextId;
713+
}
714+
}, [currentChatSession?.id]);
715+
697716
function getValidLlmDescriptor(
698717
modelName: string | null | undefined
699718
): LlmDescriptor {
@@ -715,8 +734,9 @@ export function useLlmManager(
715734

716735
if (llmProviders === undefined || llmProviders === null) {
717736
resolved = manualLlm;
718-
} else if (userHasManuallyOverriddenLLM && !currentChatSession) {
719-
// User has overridden in this session and switched to a new session
737+
} else if (userHasManuallyOverriddenLLM) {
738+
// Manual override wins over session's `current_alternate_model`.
739+
// Cleared on cross-session navigation by the effect above.
720740
resolved = manualLlm;
721741
} else if (currentChatSession?.current_alternate_model) {
722742
resolved = getValidLlmDescriptorForProviders(
@@ -728,8 +748,6 @@ export function useLlmManager(
728748
liveAgent.llm_model_version_override,
729749
llmProviders
730750
);
731-
} else if (userHasManuallyOverriddenLLM) {
732-
resolved = manualLlm;
733751
} else if (user?.preferences?.default_model) {
734752
resolved = getValidLlmDescriptorForProviders(
735753
user.preferences.default_model,

web/src/refresh-components/popovers/ModelSelector.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,9 +159,12 @@ export default function ModelSelector({
159159
);
160160

161161
if (!isMultiModel) {
162+
// Stable key — keying on model would unmount the pill
163+
// on change and leave Radix's anchorRef detached,
164+
// flashing the closing popover at (0,0).
162165
return (
163166
<OpenButton
164-
key={modelKey(model.provider, model.modelName)}
167+
key="single-model-pill"
165168
icon={ProviderIcon}
166169
onClick={(e: React.MouseEvent) =>
167170
handlePillClick(index, e.currentTarget as HTMLElement)

web/src/refresh-pages/AppPage.tsx

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -425,16 +425,27 @@ export default function AppPage({ firstMessage }: ChatPageProps) {
425425
// eslint-disable-next-line react-hooks/exhaustive-deps
426426
}, [multiModel.isMultiModelActive]);
427427

428-
// Sync single-model selection to llmManager so the submission path
429-
// uses the correct provider/version (replaces the old LLMPopover sync).
428+
// Sync single-model selection to llmManager so the submission path uses
429+
// the correct provider/version. Guard against echoing derived state back
430+
// — only call updateCurrentLlm when the selection actually differs from
431+
// currentLlm, otherwise the initial [] → [currentLlmModel] sync would
432+
// pin `userHasManuallyOverriddenLLM=true` with whatever was resolved
433+
// first (often the default model before the session's alt_model loads).
430434
useEffect(() => {
431435
if (multiModel.selectedModels.length === 1) {
432436
const model = multiModel.selectedModels[0]!;
433-
llmManager.updateCurrentLlm({
434-
name: model.name,
435-
provider: model.provider,
436-
modelName: model.modelName,
437-
});
437+
const current = llmManager.currentLlm;
438+
if (
439+
model.provider !== current.provider ||
440+
model.modelName !== current.modelName ||
441+
model.name !== current.name
442+
) {
443+
llmManager.updateCurrentLlm({
444+
name: model.name,
445+
provider: model.provider,
446+
modelName: model.modelName,
447+
});
448+
}
438449
}
439450
}, [multiModel.selectedModels]);
440451

0 commit comments

Comments
 (0)