import React from 'react'; import { Box, Text } from 'ink'; import Spinner from 'ink-spinner'; import type { Message, ToolCall } from '../hooks/use-socket.js'; export interface MessageListProps { messages: Message[]; isStreaming: boolean; currentStreamText: string; currentThinkingText: string; activeToolCalls: ToolCall[]; scrollOffset?: number; viewportSize?: number; isScrolledUp?: boolean; highlightedMessageIndices?: Set; currentHighlightIndex?: number; } function formatTime(date: Date): string { return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false, }); } function SystemMessageBubble({ msg }: { msg: Message }) { return ( {'⚙ '} {msg.content} ); } function MessageBubble({ msg, highlight, }: { msg: Message; highlight?: 'match' | 'current' | undefined; }) { if (msg.role === 'system') { return ; } const isUser = msg.role === 'user'; const prefix = isUser ? '❯' : '◆'; const color = isUser ? 'green' : 'cyan'; const borderIndicator = highlight === 'current' ? ( ▌{' '} ) : highlight === 'match' ? ( ) : null; return ( {borderIndicator} {prefix}{' '} {isUser ? 'you' : 'assistant'} {formatTime(msg.timestamp)} {msg.content} ); } function ToolCallIndicator({ toolCall }: { toolCall: ToolCall }) { const icon = toolCall.status === 'running' ? null : toolCall.status === 'success' ? '✓' : '✗'; const color = toolCall.status === 'running' ? 'yellow' : toolCall.status === 'success' ? 'green' : 'red'; return ( {toolCall.status === 'running' ? ( ) : ( {icon} )} tool: {toolCall.toolName} ); } export function MessageList({ messages, isStreaming, currentStreamText, currentThinkingText, activeToolCalls, scrollOffset, viewportSize, isScrolledUp, highlightedMessageIndices, currentHighlightIndex, }: MessageListProps) { const useSlicing = scrollOffset != null && viewportSize != null; const visibleMessages = useSlicing ? messages.slice(scrollOffset, scrollOffset + viewportSize) : messages; const hiddenAbove = useSlicing ? scrollOffset : 0; return ( {isScrolledUp && hiddenAbove > 0 && ( ↑ {hiddenAbove} more messages ↑ )} {messages.length === 0 && !isStreaming && ( No messages yet. Type below to start a conversation. )} {visibleMessages.map((msg, i) => { const globalIndex = hiddenAbove + i; const highlight = globalIndex === currentHighlightIndex ? ('current' as const) : highlightedMessageIndices?.has(globalIndex) ? ('match' as const) : undefined; return ; })} {/* Active thinking */} {isStreaming && currentThinkingText && ( 💭 {currentThinkingText} )} {/* Active tool calls */} {activeToolCalls.length > 0 && ( {activeToolCalls.map((tc) => ( ))} )} {/* Streaming response */} {isStreaming && currentStreamText && ( ◆{' '} assistant {currentStreamText} )} {/* Waiting spinner */} {isStreaming && !currentStreamText && activeToolCalls.length === 0 && ( thinking… )} ); }