import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs' import figures from 'figures' import * as React from 'react' import { TEAMMATE_MESSAGE_TAG } from '../../constants/xml.js' import { Ansi, Box, Text, type TextProps } from '../../ink.js' import { toInkColor } from '../../utils/ink.js' import { jsonParse } from '../../utils/slowOperations.js' import { isShutdownApproved } from '../../utils/teammateMailbox.js' import { MessageResponse } from '../MessageResponse.js' import { tryRenderPlanApprovalMessage } from './PlanApprovalMessage.js' import { tryRenderShutdownMessage } from './ShutdownMessage.js' import { tryRenderTaskAssignmentMessage } from './TaskAssignmentMessage.js' type Props = { addMargin: boolean param: TextBlockParam isTranscriptMode?: boolean } type ParsedMessage = { teammateId: string content: string color?: string summary?: string } const TEAMMATE_MSG_REGEX = new RegExp( `<${TEAMMATE_MESSAGE_TAG}\\s+teammate_id="([^"]+)"(?:\\s+color="([^"]+)")?(?:\\s+summary="([^"]+)")?>\\n?([\\s\\S]*?)\\n?<\\/${TEAMMATE_MESSAGE_TAG}>`, 'g', ) /** * Parse all teammate messages from XML format: * message content * Supports multiple messages in a single text block. */ function parseTeammateMessages(text: string): ParsedMessage[] { const messages: ParsedMessage[] = [] // Use matchAll to find all matches (this is a RegExp method, not child_process) for (const match of text.matchAll(TEAMMATE_MSG_REGEX)) { if (match[1] && match[4]) { messages.push({ teammateId: match[1], color: match[2], // may be undefined summary: match[3], // may be undefined content: match[4].trim(), }) } } return messages } function getDisplayName(teammateId: string): string { if (teammateId === 'leader') { return 'leader' } return teammateId } export function UserTeammateMessage({ addMargin, param: { text }, isTranscriptMode, }: Props): React.ReactNode { const messages = parseTeammateMessages(text).filter(msg => { // Pre-filter shutdown lifecycle messages to avoid empty wrapper // Box elements creating blank lines between model turns if (isShutdownApproved(msg.content)) { return false } try { const parsed = jsonParse(msg.content) if (parsed?.type === 'teammate_terminated') return false } catch { // Not JSON, keep the message } return true }) if (messages.length === 0) { return null } return ( {messages.map((msg, index) => { const inkColor = toInkColor(msg.color) const displayName = getDisplayName(msg.teammateId) // Try to render as plan approval message (request or response) const planApprovalElement = tryRenderPlanApprovalMessage( msg.content, displayName, ) if (planApprovalElement) { return ( {planApprovalElement} ) } // Try to render as shutdown message (request or rejected) const shutdownElement = tryRenderShutdownMessage(msg.content) if (shutdownElement) { return {shutdownElement} } // Try to render as task assignment message const taskAssignmentElement = tryRenderTaskAssignmentMessage( msg.content, ) if (taskAssignmentElement) { return ( {taskAssignmentElement} ) } // Try to parse as structured JSON message let parsedIdleNotification: { type?: string } | null = null try { parsedIdleNotification = jsonParse(msg.content) } catch { // Not JSON } // Hide idle notifications - they are processed silently if (parsedIdleNotification?.type === 'idle_notification') { return null } // Task completed notification - show which task was completed if (parsedIdleNotification?.type === 'task_completed') { const taskCompleted = parsedIdleNotification as { type: string from: string taskId: string taskSubject?: string } return ( {`@${displayName}${figures.pointer}`} {' '} Completed task #{taskCompleted.taskId} {taskCompleted.taskSubject && ( ({taskCompleted.taskSubject}) )} ) } // Default: plain text message (truncated) return ( ) })} ) } type TeammateMessageContentProps = { displayName: string inkColor: TextProps['color'] content: string summary?: string isTranscriptMode?: boolean } export function TeammateMessageContent({ displayName, inkColor, content, summary, isTranscriptMode, }: TeammateMessageContentProps): React.ReactNode { return ( {`@${displayName}${figures.pointer}`} {summary && {summary}} {isTranscriptMode && ( {content} )} ) }