// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered import { Box, Link, Text, type TextProps } from '@anthropic/ink'; import { FilePathLink } from '../FilePathLink.js'; import { feature } from 'bun:bundle'; import * as React from 'react'; import { useState } from 'react'; import sample from 'lodash-es/sample.js'; import { BLACK_CIRCLE, REFERENCE_MARK, TEARDROP_ASTERISK } from '../../constants/figures.js'; import figures from 'figures'; import { basename } from 'path'; import { MessageResponse } from '../MessageResponse.js'; import { openPath } from '../../utils/browser.js'; /* eslint-disable @typescript-eslint/no-require-imports */ const teamMemSaved = feature('TEAMMEM') ? (require('./teamMemSaved.js') as typeof import('./teamMemSaved.js')) : null; /* eslint-enable @typescript-eslint/no-require-imports */ import { TURN_COMPLETION_VERBS } from '../../constants/turnCompletionVerbs.js'; import { useTerminalSize } from '../../hooks/useTerminalSize.js'; import type { SystemMessage, SystemStopHookSummaryMessage, SystemBridgeStatusMessage, SystemTurnDurationMessage, SystemThinkingMessage, SystemMemorySavedMessage, } from '../../types/message.js'; import { SystemAPIErrorMessage } from './SystemAPIErrorMessage.js'; import { formatDuration, formatNumber, formatSecondsShort } from '../../utils/format.js'; import { getGlobalConfig } from '../../utils/config.js'; import ThemedText from '../design-system/ThemedText.js'; import { CtrlOToExpand } from '../CtrlOToExpand.js'; import { useAppStateStore } from '../../state/AppState.js'; import { isBackgroundTask, type TaskState } from '../../tasks/types.js'; import { getPillLabel } from '../../tasks/pillLabel.js'; import { useSelectedMessageBg } from '../messageActions.js'; type Props = { message: SystemMessage; addMargin: boolean; verbose: boolean; isTranscriptMode?: boolean; }; export function SystemTextMessage({ message, addMargin, verbose, isTranscriptMode }: Props): React.ReactNode { const bg = useSelectedMessageBg(); // Turn duration messages are always shown in grey if (message.subtype === 'turn_duration') { return ; } if (message.subtype === 'memory_saved') { return ; } if (message.subtype === 'away_summary') { return ( {REFERENCE_MARK} {String(message.content ?? '')} ); } // Agents killed confirmation if (message.subtype === 'agents_killed') { return ( {BLACK_CIRCLE} All background agents stopped ); } // Thinking messages are subtle, like turn duration (ant-only) if (message.subtype === 'thinking') { if (process.env.USER_TYPE === 'ant') { return ; } return null; } if (message.subtype === 'bridge_status') { return ; } if (message.subtype === 'scheduled_task_fire') { return ( {TEARDROP_ASTERISK} {String(message.content ?? '')} ); } if (message.subtype === 'permission_retry') { return ( {TEARDROP_ASTERISK} Allowed {(message.commands as string[]).join(', ')} ); } // Stop hook summaries should always be visible const isStopHookSummary = message.subtype === 'stop_hook_summary'; if (!isStopHookSummary && !verbose && message.level === 'info') { return null; } if (message.subtype === 'api_error') { return ; } if (message.subtype === 'stop_hook_summary') { return ( ); } const content = message.content; // In case the event doesn't have a content // validation, so content can be undefined at runtime despite the types. if (typeof content !== 'string') { return null; } return ( ); } function StopHookSummaryMessage({ message, addMargin, verbose, isTranscriptMode, }: { message: SystemStopHookSummaryMessage; addMargin: boolean; verbose: boolean; isTranscriptMode?: boolean; }): React.ReactNode { const bg = useSelectedMessageBg(); const { hookCount, hookInfos } = message; const hookErrors = (message.hookErrors ?? []) as string[]; const preventedContinuation = message.preventedContinuation as boolean | undefined; const stopReason = message.stopReason as string | undefined; const { columns } = useTerminalSize(); // Prefer wall-clock time when available (hooks run in parallel) const totalDurationMs = message.totalDurationMs ?? hookInfos.reduce((sum, h) => sum + (h.durationMs ?? 0), 0); const isAnt = process.env.USER_TYPE === 'ant'; // Only show summary if there are errors or continuation was prevented // For ants: also show when hooks took > 500ms // Non-stop hooks (e.g. PreToolUse) are pre-filtered by the caller if (hookErrors.length === 0 && !preventedContinuation && !message.hookLabel) { if (!isAnt || totalDurationMs < HOOK_TIMING_DISPLAY_THRESHOLD_MS) { return null; } } const totalStr = isAnt && totalDurationMs > 0 ? ` (${formatSecondsShort(totalDurationMs)})` : ''; // Non-stop hooks (e.g. PreToolUse) render as a child line without bullet if (message.hookLabel) { return ( {' ⎿ '}Ran {hookCount} {message.hookLabel} {hookCount === 1 ? 'hook' : 'hooks'} {totalStr} {isTranscriptMode && hookInfos.map((info, idx) => { const durationStr = isAnt && info.durationMs !== undefined ? ` (${formatSecondsShort(info.durationMs)})` : ''; return ( {' ⎿ '} {info.command === 'prompt' ? `prompt: ${info.promptText || ''}` : info.command} {durationStr} ); })} ); } return ( {BLACK_CIRCLE} Ran {hookCount} {message.hookLabel ?? 'stop'} {hookCount === 1 ? 'hook' : 'hooks'} {totalStr} {!verbose && hookInfos.length > 0 && ( <> {' '} )} {verbose && hookInfos.length > 0 && hookInfos.map((info, idx) => { const durationStr = isAnt && info.durationMs !== undefined ? ` (${formatSecondsShort(info.durationMs)})` : ''; return ( ⎿   {info.command === 'prompt' ? `prompt: ${info.promptText || ''}` : info.command} {durationStr} ); })} {preventedContinuation && stopReason && ( ⎿   {stopReason} )} {hookErrors.length > 0 && hookErrors.map((err, idx) => ( ⎿   {message.hookLabel ?? 'Stop'} hook error: {err} ))} ); } function SystemTextMessageInner({ content, addMargin, dot, color, dimColor, }: { content: string; addMargin: boolean; dot: boolean; color?: TextProps['color']; dimColor?: boolean; }): React.ReactNode { const { columns } = useTerminalSize(); const bg = useSelectedMessageBg(); return ( {dot && ( {BLACK_CIRCLE} )} {content.trim()} ); } function TurnDurationMessage({ message, addMargin, }: { message: SystemTurnDurationMessage; addMargin: boolean; }): React.ReactNode { const bg = useSelectedMessageBg(); const [verb] = useState(() => sample(TURN_COMPLETION_VERBS) ?? 'Worked'); const store = useAppStateStore(); const [backgroundTaskSummary] = useState(() => { const tasks = store.getState().tasks; const running = (Object.values(tasks ?? {}) as TaskState[]).filter(isBackgroundTask); return running.length > 0 ? getPillLabel(running) : null; }); const showTurnDuration = getGlobalConfig().showTurnDuration ?? true; const duration = formatDuration(message.durationMs as number); const hasBudget = message.budgetLimit !== undefined; const budgetSuffix = (() => { if (!hasBudget) return ''; const tokens = message.budgetTokens as number; const limit = message.budgetLimit as number; const usage = tokens >= limit ? `${formatNumber(tokens)} used (${formatNumber(limit)} min ${figures.tick})` : `${formatNumber(tokens)} / ${formatNumber(limit)} (${Math.round((tokens / limit) * 100)}%)`; const nudges = (message.budgetNudges as number) > 0 ? ` \u00B7 ${message.budgetNudges as number} ${(message.budgetNudges as number) === 1 ? 'nudge' : 'nudges'}` : ''; return `${showTurnDuration ? ' \u00B7 ' : ''}${usage}${nudges}`; })(); if (!showTurnDuration && !hasBudget) { return null; } return ( {TEARDROP_ASTERISK} {showTurnDuration && `${verb} for ${duration}`} {budgetSuffix} {backgroundTaskSummary && ` \u00B7 ${backgroundTaskSummary} still running`} ); } function MemorySavedMessage({ message, addMargin, }: { message: SystemMemorySavedMessage; addMargin: boolean; }): React.ReactNode { const bg = useSelectedMessageBg(); const writtenPaths = (message.writtenPaths ?? []) as string[]; const team = feature('TEAMMEM') ? teamMemSaved!.teamMemSavedPart(message) : null; const privateCount = writtenPaths.length - (team?.count ?? 0); const parts = [ privateCount > 0 ? `${privateCount} ${privateCount === 1 ? 'memory' : 'memories'}` : null, team?.segment as React.ReactNode, ].filter(Boolean); return ( {BLACK_CIRCLE} {(message.verb as string) ?? 'Saved'} {parts.join(' \u00B7 ')} {writtenPaths.map(p => ( ))} ); } function MemoryFileRow({ path }: { path: string }): React.ReactNode { const [hover, setHover] = useState(false); return ( void openPath(path)} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}> {basename(path)} ); } function ThinkingMessage({ message, addMargin, }: { message: SystemThinkingMessage; addMargin: boolean; }): React.ReactNode { const bg = useSelectedMessageBg(); return ( {TEARDROP_ASTERISK} {String(message.content ?? '')} ); } function BridgeStatusMessage({ message, addMargin, }: { message: SystemBridgeStatusMessage; addMargin: boolean; }): React.ReactNode { const bg = useSelectedMessageBg(); const url = message.url as string; const upgradeNudge = message.upgradeNudge as string | undefined; return ( /remote-control is active. Code in CLI or at {url} {upgradeNudge && ⎿ {upgradeNudge}} ); }