Show response model on agent hover

This commit is contained in:
Ishaan Gupta 2026-06-02 18:59:22 +05:30
parent 4ca26ef14e
commit faab975b29
2 changed files with 96 additions and 11 deletions

View file

@ -109,6 +109,22 @@ type QueuedChatMessage = {
const CHAT_QUEUE_LIMIT = 5
function normalizeModelId(value: unknown): ModelId | null {
if (typeof value !== "string") return null
return value in modelNames ? (value as ModelId) : null
}
function getMessageResponseModel(message: UIMessage): ModelId | null {
const metadata = (
message as UIMessage & { metadata?: Record<string, unknown> }
).metadata
return (
normalizeModelId(metadata?.model) ??
normalizeModelId(metadata?.responseModel) ??
null
)
}
export function ChatSidebar({
isChatOpen,
setIsChatOpen: _setIsChatOpen,
@ -180,6 +196,11 @@ export function ChatSidebar({
model: ModelId
reasoningEffort: ReasoningEffort
} | null>(null)
const pendingResponseModelsRef = useRef<ModelId[]>([])
const seenAssistantMessageIdsRef = useRef<Set<string>>(new Set())
const [responseModelByMessageId, setResponseModelByMessageId] = useState<
Record<string, ModelId>
>({})
const [regenerationBaseLength, setRegenerationBaseLength] = useState<
number | null
>(null)
@ -336,6 +357,34 @@ export function ChatSidebar({
[error, selectedModel],
)
useEffect(() => {
if (error) {
pendingResponseModelsRef.current = []
}
}, [error])
useEffect(() => {
const updates: Record<string, ModelId> = {}
for (const message of messages) {
if (message.role !== "assistant") continue
if (seenAssistantMessageIdsRef.current.has(message.id)) continue
seenAssistantMessageIdsRef.current.add(message.id)
const responseModel =
getMessageResponseModel(message) ??
pendingResponseModelsRef.current.shift() ??
null
if (responseModel) {
updates[message.id] = responseModel
}
}
if (Object.keys(updates).length === 0) return
setResponseModelByMessageId((prev) => ({ ...prev, ...updates }))
}, [messages])
const handleModelChange = useCallback(
(modelId: ModelId) => {
setSelectedModel(modelId)
@ -407,6 +456,7 @@ export function ChatSidebar({
truncateFromMessageIdRef.current = null
if (!threadId) setThreadId(fallbackChatId)
analytics.chatMessageSent({ source: "typed" })
pendingResponseModelsRef.current.push(selectedModel)
sendMessage({ text })
setInput("")
userJustSentRef.current = true
@ -420,6 +470,7 @@ export function ChatSidebar({
if (!threadId) setThreadId(fallbackChatId)
analytics.chatSuggestedQuestionClicked()
analytics.chatMessageSent({ source: "suggested" })
pendingResponseModelsRef.current.push(selectedModel)
sendMessage({ text: suggestion })
userJustSentRef.current = true
scrollToBottom()
@ -431,6 +482,7 @@ export function ChatSidebar({
status,
threadId,
scrollToBottom,
selectedModel,
],
)
@ -481,6 +533,9 @@ export function ChatSidebar({
analytics.chatMessageSent({ source: "typed" })
queuedDispatchInFlightRef.current = true
queuedDispatchSawResponseRef.current = false
pendingResponseModelsRef.current.push(
pendingSendSettingsRef.current?.model ?? selectedModelRef.current,
)
sendMessage({ text: pending.text })
window.setTimeout(() => {
truncateFromMessageIdRef.current = null
@ -550,6 +605,9 @@ export function ChatSidebar({
setFallbackChatId(newChatId)
setInput("")
setMessageQueue([])
pendingResponseModelsRef.current = []
seenAssistantMessageIdsRef.current = new Set()
setResponseModelByMessageId({})
queuedDispatchInFlightRef.current = false
queuedDispatchSawResponseRef.current = false
}, [setThreadId, setMessages])
@ -593,6 +651,7 @@ export function ChatSidebar({
role: string
parts: Array<{ type: string }>
createdAt: string
metadata?: Record<string, unknown>
}) => ({
id: m.id,
role: m.role,
@ -602,9 +661,13 @@ export function ChatSidebar({
parts: (m.parts || []).filter(
(p) => p.type === "text" || p.type === "reasoning",
),
metadata: m.metadata,
createdAt: new Date(m.createdAt),
}),
)
pendingResponseModelsRef.current = []
seenAssistantMessageIdsRef.current = new Set()
setResponseModelByMessageId({})
setThreadId(id)
setPendingThreadLoad({ id, messages: uiMessages })
setMessageQueue([])
@ -744,6 +807,7 @@ export function ChatSidebar({
if (!threadId) setThreadId(fallbackChatId)
queuedDispatchInFlightRef.current = true
queuedDispatchSawResponseRef.current = false
pendingResponseModelsRef.current.push(selectedModel)
sendMessage({ text: queuedMessage })
}
onConsumeQueuedMessage?.()
@ -799,6 +863,7 @@ export function ChatSidebar({
truncateFromMessageIdRef.current = null
queuedDispatchInFlightRef.current = true
queuedDispatchSawResponseRef.current = false
pendingResponseModelsRef.current.push(selectedModelRef.current)
sendMessage({ text: reply })
}
}, [messages, sendMessage, status])
@ -841,6 +906,7 @@ export function ChatSidebar({
model: nextMessage.model,
reasoningEffort: nextMessage.reasoningEffort,
}
pendingResponseModelsRef.current.push(nextMessage.model)
if (!threadId) setThreadId(fallbackChatId)
setMessageQueue((prev) =>
prev[0]?.id === nextMessage.id
@ -1246,6 +1312,7 @@ export function ChatSidebar({
copiedMessageId={copiedMessageId}
messageFeedback={messageFeedback}
expandedMemories={expandedMemories}
responseModel={responseModelByMessageId[message.id] ?? null}
onCopy={handleCopyMessage}
onLike={handleLikeMessage}
onDislike={handleDislikeMessage}

View file

@ -19,6 +19,7 @@ import {
} from "lucide-react"
import { cn } from "@lib/utils"
import { isWebSearchToolName } from "@/lib/chat-web-search-tools"
import { modelNames, type ModelId } from "@/lib/models"
import { RelatedMemories } from "./related-memories"
import { MessageActions } from "./message-actions"
@ -313,6 +314,7 @@ interface AgentMessageProps {
copiedMessageId: string | null
messageFeedback: Record<string, "like" | "dislike" | null>
expandedMemories: string | null
responseModel: ModelId | null
onCopy: (messageId: string, text: string) => void
onLike: (messageId: string) => void
onDislike: (messageId: string) => void
@ -327,6 +329,7 @@ export function AgentMessage({
copiedMessageId,
messageFeedback,
expandedMemories,
responseModel,
onCopy,
onLike,
onDislike,
@ -339,6 +342,9 @@ export function AgentMessage({
.filter((part) => part.type === "text")
.map((part) => part.text)
.join(" ")
const responseModelLabel = responseModel
? `${modelNames[responseModel].name} ${modelNames[responseModel].version}`
: null
return (
<div className="flex flex-col gap-1 w-full">
@ -442,17 +448,29 @@ export function AgentMessage({
})}
</div>
</div>
<MessageActions
messageId={message.id}
messageText={messageText}
isLastMessage={isLastAgentMessage}
isHovered={isHovered}
copiedMessageId={copiedMessageId}
messageFeedback={messageFeedback}
onCopy={onCopy}
onLike={onLike}
onDislike={onDislike}
/>
<div className="flex min-h-7 items-center gap-2">
<MessageActions
messageId={message.id}
messageText={messageText}
isLastMessage={isLastAgentMessage}
isHovered={isHovered}
copiedMessageId={copiedMessageId}
messageFeedback={messageFeedback}
onCopy={onCopy}
onLike={onLike}
onDislike={onDislike}
/>
{responseModelLabel && (
<span
className={cn(
"text-[10px] leading-none text-white/25 transition-opacity duration-200",
isHovered ? "opacity-100" : "opacity-0",
)}
>
{responseModelLabel}
</span>
)}
</div>
</div>
)
}