From feadfd5c1b1e8ac0dae42bad5a9c0ab5fa68a1ce Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 19 May 2026 06:56:30 -0700 Subject: [PATCH] studio/frontend: compare composer blocks send when no model picked (#5574) * studio/frontend: compare composer blocks send when no model picked Closes the racing-handle half of #5569. In Compare mode (GeneralCompare shell with model1/model2 props), if the user sends a prompt before picking models in either pane, the SharedComposer used to fall through to the per-handle append branch. Both panes then raced createOpenAIStreamAdapter -> autoLoadSmallestModel, one won, the other dispatched into an unloaded slot and produced an empty bubble with a 1000000.0 tok/s readout. The per-pane picker state never observed the global checkpoint change either, so both pickers stayed at "Select model". Add a guard before the content build: when handlesRef has model1/model2 keys but both selections are empty, surface a toast asking the user to pick models first, leave the text in the composer for retry, and never enter the racing dispatch path. Keeps the per-pane picker state as the source of truth for which model is on each side. The unphysical tok/s readout that the same path produced is separately covered by PR #5570 (display guard). * studio/frontend: tighten compare-mode guard to require both panes Review feedback on #5574: - Gemini: the redundant `model1 !== undefined && model2 !== undefined` checks let the racing-handle dispatch slip through whenever the Compare props arrive as undefined, which is the exact case the guard is trying to block. - Codex: with `isGeneralizedCompare` keyed on `model1?.id || model2?.id`, a half-selected Compare (one model picked, one empty) still falls into the generalized branch. The composer clears, the empty pane gets the user message appended, and `startRun` only fires for the side with an id, leaving the empty pane with a dangling prompt and no response. Switch `isGeneralizedCompare` to require BOTH panes (`&&`), drop the undefined gate, and surface the "Pick a model in each pane" toast for either the fully-empty or half-selected case. `hasCompareHandles` is true only inside GeneralCompareContent, so LoraCompare and the single-pane path stay unchanged. * studio/frontend: shorten compare-mode no-model-guard comment * studio/frontend: clarify compare-pane toast wording --------- Co-authored-by: danielhanchen --- .../frontend/src/features/chat/shared-composer.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/studio/frontend/src/features/chat/shared-composer.tsx b/studio/frontend/src/features/chat/shared-composer.tsx index aef004e891..11a73ee486 100644 --- a/studio/frontend/src/features/chat/shared-composer.tsx +++ b/studio/frontend/src/features/chat/shared-composer.tsx @@ -523,7 +523,19 @@ export function SharedComposer({ handlesRef.current["model1"] || handlesRef.current["model2"], ); const isGeneralizedCompare = - hasCompareHandles && Boolean(model1?.id || model2?.id); + hasCompareHandles && Boolean(model1?.id && model2?.id); + + // Generalized compare requires both panes to have a model. A + // half-selected send either races to an empty bubble with bogus + // tok/s (#5569) or leaves the empty pane with a dangling prompt. + // hasCompareHandles is true only in GeneralCompareContent, so + // LoraCompare and single-pane chats are unaffected. + if (hasCompareHandles && !isGeneralizedCompare) { + toast.error("Pick a model in each pane to compare", { + description: "Use the model dropdown above each pane, then send your prompt.", + }); + return; + } if (pendingImages.length > 0 && !isGeneralizedCompare && imageUnavailableReason) { // Single mode: the loaded model's runtime capability is known