184 lines
7.0 KiB
JavaScript
184 lines
7.0 KiB
JavaScript
/**
|
||
* AgentRail Component - "Pro" Protocol
|
||
* Minimalist left-rail layout for messages (Claude Code / Codex CLI style)
|
||
*
|
||
* @module ui/components/AgentRail
|
||
*/
|
||
|
||
import React from 'react';
|
||
import { Box, Text } from 'ink';
|
||
|
||
const h = React.createElement;
|
||
|
||
// ═══════════════════════════════════════════════════════════════
|
||
// ROLE COLORS - Color-coded vertical rail by role
|
||
// ═══════════════════════════════════════════════════════════════
|
||
|
||
export const RAIL_COLORS = {
|
||
system: 'yellow',
|
||
user: 'cyan',
|
||
assistant: 'gray',
|
||
error: 'red',
|
||
thinking: 'magenta',
|
||
tool: 'blue'
|
||
};
|
||
|
||
export const RAIL_ICONS = {
|
||
system: 'ℹ',
|
||
user: '❯',
|
||
assistant: '◐',
|
||
error: '!',
|
||
thinking: '◌',
|
||
tool: '⚙'
|
||
};
|
||
|
||
// ═══════════════════════════════════════════════════════════════
|
||
// SYSTEM MESSAGE - Compact single-line format
|
||
// ═══════════════════════════════════════════════════════════════
|
||
|
||
/**
|
||
* SystemMessage - Compact system notification
|
||
* Format: "ℹ SYSTEM: Message here"
|
||
*/
|
||
export const SystemMessage = ({ content, title = 'SYSTEM' }) => {
|
||
return h(Box, { marginY: 0 },
|
||
h(Text, { color: RAIL_COLORS.system }, `${RAIL_ICONS.system} `),
|
||
h(Text, { color: RAIL_COLORS.system, bold: true }, `${title}: `),
|
||
h(Text, { color: 'gray' }, content)
|
||
);
|
||
};
|
||
|
||
// ═══════════════════════════════════════════════════════════════
|
||
// USER MESSAGE - Clean prompt style
|
||
// ═══════════════════════════════════════════════════════════════
|
||
|
||
/**
|
||
* UserMessage - Clean prompt indicator
|
||
* Format: "❯ user message"
|
||
*/
|
||
export const UserMessage = ({ content }) => {
|
||
return h(Box, { marginTop: 1, marginBottom: 0 },
|
||
h(Text, { color: RAIL_COLORS.user, bold: true }, `${RAIL_ICONS.user} `),
|
||
h(Text, { color: 'white', wrap: 'wrap' }, content)
|
||
);
|
||
};
|
||
|
||
// ═══════════════════════════════════════════════════════════════
|
||
// ASSISTANT MESSAGE - Left rail with content
|
||
// ═══════════════════════════════════════════════════════════════
|
||
|
||
/**
|
||
* AssistantMessage - Rail-based layout (no box borders)
|
||
* Uses vertical line instead of full border
|
||
*/
|
||
export const AssistantMessage = ({ content, isStreaming = false, children }) => {
|
||
const railChar = isStreaming ? '┃' : '│';
|
||
const railColor = isStreaming ? 'yellow' : RAIL_COLORS.assistant;
|
||
|
||
return h(Box, {
|
||
flexDirection: 'row',
|
||
marginTop: 1,
|
||
marginBottom: 1
|
||
},
|
||
// Left rail (vertical line)
|
||
h(Box, {
|
||
width: 2,
|
||
flexShrink: 0,
|
||
flexDirection: 'column'
|
||
},
|
||
h(Text, { color: railColor }, railChar)
|
||
),
|
||
// Content area
|
||
h(Box, {
|
||
flexDirection: 'column',
|
||
flexGrow: 1,
|
||
paddingLeft: 1
|
||
},
|
||
children || h(Text, { wrap: 'wrap' }, content)
|
||
)
|
||
);
|
||
};
|
||
|
||
// ═══════════════════════════════════════════════════════════════
|
||
// THINKING INDICATOR - Dimmed spinner style
|
||
// ═══════════════════════════════════════════════════════════════
|
||
|
||
/**
|
||
* ThinkingIndicator - Shows AI reasoning steps
|
||
*/
|
||
export const ThinkingIndicator = ({ steps = [] }) => {
|
||
if (!steps || steps.length === 0) return null;
|
||
|
||
return h(Box, {
|
||
flexDirection: 'column',
|
||
marginBottom: 1,
|
||
paddingLeft: 2
|
||
},
|
||
h(Text, { color: RAIL_COLORS.thinking, dimColor: true },
|
||
`${RAIL_ICONS.thinking} Thinking (${steps.length} steps)`),
|
||
...steps.slice(-3).map((step, i) =>
|
||
h(Text, {
|
||
key: i,
|
||
color: 'gray',
|
||
dimColor: true,
|
||
wrap: 'truncate-end'
|
||
}, ` ${step.slice(0, 60)}${step.length > 60 ? '...' : ''}`)
|
||
)
|
||
);
|
||
};
|
||
|
||
// ═══════════════════════════════════════════════════════════════
|
||
// ERROR MESSAGE - Red rail with error content
|
||
// ═══════════════════════════════════════════════════════════════
|
||
|
||
/**
|
||
* ErrorMessage - Red-railed error display
|
||
*/
|
||
export const ErrorMessage = ({ content, title = 'Error' }) => {
|
||
return h(Box, {
|
||
flexDirection: 'row',
|
||
marginTop: 1
|
||
},
|
||
h(Box, { width: 2, flexShrink: 0 },
|
||
h(Text, { color: RAIL_COLORS.error }, '│')
|
||
),
|
||
h(Box, { flexDirection: 'column', paddingLeft: 1 },
|
||
h(Text, { color: RAIL_COLORS.error, bold: true }, `${RAIL_ICONS.error} ${title}`),
|
||
h(Text, { color: RAIL_COLORS.error, wrap: 'wrap' }, content)
|
||
)
|
||
);
|
||
};
|
||
|
||
// ═══════════════════════════════════════════════════════════════
|
||
// MESSAGE WRAPPER - Auto-selects component by role
|
||
// ═══════════════════════════════════════════════════════════════
|
||
|
||
/**
|
||
* MessageWrapper - Routes to correct component by role
|
||
*/
|
||
export const MessageWrapper = ({ role, content, meta, isStreaming, children }) => {
|
||
switch (role) {
|
||
case 'system':
|
||
return h(SystemMessage, { content, title: meta?.title });
|
||
case 'user':
|
||
return h(UserMessage, { content });
|
||
case 'assistant':
|
||
return h(AssistantMessage, { content, isStreaming, children });
|
||
case 'error':
|
||
return h(ErrorMessage, { content, title: meta?.title });
|
||
default:
|
||
return h(Text, { wrap: 'wrap' }, content);
|
||
}
|
||
};
|
||
|
||
export default {
|
||
RAIL_COLORS,
|
||
RAIL_ICONS,
|
||
SystemMessage,
|
||
UserMessage,
|
||
AssistantMessage,
|
||
ThinkingIndicator,
|
||
ErrorMessage,
|
||
MessageWrapper
|
||
};
|