diff --git a/frontend/src/app/pages/AgentChat/ChatInput/hooks/useChatInputModel.ts b/frontend/src/app/pages/AgentChat/ChatInput/hooks/useChatInputModel.ts index c491e5e9..1e7682d8 100644 --- a/frontend/src/app/pages/AgentChat/ChatInput/hooks/useChatInputModel.ts +++ b/frontend/src/app/pages/AgentChat/ChatInput/hooks/useChatInputModel.ts @@ -35,6 +35,7 @@ export function useChatInputModel(model: string) { max_completion_tokens: m.max_completion_tokens ?? null, tiers: Array.isArray(m.tiers) && m.tiers.length === 3 ? m.tiers : undefined, billing_kind: m.billing_kind, + api: m.api, })); if (enriched.length) grouped[prov] = sortModelsForPicker(enriched); for (const m of enriched) { diff --git a/frontend/src/app/pages/AgentChat/bubbles/MessageBubble.tsx b/frontend/src/app/pages/AgentChat/bubbles/MessageBubble.tsx index 6c77038e..7a227dc8 100644 --- a/frontend/src/app/pages/AgentChat/bubbles/MessageBubble.tsx +++ b/frontend/src/app/pages/AgentChat/bubbles/MessageBubble.tsx @@ -800,12 +800,23 @@ const ThinkingBubble: React.FC<{ ); }; +const REASONING_VARIANTS = [ + "It's still thinking, we just aren't allowed to peek behind the curtain.", + "Wheels are turning, but this provider keeps its thoughts private.", + "Brain's busy back there; the provider just isn't letting us listen in.", + "Mulling it over quietly. Only Claude shows its work out loud.", + "Thinking happened, just not in the open. (GPT and Gemini play their cards close.)", + "Reasoning's underway, but this provider doesn't broadcast it. Trust the process.", +]; + // Shown when the model thought but the provider didn't expose the text. const ProviderReasoningExplanation: React.FC<{ isStreaming: boolean; tokens: number | null; elapsedMs: number | null; }> = ({ isStreaming, tokens, elapsedMs }) => { + // Hook first, unconditionally: isStreaming flips false on a mounted instance when reasoning ends. + const idx = useMemo(() => Math.floor(Math.random() * REASONING_VARIANTS.length), []); if (isStreaming) { return ( @@ -826,16 +837,7 @@ const ProviderReasoningExplanation: React.FC<{ } return segs.join(', '); })(); - const variants = [ - "It's still thinking, we just aren't allowed to peek behind the curtain.", - "Wheels are turning, but this provider keeps its thoughts private.", - "Brain's busy back there; the provider just isn't letting us listen in.", - "Mulling it over quietly. Only Claude shows its work out loud.", - "Thinking happened, just not in the open. (GPT and Gemini play their cards close.)", - "Reasoning's underway, but this provider doesn't broadcast it. Trust the process.", - ]; - const idx = useMemo(() => Math.floor(Math.random() * variants.length), []); - const line = variants[idx]; + const line = REASONING_VARIANTS[idx]; return ( @@ -858,12 +860,11 @@ interface Props { revealRef?: React.RefObject; } -const MessageBubble: React.FC = React.memo(({ message, editing = false, onSaveEdit, onCancelEdit, isStreaming, dynamicTurnLabel, viewportHeight = 0, viewportWidth = 0, scrollRoot = null, revealRef }) => { +// Role dispatcher with a FIXED hook count: the chat body's ten-plus hooks live in ChatMessageBubble +// below, so a mounted instance whose role flips can never trip React's positional hook matching. +const MessageBubble: React.FC = React.memo((props) => { const c = useClaudeTokens(); - const dispatch = useAppDispatch(); - const [pickerOpen, setPickerOpen] = useState(false); - const bubbleRootRef = React.useRef(null); - const contentRef = React.useRef(null); + const { message, isStreaming, dynamicTurnLabel, revealRef } = props; const { role, content } = message; if (role === 'system') { @@ -904,6 +905,17 @@ const MessageBubble: React.FC = React.memo(({ message, editing = false, o return null; } + return ; +}); + +const ChatMessageBubble: React.FC = ({ message, editing = false, onSaveEdit, onCancelEdit, isStreaming, dynamicTurnLabel, viewportHeight = 0, viewportWidth = 0, scrollRoot = null, revealRef }) => { + const c = useClaudeTokens(); + const dispatch = useAppDispatch(); + const [pickerOpen, setPickerOpen] = useState(false); + const bubbleRootRef = React.useRef(null); + const contentRef = React.useRef(null); + const { role, content } = message; + const isUser = role === 'user'; const rawText = typeof content === 'string' ? content : JSON.stringify(content); const { userMessage: displayText, elements: selectedElements } = isUser @@ -1303,6 +1315,6 @@ const MessageBubble: React.FC = React.memo(({ message, editing = false, o /> ); -}); +}; export default MessageBubble;