Bin29's picture
Sync from main: e764154 feat(plot-skill): add math-exam-diagram SKILL.md for exam-style math figures
abcf568
import type { StudioMessage } from '../protocol/studio-agent-types'
export function isOptimisticLocalMessageId(messageId: string): boolean {
return messageId.startsWith('local-user-') || messageId.startsWith('local-assistant-')
}
export function isNearSameTimestamp(left: string, right: string, thresholdMs = 5000): boolean {
return Math.abs(new Date(left).getTime() - new Date(right).getTime()) < thresholdMs
}
export function isEmptyAssistantPlaceholder(message: Extract<StudioMessage, { role: 'assistant' }>): boolean {
return !message.parts.some((part) => {
if (part.type === 'tool') {
return true
}
return Boolean(part.text.trim())
})
}
export function extractAssistantComparableText(message: Extract<StudioMessage, { role: 'assistant' }>): string {
return message.parts
.filter((part) => part.type === 'text' || part.type === 'reasoning')
.map((part) => part.text.trim())
.filter(Boolean)
.join('\n')
.trim()
}
export function doAssistantMessagesLookEquivalent(
left: Extract<StudioMessage, { role: 'assistant' }> | string,
right: Extract<StudioMessage, { role: 'assistant' }> | string,
): boolean {
const leftText = typeof left === 'string' ? left.trim() : extractAssistantComparableText(left)
const rightText = typeof right === 'string' ? right.trim() : extractAssistantComparableText(right)
if (!leftText || !rightText) {
return false
}
return leftText === rightText || leftText.includes(rightText) || rightText.includes(leftText)
}
export function shouldMatchOptimisticAssistantMessage(
optimisticMessage: Extract<StudioMessage, { role: 'assistant' }>,
serverMessage: Extract<StudioMessage, { role: 'assistant' }>,
thresholdMs?: number,
): boolean {
if (typeof thresholdMs === 'number' && !isNearSameTimestamp(serverMessage.createdAt, optimisticMessage.createdAt, thresholdMs)) {
return false
}
if (new Date(serverMessage.createdAt).getTime() < new Date(optimisticMessage.createdAt).getTime()) {
return false
}
if (isEmptyAssistantPlaceholder(optimisticMessage)) {
return true
}
return doAssistantMessagesLookEquivalent(optimisticMessage, serverMessage)
}
export function buildAssistantToolSignature(message: Extract<StudioMessage, { role: 'assistant' }>): string {
return message.parts
.filter((part) => part.type === 'tool')
.map((part) => {
const input = 'input' in part.state && part.state.input ? JSON.stringify(part.state.input) : ''
return `${part.tool}:${part.state.status}:${input}`
})
.join('|')
}
export function normalizeComparableText(text: string): string {
const normalized = text.trim()
if (!normalized) {
return ''
}
return normalized
.replace(/\r\n/g, '\n')
.replace(/\n{3,}/g, '\n\n')
.trim()
}
export function areAssistantMessagesEquivalent(
source: Extract<StudioMessage, { role: 'assistant' }>,
candidate: Extract<StudioMessage, { role: 'assistant' }>,
): boolean {
const sourceToolSignature = buildAssistantToolSignature(source)
const candidateToolSignature = buildAssistantToolSignature(candidate)
if (sourceToolSignature !== candidateToolSignature) {
return false
}
return doAssistantMessagesLookEquivalent(
normalizeComparableText(extractAssistantComparableText(source)),
normalizeComparableText(extractAssistantComparableText(candidate)),
)
}