Files
stack/packages/mosaic/src/tui/components/message-list.tsx
T
jarvis c6fc090c98
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
feat(mosaic): merge @mosaic/cli into @mosaic/mosaic
@mosaic/mosaic is now the single package providing both:
- 'mosaic' binary (CLI: yolo, coord, prdy, tui, gateway, etc.)
- 'mosaic-wizard' binary (installation wizard)

Changes:
- Move packages/cli/src/* into packages/mosaic/src/
- Convert dynamic @mosaic/mosaic imports to static relative imports
- Add CLI deps (ink, react, socket.io-client, @mosaic/config) to mosaic
- Add jsx: react-jsx to mosaic's tsconfig
- Exclude packages/cli from workspace (pnpm-workspace.yaml)
- Update install.sh to install @mosaic/mosaic instead of @mosaic/cli
- Bump version to 0.0.17

This eliminates the circular dependency between @mosaic/cli and
@mosaic/mosaic that was blocking the build graph.
2026-04-04 20:07:27 -05:00

193 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<number>;
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 (
<Box flexDirection="row" marginBottom={1} marginLeft={2}>
<Text dimColor>{'⚙ '}</Text>
<Text dimColor wrap="wrap">
{msg.content}
</Text>
</Box>
);
}
function MessageBubble({
msg,
highlight,
}: {
msg: Message;
highlight?: 'match' | 'current' | undefined;
}) {
if (msg.role === 'system') {
return <SystemMessageBubble msg={msg} />;
}
const isUser = msg.role === 'user';
const prefix = isUser ? '' : '◆';
const color = isUser ? 'green' : 'cyan';
const borderIndicator =
highlight === 'current' ? (
<Text color="yellowBright" bold>
{' '}
</Text>
) : highlight === 'match' ? (
<Text color="yellow"> </Text>
) : null;
return (
<Box flexDirection="row" marginBottom={1}>
{borderIndicator}
<Box flexDirection="column">
<Box>
<Text bold color={color}>
{prefix}{' '}
</Text>
<Text bold color={color}>
{isUser ? 'you' : 'assistant'}
</Text>
<Text dimColor> {formatTime(msg.timestamp)}</Text>
</Box>
<Box marginLeft={2}>
<Text wrap="wrap">{msg.content}</Text>
</Box>
</Box>
</Box>
);
}
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 (
<Box marginLeft={2}>
{toolCall.status === 'running' ? (
<Text color="yellow">
<Spinner type="dots" />
</Text>
) : (
<Text color={color}>{icon}</Text>
)}
<Text dimColor> tool: </Text>
<Text color={color}>{toolCall.toolName}</Text>
</Box>
);
}
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 (
<Box flexDirection="column" flexGrow={1} paddingX={1}>
{isScrolledUp && hiddenAbove > 0 && (
<Box justifyContent="center">
<Text dimColor> {hiddenAbove} more messages </Text>
</Box>
)}
{messages.length === 0 && !isStreaming && (
<Box justifyContent="center" marginY={1}>
<Text dimColor>No messages yet. Type below to start a conversation.</Text>
</Box>
)}
{visibleMessages.map((msg, i) => {
const globalIndex = hiddenAbove + i;
const highlight =
globalIndex === currentHighlightIndex
? ('current' as const)
: highlightedMessageIndices?.has(globalIndex)
? ('match' as const)
: undefined;
return <MessageBubble key={globalIndex} msg={msg} highlight={highlight} />;
})}
{/* Active thinking */}
{isStreaming && currentThinkingText && (
<Box flexDirection="column" marginBottom={1} marginLeft={2}>
<Text dimColor italic>
💭 {currentThinkingText}
</Text>
</Box>
)}
{/* Active tool calls */}
{activeToolCalls.length > 0 && (
<Box flexDirection="column" marginBottom={1}>
{activeToolCalls.map((tc) => (
<ToolCallIndicator key={tc.toolCallId} toolCall={tc} />
))}
</Box>
)}
{/* Streaming response */}
{isStreaming && currentStreamText && (
<Box flexDirection="column" marginBottom={1}>
<Box>
<Text bold color="cyan">
{' '}
</Text>
<Text bold color="cyan">
assistant
</Text>
</Box>
<Box marginLeft={2}>
<Text wrap="wrap">{currentStreamText}</Text>
</Box>
</Box>
)}
{/* Waiting spinner */}
{isStreaming && !currentStreamText && activeToolCalls.length === 0 && (
<Box marginLeft={2}>
<Text color="cyan">
<Spinner type="dots" />
</Text>
<Text dimColor> thinking</Text>
</Box>
)}
</Box>
);
}