mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 22:35:51 +00:00
204 lines
6.0 KiB
TypeScript
204 lines
6.0 KiB
TypeScript
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:
|
|
* <teammate-message teammate_id="alice" color="red" summary="Brief update">message content</teammate-message>
|
|
* 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 (
|
|
<Box flexDirection="column" marginTop={addMargin ? 1 : 0} width="100%">
|
|
{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 (
|
|
<React.Fragment key={index}>{planApprovalElement}</React.Fragment>
|
|
)
|
|
}
|
|
|
|
// Try to render as shutdown message (request or rejected)
|
|
const shutdownElement = tryRenderShutdownMessage(msg.content)
|
|
if (shutdownElement) {
|
|
return <React.Fragment key={index}>{shutdownElement}</React.Fragment>
|
|
}
|
|
|
|
// Try to render as task assignment message
|
|
const taskAssignmentElement = tryRenderTaskAssignmentMessage(
|
|
msg.content,
|
|
)
|
|
if (taskAssignmentElement) {
|
|
return (
|
|
<React.Fragment key={index}>{taskAssignmentElement}</React.Fragment>
|
|
)
|
|
}
|
|
|
|
// 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 (
|
|
<Box key={index} flexDirection="column" marginTop={1}>
|
|
<Text
|
|
color={inkColor}
|
|
>{`@${displayName}${figures.pointer}`}</Text>
|
|
<MessageResponse>
|
|
<Text color="success">✓</Text>
|
|
<Text>
|
|
{' '}
|
|
Completed task #{taskCompleted.taskId}
|
|
{taskCompleted.taskSubject && (
|
|
<Text dimColor> ({taskCompleted.taskSubject})</Text>
|
|
)}
|
|
</Text>
|
|
</MessageResponse>
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
// Default: plain text message (truncated)
|
|
return (
|
|
<TeammateMessageContent
|
|
key={index}
|
|
displayName={displayName}
|
|
inkColor={inkColor}
|
|
content={msg.content}
|
|
summary={msg.summary}
|
|
isTranscriptMode={isTranscriptMode}
|
|
/>
|
|
)
|
|
})}
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
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 (
|
|
<Box flexDirection="column" marginTop={1}>
|
|
<Box>
|
|
<Text color={inkColor}>{`@${displayName}${figures.pointer}`}</Text>
|
|
{summary && <Text> {summary}</Text>}
|
|
</Box>
|
|
{isTranscriptMode && (
|
|
<Box paddingLeft={2}>
|
|
<Text>
|
|
<Ansi>{content}</Ansi>
|
|
</Text>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
)
|
|
}
|