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[] = [ {