From 5e4e0ab960abc64b0dfa9bfc4322e7bf2078845f Mon Sep 17 00:00:00 2001 From: rcourtman Date: Thu, 11 Jun 2026 15:37:36 +0100 Subject: [PATCH] Gate the selected-route row at its render site and compare display labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dedup landed in isRenderableStreamEvent, but that predicate only feeds the has-any-events gate; the model_switch Match rendered on its own condition, so live turns still showed the duplicate row. Extract shouldRenderModelRouteEvent, use it at the Match itself, and compare display labels instead of raw ids — the done event rewrites message.model to a provider-reported id that can differ in raw form while reading identically to the user. --- .../src/components/AI/Chat/MessageItem.tsx | 41 +++++++++++-------- .../AI/Chat/__tests__/MessageItem.test.tsx | 28 +++++++++++++ 2 files changed, 51 insertions(+), 18 deletions(-) diff --git a/frontend-modern/src/components/AI/Chat/MessageItem.tsx b/frontend-modern/src/components/AI/Chat/MessageItem.tsx index 859a21e6d7..51dbae9238 100644 --- a/frontend-modern/src/components/AI/Chat/MessageItem.tsx +++ b/frontend-modern/src/components/AI/Chat/MessageItem.tsx @@ -257,6 +257,26 @@ export const MessageItem: Component = (props) => { ); const isSelectedModelRouteEvent = (evt: StreamDisplayEvent) => evt.type === 'model_switch' && evt.modelEvent === 'selected' && !evt.failedModel?.trim(); + // A plain selected-route row repeats the route badge already shown in the + // turn header. Keep it only when it adds information: the header has no + // route, the route reads differently from the turn's final model, or a + // later switch makes the starting route part of the turn's story. Compare + // display labels, not raw ids — the done event can rewrite message.model to + // a provider-reported id that differs in raw form while reading identically + // to the user. + const shouldRenderModelRouteEvent = (evt: StreamDisplayEvent) => { + if (evt.type !== 'model_switch' || !evt.model?.trim()) return false; + if (!isSelectedModelRouteEvent(evt)) return true; + const hasRouteSwitch = (props.message.streamEvents || []).some( + (event) => + event.type === 'model_switch' && + !!event.model?.trim() && + !isSelectedModelRouteEvent(event), + ); + return ( + hasRouteSwitch || !messageModelLabel() || modelRouteLabel(evt.model) !== messageModelLabel() + ); + }; const isConcreteStreamActivity = (evt: StreamDisplayEvent) => { switch (evt.type) { case 'workflow_status': @@ -304,21 +324,8 @@ export const MessageItem: Component = (props) => { return !!evt.pendingTool; case 'tool_cancel': return !!evt.toolCancel; - case 'model_switch': { - if (!evt.model?.trim()) return false; - if (!isSelectedModelRouteEvent(evt)) return true; - // A plain selected-route row repeats the route badge already shown in - // the turn header. Keep it only when it adds information: the header - // has no route, the route differs from the turn's final model, or a - // later switch makes the starting route part of the turn's story. - const hasRouteSwitch = (props.message.streamEvents || []).some( - (event) => - event.type === 'model_switch' && - !!event.model?.trim() && - !isSelectedModelRouteEvent(event), - ); - return hasRouteSwitch || !props.message.model || evt.model !== props.message.model; - } + case 'model_switch': + return shouldRenderModelRouteEvent(evt); case 'approval': return !!evt.approval; case 'question': @@ -770,9 +777,7 @@ export const MessageItem: Component = (props) => { {(modelEvent) => { const event = modelEvent(); diff --git a/frontend-modern/src/components/AI/Chat/__tests__/MessageItem.test.tsx b/frontend-modern/src/components/AI/Chat/__tests__/MessageItem.test.tsx index f36280b878..6c5501b112 100644 --- a/frontend-modern/src/components/AI/Chat/__tests__/MessageItem.test.tsx +++ b/frontend-modern/src/components/AI/Chat/__tests__/MessageItem.test.tsx @@ -1200,6 +1200,34 @@ describe('MessageItem', () => { expect(screen.getByText('Qwen 3.7 Plus via OpenRouter')).toBeInTheDocument(); }); + it('hides the selected-route row when raw ids differ but the display label matches the badge', () => { + const events: StreamDisplayEvent[] = [ + { + type: 'model_switch', + model: 'openrouter:qwen/qwen3.7-plus', + modelEvent: 'selected', + }, + ]; + + render(() => ( + 'Qwen 3.7 Plus via OpenRouter'} + {...makeHandlers()} + /> + )); + + expect( + screen.queryByRole('status', { name: 'Assistant model route selected' }), + ).not.toBeInTheDocument(); + }); + it('keeps the selected-route row when a later switch makes it part of the turn story', () => { const events: StreamDisplayEvent[] = [ {