import type { SuperagentMessage, SuperagentReplyTo, SuperagentToolCall } from '../../types'; // Mirrors the id of the fabricated welcome placeholder (see createWelcomeMessage); // it has no server-side message to quote. const WELCOME_MESSAGE_ID = 'welcome'; // A message can be replied to only when it carries text to quote — the welcome // placeholder and empty/tool-only bubbles don't. export function messageHasReplyableContent(message: SuperagentMessage): boolean { return message.id !== WELCOME_MESSAGE_ID && (message.content?.trim().length ?? 0) > 0; } export function getMessageFiles(message: SuperagentMessage) { return message.fileUrls ?? message.file_urls ?? []; } export function getMessageToolCalls(message: SuperagentMessage): SuperagentToolCall[] { return message.toolCalls ?? message.tool_calls ?? []; } export function getReplyTo(message: SuperagentMessage): SuperagentReplyTo | undefined { if (message.replyTo) return message.replyTo; const params = message.additional_message_params; const replyTo = params && typeof params === 'object' ? params.reply_to : undefined; if (replyTo && typeof replyTo === 'object' && typeof replyTo.content === 'string') { return { content: replyTo.content, messageId: typeof replyTo.message_id === 'string' ? replyTo.message_id : undefined, }; } return undefined; } export function getRequestedConnectors(message: SuperagentMessage): string[] { const requested = message.additional_message_params?.requested_connectors; if (!Array.isArray(requested)) return []; return requested.filter((id): id is string => typeof id === 'string' && id.length > 0); } /** * Connectors the user requested but hasn't yet sent for connection (i.e. from * messages without the `connect_requested_connectors` flag). Drives phase-2. */ export function getDeclaredConnectors(messages: SuperagentMessage[]): string[] { const ids = new Set(); for (const message of messages) { if (message.role !== 'user') continue; if (message.additional_message_params?.connect_requested_connectors) continue; getRequestedConnectors(message).forEach((id) => ids.add(id)); } return [...ids]; } function getOAuthIntegrationType(toolCall: SuperagentToolCall): string | null { if (toolCall.name !== 'request_oauth_authorization') return null; const raw = toolCall.arguments ?? toolCall.arguments_string; let args: Record | null = null; if (raw && typeof raw === 'object') { args = raw as Record; } else if (typeof raw === 'string') { try { args = JSON.parse(raw) as Record; } catch { args = null; } } const type = args?.integration_type; return typeof type === 'string' && type.length > 0 ? type : null; } /** * Connectors already sent for connection — via a `connect_requested_connectors` * message or an existing `request_oauth_authorization` tool call. Subtracted from * the auto-trigger so it never re-requests OAuth the agent already started (on * native there's no phase-1 OAuth deferral, so a first-turn OAuth call can happen). */ export function getTriggeredConnectors(messages: SuperagentMessage[]): string[] { const ids = new Set(); for (const message of messages) { if (message.additional_message_params?.connect_requested_connectors) { getRequestedConnectors(message).forEach((id) => ids.add(id)); } for (const toolCall of getMessageToolCalls(message)) { const integrationType = getOAuthIntegrationType(toolCall); if (integrationType) ids.add(integrationType); } } return [...ids]; } export function shouldShowDateSeparator( message: SuperagentMessage, previousMessage?: SuperagentMessage, ) { const currentLabel = formatDateLabel(message.createdAt); if (!currentLabel) return false; return currentLabel !== formatDateLabel(previousMessage?.createdAt); } export function formatDateLabel(value?: string) { if (!value) return ''; const date = new Date(value); if (Number.isNaN(date.getTime())) return ''; const today = new Date(); if (date.toDateString() === today.toDateString()) return 'Today'; return date.toLocaleDateString([], { day: 'numeric', month: 'short', }); } export function getFileName(url: string) { const withoutQuery = url.split('?')[0] ?? url; const fileName = withoutQuery.split('/').pop() ?? 'File'; try { return decodeURIComponent(fileName); } catch { return fileName; } } // Strong-directional character ranges for content-based text alignment. RTL // covers Hebrew + Arabic (incl. supplements and presentation forms); LTR covers // the common alphabetic scripts (Latin, Greek, Cyrillic). Neutral characters // (digits, punctuation, whitespace, symbols, emoji) match neither and are // skipped, so a leading "1. ", "> " or emoji doesn't decide the direction. const STRONG_RTL = /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB1D-\uFDFF\uFE70-\uFEFF]/; const STRONG_LTR = /[A-Za-z\u00C0-\u024F\u0370-\u03FF\u0400-\u04FF\u1E00-\u1EFF]/; /** * Direction of a message from its first strong-directional character, so text * aligns by its own language rather than the device locale: Hebrew / Arabic * content is 'rtl', everything else 'ltr'. Mixed content follows the first real * letter; text with no strong character defaults to 'ltr'. */ export function getTextDirection(text: string): 'ltr' | 'rtl' { for (const char of text) { if (STRONG_RTL.test(char)) return 'rtl'; if (STRONG_LTR.test(char)) return 'ltr'; } return 'ltr'; }