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 <michaelhan2050@gmail.com>
This commit is contained in:
Daniel Han 2026-05-19 06:56:30 -07:00 committed by GitHub
commit feadfd5c1b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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