import { Ansi, Box, NoSelect, Text } from '@hermes/ink'
import { memo } from 'react'
import { SECTION_NAMES, sectionMode } from '../domain/details.js'
import { LONG_MSG } from '../config/limits.js'
import { userDisplay } from '../domain/messages.js'
import { ROLE } from '../domain/roles.js'
import { compactPreview, hasAnsi, isPasteBackedText, stripAnsi } from '../lib/text.js'
import type { Theme } from '../theme.js'
import type { DetailsMode, Msg, SectionVisibility } from '../types.js'
import { Md } from './markdown.js'
import { ToolTrail } from './thinking.js'
export const MessageLine = memo(function MessageLine({
cols,
compact,
detailsMode = 'collapsed',
isStreaming = false,
msg,
sections,
t
}: MessageLineProps) {
if (msg.kind === 'trail' && msg.tools?.length) {
// Per-section overrides win over the global mode, so don't pre-empt on
// `detailsMode === 'hidden'` — only skip when EVERY section is hidden,
// matching ToolTrail's own internal short-circuit.
const anyVisible = SECTION_NAMES.some(s => sectionMode(s, detailsMode, sections) !== 'hidden')
return anyVisible ? (
) : null
}
if (msg.role === 'tool') {
const maxChars = Math.max(24, cols - 14)
const stripped = hasAnsi(msg.text) ? stripAnsi(msg.text) : msg.text
const preview = compactPreview(stripped, maxChars) || '(empty tool result)'
return (
{hasAnsi(msg.text) ? (
{msg.text}
) : (
{preview}
)}
)
}
const { body, glyph, prefix } = ROLE[msg.role](t)
const thinking = msg.thinking?.trim() ?? ''
const showDetails = detailsMode !== 'hidden' && (Boolean(msg.tools?.length) || Boolean(thinking))
const content = (() => {
if (msg.kind === 'slash') {
return {msg.text}
}
if (msg.role !== 'user' && hasAnsi(msg.text)) {
return {msg.text}
}
if (msg.role === 'assistant') {
return isStreaming ? {msg.text} :
}
if (msg.role === 'user' && msg.text.length > LONG_MSG && isPasteBackedText(msg.text)) {
const [head, ...rest] = userDisplay(msg.text).split('[long message]')
return (
{head}
[long message]
{rest.join('')}
)
}
return {msg.text}
})()
// Diff segments (emitted by pushInlineDiffSegment between narration
// segments) need a blank line on both sides so the patch doesn't butt up
// against the prose around it.
const isDiffSegment = msg.kind === 'diff'
return (
{showDetails && (
)}
{glyph}{' '}
{content}
)
})
interface MessageLineProps {
cols: number
compact?: boolean
detailsMode?: DetailsMode
isStreaming?: boolean
msg: Msg
sections?: SectionVisibility
t: Theme
}